/ tools.ts
tools.ts
  1  // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
  2  import { toolMatchesName, type Tool, type Tools } from './Tool.js'
  3  import { AgentTool } from './tools/AgentTool/AgentTool.js'
  4  import { SkillTool } from './tools/SkillTool/SkillTool.js'
  5  import { BashTool } from './tools/BashTool/BashTool.js'
  6  import { FileEditTool } from './tools/FileEditTool/FileEditTool.js'
  7  import { FileReadTool } from './tools/FileReadTool/FileReadTool.js'
  8  import { FileWriteTool } from './tools/FileWriteTool/FileWriteTool.js'
  9  import { GlobTool } from './tools/GlobTool/GlobTool.js'
 10  import { NotebookEditTool } from './tools/NotebookEditTool/NotebookEditTool.js'
 11  import { WebFetchTool } from './tools/WebFetchTool/WebFetchTool.js'
 12  import { TaskStopTool } from './tools/TaskStopTool/TaskStopTool.js'
 13  import { BriefTool } from './tools/BriefTool/BriefTool.js'
 14  // Dead code elimination: conditional import for ant-only tools
 15  /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
 16  const REPLTool =
 17    process.env.USER_TYPE === 'ant'
 18      ? require('./tools/REPLTool/REPLTool.js').REPLTool
 19      : null
 20  const SuggestBackgroundPRTool =
 21    process.env.USER_TYPE === 'ant'
 22      ? require('./tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
 23          .SuggestBackgroundPRTool
 24      : null
 25  const SleepTool =
 26    feature('PROACTIVE') || feature('KAIROS')
 27      ? require('./tools/SleepTool/SleepTool.js').SleepTool
 28      : null
 29  const cronTools = feature('AGENT_TRIGGERS')
 30    ? [
 31        require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
 32        require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
 33        require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
 34      ]
 35    : []
 36  const RemoteTriggerTool = feature('AGENT_TRIGGERS_REMOTE')
 37    ? require('./tools/RemoteTriggerTool/RemoteTriggerTool.js').RemoteTriggerTool
 38    : null
 39  const MonitorTool = feature('MONITOR_TOOL')
 40    ? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
 41    : null
 42  const SendUserFileTool = feature('KAIROS')
 43    ? require('./tools/SendUserFileTool/SendUserFileTool.js').SendUserFileTool
 44    : null
 45  const PushNotificationTool =
 46    feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION')
 47      ? require('./tools/PushNotificationTool/PushNotificationTool.js')
 48          .PushNotificationTool
 49      : null
 50  const SubscribePRTool = feature('KAIROS_GITHUB_WEBHOOKS')
 51    ? require('./tools/SubscribePRTool/SubscribePRTool.js').SubscribePRTool
 52    : null
 53  /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
 54  import { TaskOutputTool } from './tools/TaskOutputTool/TaskOutputTool.js'
 55  import { WebSearchTool } from './tools/WebSearchTool/WebSearchTool.js'
 56  import { TodoWriteTool } from './tools/TodoWriteTool/TodoWriteTool.js'
 57  import { ExitPlanModeV2Tool } from './tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
 58  import { TestingPermissionTool } from './tools/testing/TestingPermissionTool.js'
 59  import { GrepTool } from './tools/GrepTool/GrepTool.js'
 60  import { TungstenTool } from './tools/TungstenTool/TungstenTool.js'
 61  // Lazy require to break circular dependency: tools.ts -> TeamCreateTool/TeamDeleteTool -> ... -> tools.ts
 62  /* eslint-disable @typescript-eslint/no-require-imports */
 63  const getTeamCreateTool = () =>
 64    require('./tools/TeamCreateTool/TeamCreateTool.js')
 65      .TeamCreateTool as typeof import('./tools/TeamCreateTool/TeamCreateTool.js').TeamCreateTool
 66  const getTeamDeleteTool = () =>
 67    require('./tools/TeamDeleteTool/TeamDeleteTool.js')
 68      .TeamDeleteTool as typeof import('./tools/TeamDeleteTool/TeamDeleteTool.js').TeamDeleteTool
 69  const getSendMessageTool = () =>
 70    require('./tools/SendMessageTool/SendMessageTool.js')
 71      .SendMessageTool as typeof import('./tools/SendMessageTool/SendMessageTool.js').SendMessageTool
 72  /* eslint-enable @typescript-eslint/no-require-imports */
 73  import { AskUserQuestionTool } from './tools/AskUserQuestionTool/AskUserQuestionTool.js'
 74  import { LSPTool } from './tools/LSPTool/LSPTool.js'
 75  import { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
 76  import { ReadMcpResourceTool } from './tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
 77  import { ToolSearchTool } from './tools/ToolSearchTool/ToolSearchTool.js'
 78  import { EnterPlanModeTool } from './tools/EnterPlanModeTool/EnterPlanModeTool.js'
 79  import { EnterWorktreeTool } from './tools/EnterWorktreeTool/EnterWorktreeTool.js'
 80  import { ExitWorktreeTool } from './tools/ExitWorktreeTool/ExitWorktreeTool.js'
 81  import { ConfigTool } from './tools/ConfigTool/ConfigTool.js'
 82  import { TaskCreateTool } from './tools/TaskCreateTool/TaskCreateTool.js'
 83  import { TaskGetTool } from './tools/TaskGetTool/TaskGetTool.js'
 84  import { TaskUpdateTool } from './tools/TaskUpdateTool/TaskUpdateTool.js'
 85  import { TaskListTool } from './tools/TaskListTool/TaskListTool.js'
 86  import uniqBy from 'lodash-es/uniqBy.js'
 87  import { isToolSearchEnabledOptimistic } from './utils/toolSearch.js'
 88  import { isTodoV2Enabled } from './utils/tasks.js'
 89  // Dead code elimination: conditional import for CLAUDE_CODE_VERIFY_PLAN
 90  /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
 91  const VerifyPlanExecutionTool =
 92    process.env.CLAUDE_CODE_VERIFY_PLAN === 'true'
 93      ? require('./tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.js')
 94          .VerifyPlanExecutionTool
 95      : null
 96  /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
 97  import { SYNTHETIC_OUTPUT_TOOL_NAME } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
 98  export {
 99    ALL_AGENT_DISALLOWED_TOOLS,
100    CUSTOM_AGENT_DISALLOWED_TOOLS,
101    ASYNC_AGENT_ALLOWED_TOOLS,
102    COORDINATOR_MODE_ALLOWED_TOOLS,
103  } from './constants/tools.js'
104  import { feature } from 'bun:bundle'
105  // Dead code elimination: conditional import for OVERFLOW_TEST_TOOL
106  /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
107  const OverflowTestTool = feature('OVERFLOW_TEST_TOOL')
108    ? require('./tools/OverflowTestTool/OverflowTestTool.js').OverflowTestTool
109    : null
110  const CtxInspectTool = feature('CONTEXT_COLLAPSE')
111    ? require('./tools/CtxInspectTool/CtxInspectTool.js').CtxInspectTool
112    : null
113  const TerminalCaptureTool = feature('TERMINAL_PANEL')
114    ? require('./tools/TerminalCaptureTool/TerminalCaptureTool.js')
115        .TerminalCaptureTool
116    : null
117  const WebBrowserTool = feature('WEB_BROWSER_TOOL')
118    ? require('./tools/WebBrowserTool/WebBrowserTool.js').WebBrowserTool
119    : null
120  const coordinatorModeModule = feature('COORDINATOR_MODE')
121    ? (require('./coordinator/coordinatorMode.js') as typeof import('./coordinator/coordinatorMode.js'))
122    : null
123  const SnipTool = feature('HISTORY_SNIP')
124    ? require('./tools/SnipTool/SnipTool.js').SnipTool
125    : null
126  const ListPeersTool = feature('UDS_INBOX')
127    ? require('./tools/ListPeersTool/ListPeersTool.js').ListPeersTool
128    : null
129  const WorkflowTool = feature('WORKFLOW_SCRIPTS')
130    ? (() => {
131        require('./tools/WorkflowTool/bundled/index.js').initBundledWorkflows()
132        return require('./tools/WorkflowTool/WorkflowTool.js').WorkflowTool
133      })()
134    : null
135  /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
136  import type { ToolPermissionContext } from './Tool.js'
137  import { getDenyRuleForTool } from './utils/permissions/permissions.js'
138  import { hasEmbeddedSearchTools } from './utils/embeddedTools.js'
139  import { isEnvTruthy } from './utils/envUtils.js'
140  import { isPowerShellToolEnabled } from './utils/shell/shellToolUtils.js'
141  import { isAgentSwarmsEnabled } from './utils/agentSwarmsEnabled.js'
142  import { isWorktreeModeEnabled } from './utils/worktreeModeEnabled.js'
143  import {
144    REPL_TOOL_NAME,
145    REPL_ONLY_TOOLS,
146    isReplModeEnabled,
147  } from './tools/REPLTool/constants.js'
148  export { REPL_ONLY_TOOLS }
149  /* eslint-disable @typescript-eslint/no-require-imports */
150  const getPowerShellTool = () => {
151    if (!isPowerShellToolEnabled()) return null
152    return (
153      require('./tools/PowerShellTool/PowerShellTool.js') as typeof import('./tools/PowerShellTool/PowerShellTool.js')
154    ).PowerShellTool
155  }
156  /* eslint-enable @typescript-eslint/no-require-imports */
157  
158  /**
159   * Predefined tool presets that can be used with --tools flag
160   */
161  export const TOOL_PRESETS = ['default'] as const
162  
163  export type ToolPreset = (typeof TOOL_PRESETS)[number]
164  
165  export function parseToolPreset(preset: string): ToolPreset | null {
166    const presetString = preset.toLowerCase()
167    if (!TOOL_PRESETS.includes(presetString as ToolPreset)) {
168      return null
169    }
170    return presetString as ToolPreset
171  }
172  
173  /**
174   * Get the list of tool names for a given preset
175   * Filters out tools that are disabled via isEnabled() check
176   * @param preset The preset name
177   * @returns Array of tool names
178   */
179  export function getToolsForDefaultPreset(): string[] {
180    const tools = getAllBaseTools()
181    const isEnabled = tools.map(tool => tool.isEnabled())
182    return tools.filter((_, i) => isEnabled[i]).map(tool => tool.name)
183  }
184  
185  /**
186   * Get the complete exhaustive list of all tools that could be available
187   * in the current environment (respecting process.env flags).
188   * This is the source of truth for ALL tools.
189   */
190  /**
191   * NOTE: This MUST stay in sync with https://console.statsig.com/4aF3Ewatb6xPVpCwxb5nA3/dynamic_configs/claude_code_global_system_caching, in order to cache the system prompt across users.
192   */
193  export function getAllBaseTools(): Tools {
194    return [
195      AgentTool,
196      TaskOutputTool,
197      BashTool,
198      // Ant-native builds have bfs/ugrep embedded in the bun binary (same ARGV0
199      // trick as ripgrep). When available, find/grep in Claude's shell are aliased
200      // to these fast tools, so the dedicated Glob/Grep tools are unnecessary.
201      ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
202      ExitPlanModeV2Tool,
203      FileReadTool,
204      FileEditTool,
205      FileWriteTool,
206      NotebookEditTool,
207      WebFetchTool,
208      TodoWriteTool,
209      WebSearchTool,
210      TaskStopTool,
211      AskUserQuestionTool,
212      SkillTool,
213      EnterPlanModeTool,
214      ...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
215      ...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),
216      ...(SuggestBackgroundPRTool ? [SuggestBackgroundPRTool] : []),
217      ...(WebBrowserTool ? [WebBrowserTool] : []),
218      ...(isTodoV2Enabled()
219        ? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool]
220        : []),
221      ...(OverflowTestTool ? [OverflowTestTool] : []),
222      ...(CtxInspectTool ? [CtxInspectTool] : []),
223      ...(TerminalCaptureTool ? [TerminalCaptureTool] : []),
224      ...(isEnvTruthy(process.env.ENABLE_LSP_TOOL) ? [LSPTool] : []),
225      ...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
226      getSendMessageTool(),
227      ...(ListPeersTool ? [ListPeersTool] : []),
228      ...(isAgentSwarmsEnabled()
229        ? [getTeamCreateTool(), getTeamDeleteTool()]
230        : []),
231      ...(VerifyPlanExecutionTool ? [VerifyPlanExecutionTool] : []),
232      ...(process.env.USER_TYPE === 'ant' && REPLTool ? [REPLTool] : []),
233      ...(WorkflowTool ? [WorkflowTool] : []),
234      ...(SleepTool ? [SleepTool] : []),
235      ...cronTools,
236      ...(RemoteTriggerTool ? [RemoteTriggerTool] : []),
237      ...(MonitorTool ? [MonitorTool] : []),
238      BriefTool,
239      ...(SendUserFileTool ? [SendUserFileTool] : []),
240      ...(PushNotificationTool ? [PushNotificationTool] : []),
241      ...(SubscribePRTool ? [SubscribePRTool] : []),
242      ...(getPowerShellTool() ? [getPowerShellTool()] : []),
243      ...(SnipTool ? [SnipTool] : []),
244      ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
245      ListMcpResourcesTool,
246      ReadMcpResourceTool,
247      // Include ToolSearchTool when tool search might be enabled (optimistic check)
248      // The actual decision to defer tools happens at request time in claude.ts
249      ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
250    ]
251  }
252  
253  /**
254   * Filters out tools that are blanket-denied by the permission context.
255   * A tool is filtered out if there's a deny rule matching its name with no
256   * ruleContent (i.e., a blanket deny for that tool).
257   *
258   * Uses the same matcher as the runtime permission check (step 1a), so MCP
259   * server-prefix rules like `mcp__server` strip all tools from that server
260   * before the model sees them — not just at call time.
261   */
262  export function filterToolsByDenyRules<
263    T extends {
264      name: string
265      mcpInfo?: { serverName: string; toolName: string }
266    },
267  >(tools: readonly T[], permissionContext: ToolPermissionContext): T[] {
268    return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
269  }
270  
271  export const getTools = (permissionContext: ToolPermissionContext): Tools => {
272    // Simple mode: only Bash, Read, and Edit tools
273    if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
274      // --bare + REPL mode: REPL wraps Bash/Read/Edit/etc inside the VM, so
275      // return REPL instead of the raw primitives. Matches the non-bare path
276      // below which also hides REPL_ONLY_TOOLS when REPL is enabled.
277      if (isReplModeEnabled() && REPLTool) {
278        const replSimple: Tool[] = [REPLTool]
279        if (
280          feature('COORDINATOR_MODE') &&
281          coordinatorModeModule?.isCoordinatorMode()
282        ) {
283          replSimple.push(TaskStopTool, getSendMessageTool())
284        }
285        return filterToolsByDenyRules(replSimple, permissionContext)
286      }
287      const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
288      // When coordinator mode is also active, include AgentTool and TaskStopTool
289      // so the coordinator gets Task+TaskStop (via useMergedTools filtering) and
290      // workers get Bash/Read/Edit (via filterToolsForAgent filtering).
291      if (
292        feature('COORDINATOR_MODE') &&
293        coordinatorModeModule?.isCoordinatorMode()
294      ) {
295        simpleTools.push(AgentTool, TaskStopTool, getSendMessageTool())
296      }
297      return filterToolsByDenyRules(simpleTools, permissionContext)
298    }
299  
300    // Get all base tools and filter out special tools that get added conditionally
301    const specialTools = new Set([
302      ListMcpResourcesTool.name,
303      ReadMcpResourceTool.name,
304      SYNTHETIC_OUTPUT_TOOL_NAME,
305    ])
306  
307    const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
308  
309    // Filter out tools that are denied by the deny rules
310    let allowedTools = filterToolsByDenyRules(tools, permissionContext)
311  
312    // When REPL mode is enabled, hide primitive tools from direct use.
313    // They're still accessible inside REPL via the VM context.
314    if (isReplModeEnabled()) {
315      const replEnabled = allowedTools.some(tool =>
316        toolMatchesName(tool, REPL_TOOL_NAME),
317      )
318      if (replEnabled) {
319        allowedTools = allowedTools.filter(
320          tool => !REPL_ONLY_TOOLS.has(tool.name),
321        )
322      }
323    }
324  
325    const isEnabled = allowedTools.map(_ => _.isEnabled())
326    return allowedTools.filter((_, i) => isEnabled[i])
327  }
328  
329  /**
330   * Assemble the full tool pool for a given permission context and MCP tools.
331   *
332   * This is the single source of truth for combining built-in tools with MCP tools.
333   * Both REPL.tsx (via useMergedTools hook) and runAgent.ts (for coordinator workers)
334   * use this function to ensure consistent tool pool assembly.
335   *
336   * The function:
337   * 1. Gets built-in tools via getTools() (respects mode filtering)
338   * 2. Filters MCP tools by deny rules
339   * 3. Deduplicates by tool name (built-in tools take precedence)
340   *
341   * @param permissionContext - Permission context for filtering built-in tools
342   * @param mcpTools - MCP tools from appState.mcp.tools
343   * @returns Combined, deduplicated array of built-in and MCP tools
344   */
345  export function assembleToolPool(
346    permissionContext: ToolPermissionContext,
347    mcpTools: Tools,
348  ): Tools {
349    const builtInTools = getTools(permissionContext)
350  
351    // Filter out MCP tools that are in the deny list
352    const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
353  
354    // Sort each partition for prompt-cache stability, keeping built-ins as a
355    // contiguous prefix. The server's claude_code_system_cache_policy places a
356    // global cache breakpoint after the last prefix-matched built-in tool; a flat
357    // sort would interleave MCP tools into built-ins and invalidate all downstream
358    // cache keys whenever an MCP tool sorts between existing built-ins. uniqBy
359    // preserves insertion order, so built-ins win on name conflict.
360    // Avoid Array.toSorted (Node 20+) — we support Node 18. builtInTools is
361    // readonly so copy-then-sort; allowedMcpTools is a fresh .filter() result.
362    const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
363    return uniqBy(
364      [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
365      'name',
366    )
367  }
368  
369  /**
370   * Get all tools including both built-in tools and MCP tools.
371   *
372   * This is the preferred function when you need the complete tools list for:
373   * - Tool search threshold calculations (isToolSearchEnabled)
374   * - Token counting that includes MCP tools
375   * - Any context where MCP tools should be considered
376   *
377   * Use getTools() only when you specifically need just built-in tools.
378   *
379   * @param permissionContext - Permission context for filtering built-in tools
380   * @param mcpTools - MCP tools from appState.mcp.tools
381   * @returns Combined array of built-in and MCP tools
382   */
383  export function getMergedTools(
384    permissionContext: ToolPermissionContext,
385    mcpTools: Tools,
386  ): Tools {
387    const builtInTools = getTools(permissionContext)
388    return [...builtInTools, ...mcpTools]
389  }