Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
636 lines
23 KiB
TypeScript
636 lines
23 KiB
TypeScript
import type { Plugin } from "@opencode-ai/plugin";
|
|
import {
|
|
createTodoContinuationEnforcer,
|
|
createContextWindowMonitorHook,
|
|
createSessionRecoveryHook,
|
|
createSessionNotification,
|
|
createCommentCheckerHooks,
|
|
createToolOutputTruncatorHook,
|
|
createDirectoryAgentsInjectorHook,
|
|
createDirectoryReadmeInjectorHook,
|
|
createEmptyTaskResponseDetectorHook,
|
|
createThinkModeHook,
|
|
createClaudeCodeHooksHook,
|
|
createAnthropicContextWindowLimitRecoveryHook,
|
|
|
|
createCompactionContextInjector,
|
|
createRulesInjectorHook,
|
|
createBackgroundNotificationHook,
|
|
createAutoUpdateCheckerHook,
|
|
createKeywordDetectorHook,
|
|
createAgentUsageReminderHook,
|
|
createNonInteractiveEnvHook,
|
|
createInteractiveBashSessionHook,
|
|
|
|
createThinkingBlockValidatorHook,
|
|
createCategorySkillReminderHook,
|
|
createRalphLoopHook,
|
|
createAutoSlashCommandHook,
|
|
createEditErrorRecoveryHook,
|
|
createDelegateTaskRetryHook,
|
|
createTaskResumeInfoHook,
|
|
createStartWorkHook,
|
|
createAtlasHook,
|
|
createPrometheusMdOnlyHook,
|
|
createSisyphusJuniorNotepadHook,
|
|
createQuestionLabelTruncatorHook,
|
|
} from "./hooks";
|
|
import {
|
|
contextCollector,
|
|
createContextInjectorMessagesTransformHook,
|
|
} from "./features/context-injector";
|
|
import { applyAgentVariant, resolveAgentVariant } from "./shared/agent-variant";
|
|
import { createFirstMessageVariantGate } from "./shared/first-message-variant";
|
|
import {
|
|
discoverUserClaudeSkills,
|
|
discoverProjectClaudeSkills,
|
|
discoverOpencodeGlobalSkills,
|
|
discoverOpencodeProjectSkills,
|
|
mergeSkills,
|
|
} from "./features/opencode-skill-loader";
|
|
import { createBuiltinSkills } from "./features/builtin-skills";
|
|
import { getSystemMcpServerNames } from "./features/claude-code-mcp-loader";
|
|
import {
|
|
setMainSession,
|
|
getMainSessionID,
|
|
setSessionAgent,
|
|
updateSessionAgent,
|
|
clearSessionAgent,
|
|
} from "./features/claude-code-session-state";
|
|
import {
|
|
builtinTools,
|
|
createCallOmoAgent,
|
|
createBackgroundTools,
|
|
createLookAt,
|
|
createSkillTool,
|
|
createSkillMcpTool,
|
|
createSlashcommandTool,
|
|
discoverCommandsSync,
|
|
sessionExists,
|
|
createDelegateTask,
|
|
interactive_bash,
|
|
startTmuxCheck,
|
|
lspManager,
|
|
} from "./tools";
|
|
import { BackgroundManager } from "./features/background-agent";
|
|
import { SkillMcpManager } from "./features/skill-mcp-manager";
|
|
import { initTaskToastManager } from "./features/task-toast-manager";
|
|
import { TmuxSessionManager } from "./features/tmux-subagent";
|
|
import { type HookName } from "./config";
|
|
import { log, detectExternalNotificationPlugin, getNotificationConflictWarning, resetMessageCursor, includesCaseInsensitive } from "./shared";
|
|
import { loadPluginConfig } from "./plugin-config";
|
|
import { createModelCacheState, getModelLimit } from "./plugin-state";
|
|
import { createConfigHandler } from "./plugin-handlers";
|
|
|
|
const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|
log("[OhMyOpenCodePlugin] ENTRY - plugin loading", { directory: ctx.directory })
|
|
// Start background tmux check immediately
|
|
startTmuxCheck();
|
|
|
|
const pluginConfig = loadPluginConfig(ctx.directory, ctx);
|
|
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
|
const firstMessageVariantGate = createFirstMessageVariantGate();
|
|
|
|
const tmuxConfig = {
|
|
enabled: pluginConfig.tmux?.enabled ?? false,
|
|
layout: pluginConfig.tmux?.layout ?? 'main-vertical',
|
|
main_pane_size: pluginConfig.tmux?.main_pane_size ?? 60,
|
|
} as const;
|
|
const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName);
|
|
|
|
const modelCacheState = createModelCacheState();
|
|
|
|
const contextWindowMonitor = isHookEnabled("context-window-monitor")
|
|
? createContextWindowMonitorHook(ctx)
|
|
: null;
|
|
const sessionRecovery = isHookEnabled("session-recovery")
|
|
? createSessionRecoveryHook(ctx, { experimental: pluginConfig.experimental })
|
|
: null;
|
|
|
|
// Check for conflicting notification plugins before creating session-notification
|
|
let sessionNotification = null;
|
|
if (isHookEnabled("session-notification")) {
|
|
const forceEnable = pluginConfig.notification?.force_enable ?? false;
|
|
const externalNotifier = detectExternalNotificationPlugin(ctx.directory);
|
|
|
|
if (externalNotifier.detected && !forceEnable) {
|
|
// External notification plugin detected - skip our notification to avoid conflicts
|
|
console.warn(getNotificationConflictWarning(externalNotifier.pluginName!));
|
|
log("session-notification disabled due to external notifier conflict", {
|
|
detected: externalNotifier.pluginName,
|
|
allPlugins: externalNotifier.allPlugins,
|
|
});
|
|
} else {
|
|
sessionNotification = createSessionNotification(ctx);
|
|
}
|
|
}
|
|
|
|
const commentChecker = isHookEnabled("comment-checker")
|
|
? createCommentCheckerHooks(pluginConfig.comment_checker)
|
|
: null;
|
|
const toolOutputTruncator = isHookEnabled("tool-output-truncator")
|
|
? createToolOutputTruncatorHook(ctx, {
|
|
experimental: pluginConfig.experimental,
|
|
})
|
|
: null;
|
|
const directoryAgentsInjector = isHookEnabled("directory-agents-injector")
|
|
? createDirectoryAgentsInjectorHook(ctx)
|
|
: null;
|
|
const directoryReadmeInjector = isHookEnabled("directory-readme-injector")
|
|
? createDirectoryReadmeInjectorHook(ctx)
|
|
: null;
|
|
const emptyTaskResponseDetector = isHookEnabled("empty-task-response-detector")
|
|
? createEmptyTaskResponseDetectorHook(ctx)
|
|
: null;
|
|
const thinkMode = isHookEnabled("think-mode") ? createThinkModeHook() : null;
|
|
const claudeCodeHooks = createClaudeCodeHooksHook(
|
|
ctx,
|
|
{
|
|
disabledHooks: (pluginConfig.claude_code?.hooks ?? true) ? undefined : true,
|
|
keywordDetectorDisabled: !isHookEnabled("keyword-detector"),
|
|
},
|
|
contextCollector
|
|
);
|
|
const anthropicContextWindowLimitRecovery = isHookEnabled(
|
|
"anthropic-context-window-limit-recovery"
|
|
)
|
|
? createAnthropicContextWindowLimitRecoveryHook(ctx, {
|
|
experimental: pluginConfig.experimental,
|
|
})
|
|
: null;
|
|
const compactionContextInjector = isHookEnabled("compaction-context-injector")
|
|
? createCompactionContextInjector()
|
|
: undefined;
|
|
const rulesInjector = isHookEnabled("rules-injector")
|
|
? createRulesInjectorHook(ctx)
|
|
: null;
|
|
const autoUpdateChecker = isHookEnabled("auto-update-checker")
|
|
? createAutoUpdateCheckerHook(ctx, {
|
|
showStartupToast: isHookEnabled("startup-toast"),
|
|
isSisyphusEnabled: pluginConfig.sisyphus_agent?.disabled !== true,
|
|
autoUpdate: pluginConfig.auto_update ?? true,
|
|
})
|
|
: null;
|
|
const keywordDetector = isHookEnabled("keyword-detector")
|
|
? createKeywordDetectorHook(ctx, contextCollector)
|
|
: null;
|
|
const contextInjectorMessagesTransform =
|
|
createContextInjectorMessagesTransformHook(contextCollector);
|
|
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
|
|
? createAgentUsageReminderHook(ctx)
|
|
: null;
|
|
const nonInteractiveEnv = isHookEnabled("non-interactive-env")
|
|
? createNonInteractiveEnvHook(ctx)
|
|
: null;
|
|
const interactiveBashSession = isHookEnabled("interactive-bash-session")
|
|
? createInteractiveBashSessionHook(ctx)
|
|
: null;
|
|
|
|
const thinkingBlockValidator = isHookEnabled("thinking-block-validator")
|
|
? createThinkingBlockValidatorHook()
|
|
: null;
|
|
|
|
const categorySkillReminder = isHookEnabled("category-skill-reminder")
|
|
? createCategorySkillReminderHook(ctx)
|
|
: null;
|
|
|
|
const ralphLoop = isHookEnabled("ralph-loop")
|
|
? createRalphLoopHook(ctx, {
|
|
config: pluginConfig.ralph_loop,
|
|
checkSessionExists: async (sessionId) => sessionExists(sessionId),
|
|
})
|
|
: null;
|
|
|
|
const editErrorRecovery = isHookEnabled("edit-error-recovery")
|
|
? createEditErrorRecoveryHook(ctx)
|
|
: null;
|
|
|
|
const delegateTaskRetry = isHookEnabled("delegate-task-retry")
|
|
? createDelegateTaskRetryHook(ctx)
|
|
: null;
|
|
|
|
const startWork = isHookEnabled("start-work")
|
|
? createStartWorkHook(ctx)
|
|
: null;
|
|
|
|
const prometheusMdOnly = isHookEnabled("prometheus-md-only")
|
|
? createPrometheusMdOnlyHook(ctx)
|
|
: null;
|
|
|
|
const sisyphusJuniorNotepad = isHookEnabled("sisyphus-junior-notepad")
|
|
? createSisyphusJuniorNotepadHook(ctx)
|
|
: null;
|
|
|
|
const questionLabelTruncator = createQuestionLabelTruncatorHook();
|
|
|
|
const taskResumeInfo = createTaskResumeInfoHook();
|
|
|
|
const backgroundManager = new BackgroundManager(ctx, pluginConfig.background_task);
|
|
|
|
const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
|
|
|
|
const atlasHook = isHookEnabled("atlas")
|
|
? createAtlasHook(ctx, { directory: ctx.directory, backgroundManager })
|
|
: null;
|
|
|
|
initTaskToastManager(ctx.client);
|
|
|
|
const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer")
|
|
? createTodoContinuationEnforcer(ctx, { backgroundManager })
|
|
: null;
|
|
|
|
if (sessionRecovery && todoContinuationEnforcer) {
|
|
sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
|
|
sessionRecovery.setOnRecoveryCompleteCallback(
|
|
todoContinuationEnforcer.markRecoveryComplete
|
|
);
|
|
}
|
|
|
|
const backgroundNotificationHook = isHookEnabled("background-notification")
|
|
? createBackgroundNotificationHook(backgroundManager)
|
|
: null;
|
|
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
|
|
|
|
const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
|
|
const isMultimodalLookerEnabled = !includesCaseInsensitive(
|
|
pluginConfig.disabled_agents ?? [],
|
|
"multimodal-looker"
|
|
);
|
|
const lookAt = isMultimodalLookerEnabled ? createLookAt(ctx) : null;
|
|
const browserProvider = pluginConfig.browser_automation_engine?.provider ?? "playwright";
|
|
const delegateTask = createDelegateTask({
|
|
manager: backgroundManager,
|
|
client: ctx.client,
|
|
directory: ctx.directory,
|
|
userCategories: pluginConfig.categories,
|
|
gitMasterConfig: pluginConfig.git_master,
|
|
sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model,
|
|
browserProvider,
|
|
});
|
|
const disabledSkills = new Set(pluginConfig.disabled_skills ?? []);
|
|
const systemMcpNames = getSystemMcpServerNames();
|
|
const builtinSkills = createBuiltinSkills({ browserProvider }).filter((skill) => {
|
|
if (disabledSkills.has(skill.name as never)) return false;
|
|
if (skill.mcpConfig) {
|
|
for (const mcpName of Object.keys(skill.mcpConfig)) {
|
|
if (systemMcpNames.has(mcpName)) return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
const includeClaudeSkills = pluginConfig.claude_code?.skills !== false;
|
|
const [userSkills, globalSkills, projectSkills, opencodeProjectSkills] = await Promise.all([
|
|
includeClaudeSkills ? discoverUserClaudeSkills() : Promise.resolve([]),
|
|
discoverOpencodeGlobalSkills(),
|
|
includeClaudeSkills ? discoverProjectClaudeSkills() : Promise.resolve([]),
|
|
discoverOpencodeProjectSkills(),
|
|
]);
|
|
const mergedSkills = mergeSkills(
|
|
builtinSkills,
|
|
pluginConfig.skills,
|
|
userSkills,
|
|
globalSkills,
|
|
projectSkills,
|
|
opencodeProjectSkills
|
|
);
|
|
const skillMcpManager = new SkillMcpManager();
|
|
const getSessionIDForMcp = () => getMainSessionID() || "";
|
|
const skillTool = createSkillTool({
|
|
skills: mergedSkills,
|
|
mcpManager: skillMcpManager,
|
|
getSessionID: getSessionIDForMcp,
|
|
gitMasterConfig: pluginConfig.git_master,
|
|
});
|
|
const skillMcpTool = createSkillMcpTool({
|
|
manager: skillMcpManager,
|
|
getLoadedSkills: () => mergedSkills,
|
|
getSessionID: getSessionIDForMcp,
|
|
});
|
|
|
|
const commands = discoverCommandsSync();
|
|
const slashcommandTool = createSlashcommandTool({
|
|
commands,
|
|
skills: mergedSkills,
|
|
});
|
|
|
|
const autoSlashCommand = isHookEnabled("auto-slash-command")
|
|
? createAutoSlashCommandHook({ skills: mergedSkills })
|
|
: null;
|
|
|
|
const configHandler = createConfigHandler({
|
|
ctx: { directory: ctx.directory, client: ctx.client },
|
|
pluginConfig,
|
|
modelCacheState,
|
|
});
|
|
|
|
return {
|
|
tool: {
|
|
...builtinTools,
|
|
...backgroundTools,
|
|
call_omo_agent: callOmoAgent,
|
|
...(lookAt ? { look_at: lookAt } : {}),
|
|
delegate_task: delegateTask,
|
|
skill: skillTool,
|
|
skill_mcp: skillMcpTool,
|
|
slashcommand: slashcommandTool,
|
|
interactive_bash,
|
|
},
|
|
|
|
"chat.message": async (input, output) => {
|
|
if (input.agent) {
|
|
setSessionAgent(input.sessionID, input.agent);
|
|
}
|
|
|
|
const message = (output as { message: { variant?: string } }).message
|
|
if (firstMessageVariantGate.shouldOverride(input.sessionID)) {
|
|
const variant = resolveAgentVariant(pluginConfig, input.agent)
|
|
if (variant !== undefined) {
|
|
message.variant = variant
|
|
}
|
|
firstMessageVariantGate.markApplied(input.sessionID)
|
|
} else {
|
|
applyAgentVariant(pluginConfig, input.agent, message)
|
|
}
|
|
|
|
await keywordDetector?.["chat.message"]?.(input, output);
|
|
await claudeCodeHooks["chat.message"]?.(input, output);
|
|
await autoSlashCommand?.["chat.message"]?.(input, output);
|
|
await startWork?.["chat.message"]?.(input, output);
|
|
|
|
if (ralphLoop) {
|
|
const parts = (
|
|
output as { parts?: Array<{ type: string; text?: string }> }
|
|
).parts;
|
|
const promptText =
|
|
parts
|
|
?.filter((p) => p.type === "text" && p.text)
|
|
.map((p) => p.text)
|
|
.join("\n")
|
|
.trim() || "";
|
|
|
|
const isRalphLoopTemplate =
|
|
promptText.includes("You are starting a Ralph Loop") &&
|
|
promptText.includes("<user-task>");
|
|
const isCancelRalphTemplate = promptText.includes(
|
|
"Cancel the currently active Ralph Loop"
|
|
);
|
|
|
|
if (isRalphLoopTemplate) {
|
|
const taskMatch = promptText.match(
|
|
/<user-task>\s*([\s\S]*?)\s*<\/user-task>/i
|
|
);
|
|
const rawTask = taskMatch?.[1]?.trim() || "";
|
|
|
|
const quotedMatch = rawTask.match(/^["'](.+?)["']/);
|
|
const prompt =
|
|
quotedMatch?.[1] ||
|
|
rawTask.split(/\s+--/)[0]?.trim() ||
|
|
"Complete the task as instructed";
|
|
|
|
const maxIterMatch = rawTask.match(/--max-iterations=(\d+)/i);
|
|
const promiseMatch = rawTask.match(
|
|
/--completion-promise=["']?([^"'\s]+)["']?/i
|
|
);
|
|
|
|
log("[ralph-loop] Starting loop from chat.message", {
|
|
sessionID: input.sessionID,
|
|
prompt,
|
|
});
|
|
ralphLoop.startLoop(input.sessionID, prompt, {
|
|
maxIterations: maxIterMatch
|
|
? parseInt(maxIterMatch[1], 10)
|
|
: undefined,
|
|
completionPromise: promiseMatch?.[1],
|
|
});
|
|
} else if (isCancelRalphTemplate) {
|
|
log("[ralph-loop] Cancelling loop from chat.message", {
|
|
sessionID: input.sessionID,
|
|
});
|
|
ralphLoop.cancelLoop(input.sessionID);
|
|
}
|
|
}
|
|
},
|
|
|
|
"experimental.chat.messages.transform": async (
|
|
input: Record<string, never>,
|
|
output: { messages: Array<{ info: unknown; parts: unknown[] }> }
|
|
) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await contextInjectorMessagesTransform?.["experimental.chat.messages.transform"]?.(input, output as any);
|
|
await thinkingBlockValidator?.[
|
|
"experimental.chat.messages.transform"
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
]?.(input, output as any);
|
|
|
|
},
|
|
|
|
config: configHandler,
|
|
|
|
event: async (input) => {
|
|
await autoUpdateChecker?.event(input);
|
|
await claudeCodeHooks.event(input);
|
|
await backgroundNotificationHook?.event(input);
|
|
await sessionNotification?.(input);
|
|
await todoContinuationEnforcer?.handler(input);
|
|
await contextWindowMonitor?.event(input);
|
|
await directoryAgentsInjector?.event(input);
|
|
await directoryReadmeInjector?.event(input);
|
|
await rulesInjector?.event(input);
|
|
await thinkMode?.event(input);
|
|
await anthropicContextWindowLimitRecovery?.event(input);
|
|
await agentUsageReminder?.event(input);
|
|
await categorySkillReminder?.event(input);
|
|
await interactiveBashSession?.event(input);
|
|
await ralphLoop?.event(input);
|
|
await atlasHook?.handler(input);
|
|
|
|
const { event } = input;
|
|
const props = event.properties as Record<string, unknown> | undefined;
|
|
|
|
if (event.type === "session.created") {
|
|
const sessionInfo = props?.info as
|
|
| { id?: string; title?: string; parentID?: string }
|
|
| undefined;
|
|
if (!sessionInfo?.parentID) {
|
|
setMainSession(sessionInfo?.id);
|
|
}
|
|
firstMessageVariantGate.markSessionCreated(sessionInfo);
|
|
if (sessionInfo?.id && sessionInfo?.title) {
|
|
await tmuxSessionManager.onSessionCreated({
|
|
sessionID: sessionInfo.id,
|
|
parentID: sessionInfo.parentID,
|
|
title: sessionInfo.title,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (event.type === "session.deleted") {
|
|
const sessionInfo = props?.info as { id?: string } | undefined;
|
|
if (sessionInfo?.id === getMainSessionID()) {
|
|
setMainSession(undefined);
|
|
}
|
|
if (sessionInfo?.id) {
|
|
clearSessionAgent(sessionInfo.id);
|
|
resetMessageCursor(sessionInfo.id);
|
|
firstMessageVariantGate.clear(sessionInfo.id);
|
|
await skillMcpManager.disconnectSession(sessionInfo.id);
|
|
await lspManager.cleanupTempDirectoryClients();
|
|
await tmuxSessionManager.onSessionDeleted({
|
|
sessionID: sessionInfo.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (event.type === "message.updated") {
|
|
const info = props?.info as Record<string, unknown> | undefined;
|
|
const sessionID = info?.sessionID as string | undefined;
|
|
const agent = info?.agent as string | undefined;
|
|
const role = info?.role as string | undefined;
|
|
if (sessionID && agent && role === "user") {
|
|
updateSessionAgent(sessionID, agent);
|
|
}
|
|
}
|
|
|
|
if (event.type === "session.error") {
|
|
const sessionID = props?.sessionID as string | undefined;
|
|
const error = props?.error;
|
|
|
|
if (sessionRecovery?.isRecoverableError(error)) {
|
|
const messageInfo = {
|
|
id: props?.messageID as string | undefined,
|
|
role: "assistant" as const,
|
|
sessionID,
|
|
error,
|
|
};
|
|
const recovered =
|
|
await sessionRecovery.handleSessionRecovery(messageInfo);
|
|
|
|
if (recovered && sessionID && sessionID === getMainSessionID()) {
|
|
await ctx.client.session
|
|
.prompt({
|
|
path: { id: sessionID },
|
|
body: { parts: [{ type: "text", text: "continue" }] },
|
|
query: { directory: ctx.directory },
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
"tool.execute.before": async (input, output) => {
|
|
await questionLabelTruncator["tool.execute.before"]?.(input, output);
|
|
await claudeCodeHooks["tool.execute.before"](input, output);
|
|
await nonInteractiveEnv?.["tool.execute.before"](input, output);
|
|
await commentChecker?.["tool.execute.before"](input, output);
|
|
await directoryAgentsInjector?.["tool.execute.before"]?.(input, output);
|
|
await directoryReadmeInjector?.["tool.execute.before"]?.(input, output);
|
|
await rulesInjector?.["tool.execute.before"]?.(input, output);
|
|
await prometheusMdOnly?.["tool.execute.before"]?.(input, output);
|
|
await sisyphusJuniorNotepad?.["tool.execute.before"]?.(input, output);
|
|
await atlasHook?.["tool.execute.before"]?.(input, output);
|
|
|
|
if (input.tool === "task") {
|
|
const args = output.args as Record<string, unknown>;
|
|
const subagentType = args.subagent_type as string;
|
|
const isExploreOrLibrarian = includesCaseInsensitive(
|
|
["explore", "librarian"],
|
|
subagentType ?? ""
|
|
);
|
|
|
|
args.tools = {
|
|
...(args.tools as Record<string, boolean> | undefined),
|
|
delegate_task: false,
|
|
...(isExploreOrLibrarian ? { call_omo_agent: false } : {}),
|
|
};
|
|
}
|
|
|
|
if (ralphLoop && input.tool === "slashcommand") {
|
|
const args = output.args as { command?: string } | undefined;
|
|
const command = args?.command?.replace(/^\//, "").toLowerCase();
|
|
const sessionID = input.sessionID || getMainSessionID();
|
|
|
|
if (command === "ralph-loop" && sessionID) {
|
|
const rawArgs =
|
|
args?.command?.replace(/^\/?(ralph-loop)\s*/i, "") || "";
|
|
const taskMatch = rawArgs.match(/^["'](.+?)["']/);
|
|
const prompt =
|
|
taskMatch?.[1] ||
|
|
rawArgs.split(/\s+--/)[0]?.trim() ||
|
|
"Complete the task as instructed";
|
|
|
|
const maxIterMatch = rawArgs.match(/--max-iterations=(\d+)/i);
|
|
const promiseMatch = rawArgs.match(
|
|
/--completion-promise=["']?([^"'\s]+)["']?/i
|
|
);
|
|
|
|
ralphLoop.startLoop(sessionID, prompt, {
|
|
maxIterations: maxIterMatch
|
|
? parseInt(maxIterMatch[1], 10)
|
|
: undefined,
|
|
completionPromise: promiseMatch?.[1],
|
|
});
|
|
} else if (command === "cancel-ralph" && sessionID) {
|
|
ralphLoop.cancelLoop(sessionID);
|
|
} else if (command === "ulw-loop" && sessionID) {
|
|
const rawArgs =
|
|
args?.command?.replace(/^\/?(ulw-loop)\s*/i, "") || "";
|
|
const taskMatch = rawArgs.match(/^["'](.+?)["']/);
|
|
const prompt =
|
|
taskMatch?.[1] ||
|
|
rawArgs.split(/\s+--/)[0]?.trim() ||
|
|
"Complete the task as instructed";
|
|
|
|
const maxIterMatch = rawArgs.match(/--max-iterations=(\d+)/i);
|
|
const promiseMatch = rawArgs.match(
|
|
/--completion-promise=["']?([^"'\s]+)["']?/i
|
|
);
|
|
|
|
ralphLoop.startLoop(sessionID, prompt, {
|
|
ultrawork: true,
|
|
maxIterations: maxIterMatch
|
|
? parseInt(maxIterMatch[1], 10)
|
|
: undefined,
|
|
completionPromise: promiseMatch?.[1],
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
"tool.execute.after": async (input, output) => {
|
|
await claudeCodeHooks["tool.execute.after"](input, output);
|
|
await toolOutputTruncator?.["tool.execute.after"](input, output);
|
|
await contextWindowMonitor?.["tool.execute.after"](input, output);
|
|
await commentChecker?.["tool.execute.after"](input, output);
|
|
await directoryAgentsInjector?.["tool.execute.after"](input, output);
|
|
await directoryReadmeInjector?.["tool.execute.after"](input, output);
|
|
await rulesInjector?.["tool.execute.after"](input, output);
|
|
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
|
await agentUsageReminder?.["tool.execute.after"](input, output);
|
|
await categorySkillReminder?.["tool.execute.after"](input, output);
|
|
await interactiveBashSession?.["tool.execute.after"](input, output);
|
|
await editErrorRecovery?.["tool.execute.after"](input, output);
|
|
await delegateTaskRetry?.["tool.execute.after"](input, output);
|
|
await atlasHook?.["tool.execute.after"]?.(input, output);
|
|
await taskResumeInfo["tool.execute.after"](input, output);
|
|
},
|
|
};
|
|
};
|
|
|
|
export default OhMyOpenCodePlugin;
|
|
|
|
export type {
|
|
OhMyOpenCodeConfig,
|
|
AgentName,
|
|
AgentOverrideConfig,
|
|
AgentOverrides,
|
|
McpName,
|
|
HookName,
|
|
BuiltinCommandName,
|
|
} from "./config";
|
|
|
|
// NOTE: Do NOT export functions from main index.ts!
|
|
// OpenCode treats ALL exports as plugin instances and calls them.
|
|
// Config error utilities are available via "./shared/config-errors" for internal use only.
|
|
export type { ConfigLoadError } from "./shared/config-errors";
|