diff --git a/src/index.disabled-tools.test.ts b/src/index.disabled-tools.test.ts new file mode 100644 index 00000000..f370746f --- /dev/null +++ b/src/index.disabled-tools.test.ts @@ -0,0 +1,273 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; +import type { PluginInput } from "@opencode-ai/plugin"; + +let currentConfig: Record = {}; + +const DUMMY_TOOL = { + description: "dummy", + args: {}, + execute: async () => "ok", +}; + +mock.module("./plugin-config", () => ({ + loadPluginConfig: () => currentConfig, +})); + +mock.module("./shared", () => ({ + log: () => {}, + detectExternalNotificationPlugin: () => ({ + detected: false, + pluginName: null, + allPlugins: [], + }), + getNotificationConflictWarning: () => "", + resetMessageCursor: () => {}, + hasConnectedProvidersCache: () => true, + getOpenCodeVersion: () => null, + isOpenCodeVersionAtLeast: () => false, + OPENCODE_NATIVE_AGENTS_INJECTION_VERSION: "0.0.0", + injectServerAuthIntoClient: () => {}, +})); + +mock.module("./hooks", () => { + const noopHook = { + event: async () => {}, + handler: async () => {}, + "chat.message": async () => {}, + "tool.execute.before": async () => {}, + "tool.execute.after": async () => {}, + "experimental.chat.messages.transform": async () => {}, + }; + + return { + createTodoContinuationEnforcer: () => null, + createContextWindowMonitorHook: () => null, + createSessionRecoveryHook: () => null, + createSessionNotification: () => async () => {}, + createCommentCheckerHooks: () => null, + createToolOutputTruncatorHook: () => null, + createDirectoryAgentsInjectorHook: () => null, + createDirectoryReadmeInjectorHook: () => null, + createEmptyTaskResponseDetectorHook: () => null, + createThinkModeHook: () => null, + createClaudeCodeHooksHook: () => noopHook, + createAnthropicContextWindowLimitRecoveryHook: () => null, + createRulesInjectorHook: () => null, + createBackgroundNotificationHook: () => null, + createAutoUpdateCheckerHook: () => ({ event: async () => {} }), + createKeywordDetectorHook: () => null, + createAgentUsageReminderHook: () => null, + createNonInteractiveEnvHook: () => null, + createInteractiveBashSessionHook: () => null, + createThinkingBlockValidatorHook: () => null, + createCategorySkillReminderHook: () => null, + createRalphLoopHook: () => null, + createAutoSlashCommandHook: () => null, + createEditErrorRecoveryHook: () => null, + createDelegateTaskRetryHook: () => null, + createTaskResumeInfoHook: () => ({ + "tool.execute.after": async () => {}, + }), + createStartWorkHook: () => null, + createAtlasHook: () => null, + createPrometheusMdOnlyHook: () => null, + createSisyphusJuniorNotepadHook: () => null, + createQuestionLabelTruncatorHook: () => null, + createSubagentQuestionBlockerHook: () => null, + createStopContinuationGuardHook: () => null, + createCompactionContextInjector: () => null, + createUnstableAgentBabysitterHook: () => null, + createPreemptiveCompactionHook: () => null, + createTasksTodowriteDisablerHook: () => null, + createWriteExistingFileGuardHook: () => null, + }; +}); + +mock.module("./features/context-injector", () => ({ + contextCollector: {}, + createContextInjectorMessagesTransformHook: () => null, +})); + +mock.module("./shared/agent-variant", () => ({ + applyAgentVariant: () => {}, + resolveAgentVariant: () => undefined, + resolveVariantForModel: () => undefined, +})); + +mock.module("./shared/first-message-variant", () => ({ + createFirstMessageVariantGate: () => ({ + shouldOverride: () => false, + markApplied: () => {}, + markSessionCreated: () => {}, + clear: () => {}, + }), +})); + +mock.module("./features/opencode-skill-loader", () => ({ + discoverUserClaudeSkills: async () => [], + discoverProjectClaudeSkills: async () => [], + discoverOpencodeGlobalSkills: async () => [], + discoverOpencodeProjectSkills: async () => [], + mergeSkills: (...skills: unknown[][]) => skills.flat(), +})); + +mock.module("./features/builtin-skills", () => ({ + createBuiltinSkills: () => [], +})); + +mock.module("./features/claude-code-mcp-loader", () => ({ + getSystemMcpServerNames: () => new Set(), +})); + +mock.module("./features/claude-code-session-state", () => ({ + setMainSession: () => {}, + getMainSessionID: () => undefined, + setSessionAgent: () => {}, + updateSessionAgent: () => {}, + clearSessionAgent: () => {}, +})); + +mock.module("./features/background-agent", () => ({ + BackgroundManager: class BackgroundManager { + constructor(..._args: unknown[]) {} + }, +})); + +mock.module("./features/skill-mcp-manager", () => ({ + SkillMcpManager: class SkillMcpManager { + disconnectSession = async () => {}; + }, +})); + +mock.module("./features/task-toast-manager", () => ({ + initTaskToastManager: () => {}, +})); + +mock.module("./features/tmux-subagent", () => ({ + TmuxSessionManager: class TmuxSessionManager { + constructor(..._args: unknown[]) {} + cleanup = async () => {}; + onSessionCreated = async () => {}; + onSessionDeleted = async () => {}; + }, +})); + +mock.module("./features/boulder-state", () => ({ + clearBoulderState: () => {}, +})); + +mock.module("./plugin-state", () => ({ + createModelCacheState: () => ({}), +})); + +mock.module("./plugin-handlers", () => ({ + createConfigHandler: () => ({}), +})); + +mock.module("./tools", () => ({ + builtinTools: { + foo: DUMMY_TOOL, + bar: DUMMY_TOOL, + }, + createCallOmoAgent: () => DUMMY_TOOL, + createBackgroundTools: () => ({ + background_output: DUMMY_TOOL, + }), + createLookAt: () => DUMMY_TOOL, + createSkillTool: () => DUMMY_TOOL, + createSkillMcpTool: () => DUMMY_TOOL, + createSlashcommandTool: () => DUMMY_TOOL, + discoverCommandsSync: () => [], + sessionExists: () => false, + createDelegateTask: () => DUMMY_TOOL, + interactive_bash: DUMMY_TOOL, + startTmuxCheck: () => {}, + lspManager: { + cleanupTempDirectoryClients: async () => {}, + }, + createTaskCreateTool: () => DUMMY_TOOL, + createTaskGetTool: () => DUMMY_TOOL, + createTaskList: () => DUMMY_TOOL, + createTaskUpdateTool: () => DUMMY_TOOL, +})); + +const { default: OhMyOpenCodePlugin } = await import("./index"); + +describe("disabled_tools config", () => { + beforeEach(() => { + currentConfig = { + experimental: { task_system: false }, + }; + }); + + test("returns all tools when disabled_tools is unset", async () => { + //#given + const ctx = { + directory: "/tmp/omo-test", + client: {}, + } as PluginInput; + + //#when + const plugin = await OhMyOpenCodePlugin(ctx); + const toolNames = Object.keys(plugin.tool).sort(); + + //#then + expect(toolNames).toEqual( + [ + "background_output", + "bar", + "call_omo_agent", + "delegate_task", + "foo", + "interactive_bash", + "look_at", + "skill", + "skill_mcp", + "slashcommand", + ].sort(), + ); + }); + + test("filters out tools listed in disabled_tools", async () => { + //#given + currentConfig = { + experimental: { task_system: false }, + disabled_tools: ["call_omo_agent", "delegate_task"], + }; + + const ctx = { + directory: "/tmp/omo-test", + client: {}, + } as PluginInput; + + //#when + const plugin = await OhMyOpenCodePlugin(ctx); + const toolNames = Object.keys(plugin.tool); + + //#then + expect(toolNames).not.toContain("call_omo_agent"); + expect(toolNames).not.toContain("delegate_task"); + expect(toolNames).toContain("foo"); + expect(toolNames).toContain("background_output"); + }); + + test("matches tool names exactly", async () => { + //#given + currentConfig = { + experimental: { task_system: false }, + disabled_tools: ["call"], + }; + + const ctx = { + directory: "/tmp/omo-test", + client: {}, + } as PluginInput; + + //#when + const plugin = await OhMyOpenCodePlugin(ctx); + const toolNames = Object.keys(plugin.tool); + + //#then + expect(toolNames).toContain("call_omo_agent"); + }); +}); diff --git a/src/index.ts b/src/index.ts index c62c68b0..54891299 100644 --- a/src/index.ts +++ b/src/index.ts @@ -115,6 +115,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const pluginConfig = loadPluginConfig(ctx.directory, ctx); const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []); + const disabledTools = new Set(pluginConfig.disabled_tools ?? []); const firstMessageVariantGate = createFirstMessageVariantGate(); const tmuxConfig = { @@ -487,19 +488,31 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { } : {}; + const allTools: Record = { + ...builtinTools, + ...backgroundTools, + call_omo_agent: callOmoAgent, + ...(lookAt ? { look_at: lookAt } : {}), + delegate_task: delegateTask, + skill: skillTool, + skill_mcp: skillMcpTool, + slashcommand: slashcommandTool, + interactive_bash, + ...taskToolsRecord, + }; + + const filteredTools: Record = + disabledTools.size > 0 ? {} : allTools; + if (disabledTools.size > 0) { + for (const [toolName, toolDefinition] of Object.entries(allTools)) { + if (!disabledTools.has(toolName)) { + filteredTools[toolName] = toolDefinition; + } + } + } + return { - tool: { - ...builtinTools, - ...backgroundTools, - call_omo_agent: callOmoAgent, - ...(lookAt ? { look_at: lookAt } : {}), - delegate_task: delegateTask, - skill: skillTool, - skill_mcp: skillMcpTool, - slashcommand: slashcommandTool, - interactive_bash, - ...taskToolsRecord, - }, + tool: filteredTools, "chat.message": async (input, output) => { if (input.agent) {