bingocode 1.1.200-beta.13 → 1.1.200-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.1.200-beta.13",
3
+ "version": "1.1.200-beta.15",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -976,6 +976,14 @@ Each agent prompt must include:
976
976
 
977
977
  Report compressed: decisions made, blockers hit, next steps. Never narrate what
978
978
  agents are doing moment-by-moment. "Agent A found X, agent B confirmed Y. Decision:
979
- rollback needed." is enough.`
979
+ rollback needed." is enough.
980
+
981
+ ## Sub-agent Capabilities
982
+
983
+ Sub-agents have full access to: Bash (shell execution), Read (file reading),
984
+ Write (file creation), Edit (precise string replacement), Grep (regex search),
985
+ Glob (file pattern matching), WebFetch (URL content), WebSearch (web search),
986
+ NotebookEdit (Jupyter editing), plus task management tools and any configured
987
+ MCP servers.`
980
988
  }
981
989
 
@@ -22,12 +22,13 @@ export function useMergedTools(
22
22
  mcpTools: Tools,
23
23
  toolPermissionContext: ToolPermissionContext,
24
24
  ): Tools {
25
+ const brainMode = useAppState(s => s.brainMode ?? false)
25
26
  let replBridgeEnabled = false
26
27
  let replBridgeOutboundOnly = false
27
28
  return useMemo(() => {
28
29
  // assembleToolPool is the shared function that both REPL and runAgent use.
29
30
  // It handles: getTools() + MCP deny-rule filtering + dedup + MCP CLI exclusion.
30
- const assembled = assembleToolPool(toolPermissionContext, mcpTools)
31
+ const assembled = assembleToolPool(toolPermissionContext, mcpTools, brainMode)
31
32
 
32
33
  return mergeAndFilterTools(
33
34
  initialTools,
@@ -38,6 +39,7 @@ export function useMergedTools(
38
39
  initialTools,
39
40
  mcpTools,
40
41
  toolPermissionContext,
42
+ brainMode,
41
43
  replBridgeEnabled,
42
44
  replBridgeOutboundOnly,
43
45
  ])
package/src/main.tsx CHANGED
@@ -1865,7 +1865,8 @@ async function run(): Promise<CommanderCommand> {
1865
1865
  // (which returns isProactiveActive()) passes and Sleep is included.
1866
1866
  // The later REPL-path maybeActivateProactive() calls are idempotent.
1867
1867
  maybeActivateProactive(options);
1868
- let tools = getTools(toolPermissionContext);
1868
+ const brainMode = getInitialSettings().brainMode === true || getCurrentSessionBrainMode() === true
1869
+ let tools = getTools(toolPermissionContext, brainMode);
1869
1870
 
1870
1871
  // Apply coordinator mode tool filtering for headless path
1871
1872
  // (mirrors useMergedTools.ts filtering for REPL/interactive path)
@@ -695,7 +695,8 @@ export function REPL({
695
695
  // /brief mid-session leaves the stale tool list (no SendUserMessage) and
696
696
  // the model emits plain text the brief filter hides.
697
697
  const isBriefOnly = useAppState(s => s.isBriefOnly);
698
- const localTools = useMemo(() => getTools(toolPermissionContext), [toolPermissionContext, proactiveActive, isBriefOnly]);
698
+ const isBrainMode = useAppState(s => s.brainMode ?? false);
699
+ const localTools = useMemo(() => getTools(toolPermissionContext, isBrainMode), [toolPermissionContext, proactiveActive, isBriefOnly, isBrainMode]);
699
700
  useKickOffCheckAndDisableBypassPermissionsIfNeeded();
700
701
  useKickOffCheckAndDisableAutoModeIfNeeded();
701
702
  const [dynamicMcpConfig, setDynamicMcpConfig] = useState<Record<string, ScopedMcpServerConfig> | undefined>(initialDynamicMcpConfig);
@@ -2404,7 +2405,7 @@ export function REPL({
2404
2405
  // for mid-query tool list updates.
2405
2406
  const computeTools = () => {
2406
2407
  const state = store.getState();
2407
- const assembled = assembleToolPool(state.toolPermissionContext, state.mcp.tools);
2408
+ const assembled = assembleToolPool(state.toolPermissionContext, state.mcp.tools, state.brainMode);
2408
2409
  const merged = mergeAndFilterTools(combinedInitialTools, assembled, state.toolPermissionContext.mode);
2409
2410
  if (!mainThreadAgentDefinition) return merged;
2410
2411
  return resolveAgentTools(mainThreadAgentDefinition, merged, false, true).resolvedTools;
@@ -602,12 +602,6 @@ export class ProviderService {
602
602
  const writtenPaths: string[] = []
603
603
  const failedPaths: string[] = []
604
604
 
605
- // Save pre-link bingo env so we can restore it on disconnect.
606
- const bingoSettings = await this.readSettings()
607
- const preLinkEnv = bingoSettings.env ?? null
608
- bingoSettings.vscodePreLinkEnv = preLinkEnv
609
- await this.writeSettings(bingoSettings)
610
-
611
605
  // Fix #8: Snapshot original VS Code claudeCode per path for unlink restore.
612
606
  const vscodePreLinkClaudeCode: Record<string, unknown> = {}
613
607
  // Fix #9: Backup original contents for rollback on partial failure.
@@ -724,6 +718,7 @@ export class ProviderService {
724
718
  }
725
719
 
726
720
  // Persist so re-sync on restart and unlink work correctly
721
+ const bingoSettings = await this.readSettings()
727
722
  bingoSettings.vscodeLinked = true
728
723
  bingoSettings.vscodeLinkedPaths = writtenPaths
729
724
  bingoSettings.vscodePreLinkClaudeCode = vscodePreLinkClaudeCode
@@ -741,32 +736,18 @@ export class ProviderService {
741
736
  }
742
737
 
743
738
  /**
744
- * Restore pre-link bingo env so the CLI no longer routes through the proxy,
745
- * and reset VS Code settings to their pre-link state (the original claudeCode
746
- * that was saved in vscodePreLinkClaudeCode). Falls back to a minimal stub
747
- * if no snapshot exists.
739
+ * Reset VS Code settings to pre-link state. Does NOT touching bingo
740
+ * settings.env the CLI keeps routing through the proxy independently.
748
741
  *
749
- * Without this, the only way to stop VS Code from using bingo would be to
750
- * delete ~/.claude/bingo/settings.json entirely which would also kill CLI
751
- * routing. Saving/restoring the pre-link env keeps CLI and VS Code independent.
742
+ * Restores the original claudeCode snapshot (vscodePreLinkClaudeCode)
743
+ * when available, otherwise writes an override that points VS Code to
744
+ * api.anthropic.com so the extension shows its native login screen.
752
745
  */
753
746
  async unlinkVscode(): Promise<{ paths: string[]; linked: boolean }> {
754
747
  const bingoSettings = await this.readSettings()
755
748
  const linkedPaths = (bingoSettings.vscodeLinkedPaths as string[]) || []
756
749
 
757
- // Restore bingo env to pre-link state
758
- const preLinkEnv = bingoSettings.vscodePreLinkEnv
759
- if (preLinkEnv !== undefined) {
760
- if (preLinkEnv === null || Object.keys(preLinkEnv as Record<string, unknown>).length === 0) {
761
- delete bingoSettings.env
762
- } else {
763
- bingoSettings.env = preLinkEnv
764
- }
765
- delete bingoSettings.vscodePreLinkEnv
766
- }
767
-
768
- // Fix #8: Restore original claudeCode snapshot per-path instead of a hardcoded stub.
769
- // Fall back to { preferredLocation: "panel" } when no snapshot exists for a path.
750
+ // Restore original claudeCode snapshot per-path. Fall back to a clean
770
751
  const preLinkClaudeCode =
771
752
  (bingoSettings.vscodePreLinkClaudeCode as Record<string, unknown> | undefined) || {}
772
753
 
@@ -774,7 +755,12 @@ export class ProviderService {
774
755
  try {
775
756
  const raw = await fs.readFile(settingsPath, "utf-8")
776
757
  const settings = JSON.parse(raw)
777
- settings.claudeCode = preLinkClaudeCode[settingsPath] ?? { preferredLocation: "panel" }
758
+ settings.claudeCode = preLinkClaudeCode[settingsPath] ?? {
759
+ preferredLocation: "panel",
760
+ environmentVariables: [
761
+ { name: "ANTHROPIC_BASE_URL", value: "https://api.anthropic.com" },
762
+ ],
763
+ }
778
764
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 4) + '\n')
779
765
  } catch {
780
766
  // File may have been moved or deleted since linking — harmless
@@ -784,6 +770,7 @@ export class ProviderService {
784
770
  bingoSettings.vscodeLinked = false
785
771
  delete bingoSettings.vscodeLinkedPaths
786
772
  delete bingoSettings.vscodePreLinkClaudeCode
773
+ delete bingoSettings.vscodePreLinkEnv
787
774
  await this.writeSettings(bingoSettings)
788
775
 
789
776
  return { paths: linkedPaths, linked: false }
package/src/tools.ts CHANGED
@@ -1,390 +1,397 @@
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/CronUpdateTool.js').CronUpdateTool,
33
- require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
34
- require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
35
- ]
36
- : []
37
- const RemoteTriggerTool = feature('AGENT_TRIGGERS_REMOTE')
38
- ? require('./tools/RemoteTriggerTool/RemoteTriggerTool.js').RemoteTriggerTool
39
- : null
40
- const MonitorTool = feature('MONITOR_TOOL')
41
- ? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
42
- : null
43
- const SendUserFileTool = feature('KAIROS')
44
- ? require('./tools/SendUserFileTool/SendUserFileTool.js').SendUserFileTool
45
- : null
46
- const PushNotificationTool =
47
- feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION')
48
- ? require('./tools/PushNotificationTool/PushNotificationTool.js')
49
- .PushNotificationTool
50
- : null
51
- const SubscribePRTool = feature('KAIROS_GITHUB_WEBHOOKS')
52
- ? require('./tools/SubscribePRTool/SubscribePRTool.js').SubscribePRTool
53
- : null
54
- /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
55
- import { TaskOutputTool } from './tools/TaskOutputTool/TaskOutputTool.js'
56
- import { WebSearchTool } from './tools/WebSearchTool/WebSearchTool.js'
57
- import { TodoWriteTool } from './tools/TodoWriteTool/TodoWriteTool.js'
58
- import { ExitPlanModeV2Tool } from './tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
59
- import { TestingPermissionTool } from './tools/testing/TestingPermissionTool.js'
60
- import { GrepTool } from './tools/GrepTool/GrepTool.js'
61
- import { TungstenTool } from './tools/TungstenTool/TungstenTool.js'
62
- // Lazy require to break circular dependency: tools.ts -> TeamCreateTool/TeamDeleteTool -> ... -> tools.ts
63
- /* eslint-disable @typescript-eslint/no-require-imports */
64
- const getTeamCreateTool = () =>
65
- require('./tools/TeamCreateTool/TeamCreateTool.js')
66
- .TeamCreateTool as typeof import('./tools/TeamCreateTool/TeamCreateTool.js').TeamCreateTool
67
- const getTeamDeleteTool = () =>
68
- require('./tools/TeamDeleteTool/TeamDeleteTool.js')
69
- .TeamDeleteTool as typeof import('./tools/TeamDeleteTool/TeamDeleteTool.js').TeamDeleteTool
70
- const getSendMessageTool = () =>
71
- require('./tools/SendMessageTool/SendMessageTool.js')
72
- .SendMessageTool as typeof import('./tools/SendMessageTool/SendMessageTool.js').SendMessageTool
73
- /* eslint-enable @typescript-eslint/no-require-imports */
74
- import { AskUserQuestionTool } from './tools/AskUserQuestionTool/AskUserQuestionTool.js'
75
- import { LSPTool } from './tools/LSPTool/LSPTool.js'
76
- import { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
77
- import { ReadMcpResourceTool } from './tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
78
- import { ToolSearchTool } from './tools/ToolSearchTool/ToolSearchTool.js'
79
- import { EnterPlanModeTool } from './tools/EnterPlanModeTool/EnterPlanModeTool.js'
80
- import { EnterWorktreeTool } from './tools/EnterWorktreeTool/EnterWorktreeTool.js'
81
- import { ExitWorktreeTool } from './tools/ExitWorktreeTool/ExitWorktreeTool.js'
82
- import { ConfigTool } from './tools/ConfigTool/ConfigTool.js'
83
- import { TaskCreateTool } from './tools/TaskCreateTool/TaskCreateTool.js'
84
- import { TaskGetTool } from './tools/TaskGetTool/TaskGetTool.js'
85
- import { TaskUpdateTool } from './tools/TaskUpdateTool/TaskUpdateTool.js'
86
- import { TaskListTool } from './tools/TaskListTool/TaskListTool.js'
87
- import uniqBy from 'lodash-es/uniqBy.js'
88
- import { isToolSearchEnabledOptimistic } from './utils/toolSearch.js'
89
- import { isTodoV2Enabled } from './utils/tasks.js'
90
- // Dead code elimination: conditional import for CLAUDE_CODE_VERIFY_PLAN
91
- /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
92
- const VerifyPlanExecutionTool =
93
- process.env.CLAUDE_CODE_VERIFY_PLAN === 'true'
94
- ? require('./tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.js')
95
- .VerifyPlanExecutionTool
96
- : null
97
- /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
98
- import { SYNTHETIC_OUTPUT_TOOL_NAME } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
99
- export {
100
- ALL_AGENT_DISALLOWED_TOOLS,
101
- CUSTOM_AGENT_DISALLOWED_TOOLS,
102
- ASYNC_AGENT_ALLOWED_TOOLS,
103
- COORDINATOR_MODE_ALLOWED_TOOLS,
104
- } from './constants/tools.js'
105
- import { feature } from 'bun:bundle'
106
- // Dead code elimination: conditional import for OVERFLOW_TEST_TOOL
107
- /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
108
- const OverflowTestTool = feature('OVERFLOW_TEST_TOOL')
109
- ? require('./tools/OverflowTestTool/OverflowTestTool.js').OverflowTestTool
110
- : null
111
- const CtxInspectTool = feature('CONTEXT_COLLAPSE')
112
- ? require('./tools/CtxInspectTool/CtxInspectTool.js').CtxInspectTool
113
- : null
114
- const TerminalCaptureTool = feature('TERMINAL_PANEL')
115
- ? require('./tools/TerminalCaptureTool/TerminalCaptureTool.js')
116
- .TerminalCaptureTool
117
- : null
118
- const WebBrowserTool = feature('WEB_BROWSER_TOOL')
119
- ? require('./tools/WebBrowserTool/WebBrowserTool.js').WebBrowserTool
120
- : null
121
- const coordinatorModeModule = feature('COORDINATOR_MODE')
122
- ? (require('./coordinator/coordinatorMode.js') as typeof import('./coordinator/coordinatorMode.js'))
123
- : null
124
- const SnipTool = feature('HISTORY_SNIP')
125
- ? require('./tools/SnipTool/SnipTool.js').SnipTool
126
- : null
127
- const ListPeersTool = feature('UDS_INBOX')
128
- ? require('./tools/ListPeersTool/ListPeersTool.js').ListPeersTool
129
- : null
130
- const WorkflowTool = feature('WORKFLOW_SCRIPTS')
131
- ? (() => {
132
- require('./tools/WorkflowTool/bundled/index.js').initBundledWorkflows()
133
- return require('./tools/WorkflowTool/WorkflowTool.js').WorkflowTool
134
- })()
135
- : null
136
- /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
137
- import type { ToolPermissionContext } from './Tool.js'
138
- import { getDenyRuleForTool } from './utils/permissions/permissions.js'
139
- import { hasEmbeddedSearchTools } from './utils/embeddedTools.js'
140
- import { isEnvTruthy } from './utils/envUtils.js'
141
- import { isPowerShellToolEnabled } from './utils/shell/shellToolUtils.js'
142
- import { isAgentSwarmsEnabled } from './utils/agentSwarmsEnabled.js'
143
- import { isWorktreeModeEnabled } from './utils/worktreeModeEnabled.js'
144
- import {
145
- REPL_TOOL_NAME,
146
- REPL_ONLY_TOOLS,
147
- isReplModeEnabled,
148
- } from './tools/REPLTool/constants.js'
149
- export { REPL_ONLY_TOOLS }
150
- /* eslint-disable @typescript-eslint/no-require-imports */
151
- const getPowerShellTool = () => {
152
- if (!isPowerShellToolEnabled()) return null
153
- return (
154
- require('./tools/PowerShellTool/PowerShellTool.js') as typeof import('./tools/PowerShellTool/PowerShellTool.js')
155
- ).PowerShellTool
156
- }
157
- /* eslint-enable @typescript-eslint/no-require-imports */
158
-
159
- /**
160
- * Predefined tool presets that can be used with --tools flag
161
- */
162
- export const TOOL_PRESETS = ['default'] as const
163
-
164
- export type ToolPreset = (typeof TOOL_PRESETS)[number]
165
-
166
- export function parseToolPreset(preset: string): ToolPreset | null {
167
- const presetString = preset.toLowerCase()
168
- if (!TOOL_PRESETS.includes(presetString as ToolPreset)) {
169
- return null
170
- }
171
- return presetString as ToolPreset
172
- }
173
-
174
- /**
175
- * Get the list of tool names for a given preset
176
- * Filters out tools that are disabled via isEnabled() check
177
- * @param preset The preset name
178
- * @returns Array of tool names
179
- */
180
- export function getToolsForDefaultPreset(): string[] {
181
- const tools = getAllBaseTools()
182
- const isEnabled = tools.map(tool => tool.isEnabled())
183
- return tools.filter((_, i) => isEnabled[i]).map(tool => tool.name)
184
- }
185
-
186
- /**
187
- * Get the complete exhaustive list of all tools that could be available
188
- * in the current environment (respecting process.env flags).
189
- * This is the source of truth for ALL tools.
190
- */
191
- /**
192
- * 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.
193
- */
194
- export function getAllBaseTools(): Tools {
195
- return [
196
- AgentTool,
197
- TaskOutputTool,
198
- BashTool,
199
- // Ant-native builds have bfs/ugrep embedded in the bun binary (same ARGV0
200
- // trick as ripgrep). When available, find/grep in Claude's shell are aliased
201
- // to these fast tools, so the dedicated Glob/Grep tools are unnecessary.
202
- ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
203
- ExitPlanModeV2Tool,
204
- FileReadTool,
205
- FileEditTool,
206
- FileWriteTool,
207
- NotebookEditTool,
208
- WebFetchTool,
209
- TodoWriteTool,
210
- WebSearchTool,
211
- TaskStopTool,
212
- AskUserQuestionTool,
213
- SkillTool,
214
- EnterPlanModeTool,
215
- ...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
216
- ...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),
217
- ...(SuggestBackgroundPRTool ? [SuggestBackgroundPRTool] : []),
218
- ...(WebBrowserTool ? [WebBrowserTool] : []),
219
- ...(isTodoV2Enabled()
220
- ? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool]
221
- : []),
222
- ...(OverflowTestTool ? [OverflowTestTool] : []),
223
- ...(CtxInspectTool ? [CtxInspectTool] : []),
224
- ...(TerminalCaptureTool ? [TerminalCaptureTool] : []),
225
- ...(isEnvTruthy(process.env.ENABLE_LSP_TOOL) ? [LSPTool] : []),
226
- ...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
227
- getSendMessageTool(),
228
- ...(ListPeersTool ? [ListPeersTool] : []),
229
- ...(isAgentSwarmsEnabled()
230
- ? [getTeamCreateTool(), getTeamDeleteTool()]
231
- : []),
232
- ...(VerifyPlanExecutionTool ? [VerifyPlanExecutionTool] : []),
233
- ...(process.env.USER_TYPE === 'ant' && REPLTool ? [REPLTool] : []),
234
- ...(WorkflowTool ? [WorkflowTool] : []),
235
- ...(SleepTool ? [SleepTool] : []),
236
- ...cronTools,
237
- ...(RemoteTriggerTool ? [RemoteTriggerTool] : []),
238
- ...(MonitorTool ? [MonitorTool] : []),
239
- BriefTool,
240
- ...(SendUserFileTool ? [SendUserFileTool] : []),
241
- ...(PushNotificationTool ? [PushNotificationTool] : []),
242
- ...(SubscribePRTool ? [SubscribePRTool] : []),
243
- ...(getPowerShellTool() ? [getPowerShellTool()] : []),
244
- ...(SnipTool ? [SnipTool] : []),
245
- ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
246
- ListMcpResourcesTool,
247
- ReadMcpResourceTool,
248
- // Include ToolSearchTool when tool search might be enabled (optimistic check)
249
- // The actual decision to defer tools happens at request time in claude.ts
250
- ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
251
- ]
252
- }
253
-
254
- /**
255
- * Filters out tools that are blanket-denied by the permission context.
256
- * A tool is filtered out if there's a deny rule matching its name with no
257
- * ruleContent (i.e., a blanket deny for that tool).
258
- *
259
- * Uses the same matcher as the runtime permission check (step 1a), so MCP
260
- * server-prefix rules like `mcp__server` strip all tools from that server
261
- * before the model sees them — not just at call time.
262
- */
263
- export function filterToolsByDenyRules<
264
- T extends {
265
- name: string
266
- mcpInfo?: { serverName: string; toolName: string }
267
- },
268
- >(tools: readonly T[], permissionContext: ToolPermissionContext): T[] {
269
- return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
270
- }
271
-
272
- export const getTools = (permissionContext: ToolPermissionContext): Tools => {
273
- // Simple mode: only Bash, Read, and Edit tools
274
- if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
275
- // --bare + REPL mode: REPL wraps Bash/Read/Edit/etc inside the VM, so
276
- // return REPL instead of the raw primitives. Matches the non-bare path
277
- // below which also hides REPL_ONLY_TOOLS when REPL is enabled.
278
- if (isReplModeEnabled() && REPLTool) {
279
- const replSimple: Tool[] = [REPLTool]
280
- if (
281
- feature('COORDINATOR_MODE') &&
282
- coordinatorModeModule?.isCoordinatorMode()
283
- ) {
284
- replSimple.push(TaskStopTool, getSendMessageTool())
285
- }
286
- return filterToolsByDenyRules(replSimple, permissionContext)
287
- }
288
- const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
289
- // When coordinator mode is also active, include AgentTool and TaskStopTool
290
- // so the coordinator gets Task+TaskStop (via useMergedTools filtering) and
291
- // workers get Bash/Read/Edit (via filterToolsForAgent filtering).
292
- if (
293
- feature('COORDINATOR_MODE') &&
294
- coordinatorModeModule?.isCoordinatorMode()
295
- ) {
296
- simpleTools.push(AgentTool, TaskStopTool, getSendMessageTool())
297
- }
298
- return filterToolsByDenyRules(simpleTools, permissionContext)
299
- }
300
-
301
- // Get all base tools and filter out special tools that get added conditionally
302
- const specialTools = new Set([
303
- ListMcpResourcesTool.name,
304
- ReadMcpResourceTool.name,
305
- SYNTHETIC_OUTPUT_TOOL_NAME,
306
- ])
307
-
308
- const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
309
-
310
- // Filter out tools that are denied by the deny rules
311
- let allowedTools = filterToolsByDenyRules(tools, permissionContext)
312
-
313
- // When REPL mode is enabled, hide primitive tools from direct use.
314
- // They're still accessible inside REPL via the VM context.
315
- if (isReplModeEnabled()) {
316
- const replEnabled = allowedTools.some(tool =>
317
- toolMatchesName(tool, REPL_TOOL_NAME),
318
- )
319
- if (replEnabled) {
320
- allowedTools = allowedTools.filter(
321
- tool => !REPL_ONLY_TOOLS.has(tool.name),
322
- )
323
- }
324
- }
325
-
326
- const isEnabled = allowedTools.map(_ => _.isEnabled())
327
- return allowedTools.filter((_, i) => isEnabled[i])
328
- }
329
-
330
- /**
331
- * Assemble the full tool pool for a given permission context and MCP tools.
332
- *
333
- * This is the single source of truth for combining built-in tools with MCP tools.
334
- * Both REPL.tsx (via useMergedTools hook) and runAgent.ts (for coordinator workers)
335
- * use this function to ensure consistent tool pool assembly.
336
- *
337
- * The function:
338
- * 1. Gets built-in tools via getTools() (respects mode filtering)
339
- * 2. Filters MCP tools by deny rules
340
- * 3. Deduplicates by tool name (built-in tools take precedence)
341
- *
342
- * @param permissionContext - Permission context for filtering built-in tools
343
- * @param mcpTools - MCP tools from appState.mcp.tools
344
- * @returns Combined, deduplicated array of built-in and MCP tools
345
- */
346
- export function assembleToolPool(
347
- permissionContext: ToolPermissionContext,
348
- mcpTools: Tools,
349
- ): Tools {
350
- const builtInTools = getTools(permissionContext)
351
-
352
- // Filter out MCP tools that are in the deny list
353
- const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
354
-
355
- // Sort each partition for prompt-cache stability, keeping built-ins as a
356
- // contiguous prefix. The server's claude_code_system_cache_policy places a
357
- // global cache breakpoint after the last prefix-matched built-in tool; a flat
358
- // sort would interleave MCP tools into built-ins and invalidate all downstream
359
- // cache keys whenever an MCP tool sorts between existing built-ins. uniqBy
360
- // preserves insertion order, so built-ins win on name conflict.
361
- // Avoid Array.toSorted (Node 20+) — we support Node 18. builtInTools is
362
- // readonly so copy-then-sort; allowedMcpTools is a fresh .filter() result.
363
- const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
364
- return uniqBy(
365
- [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
366
- 'name',
367
- )
368
- }
369
-
370
- /**
371
- * Get all tools including both built-in tools and MCP tools.
372
- *
373
- * This is the preferred function when you need the complete tools list for:
374
- * - Tool search threshold calculations (isToolSearchEnabled)
375
- * - Token counting that includes MCP tools
376
- * - Any context where MCP tools should be considered
377
- *
378
- * Use getTools() only when you specifically need just built-in tools.
379
- *
380
- * @param permissionContext - Permission context for filtering built-in tools
381
- * @param mcpTools - MCP tools from appState.mcp.tools
382
- * @returns Combined array of built-in and MCP tools
383
- */
384
- export function getMergedTools(
385
- permissionContext: ToolPermissionContext,
386
- mcpTools: Tools,
387
- ): Tools {
388
- const builtInTools = getTools(permissionContext)
389
- return [...builtInTools, ...mcpTools]
390
- }
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/CronUpdateTool.js').CronUpdateTool,
33
+ require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
34
+ require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
35
+ ]
36
+ : []
37
+ const RemoteTriggerTool = feature('AGENT_TRIGGERS_REMOTE')
38
+ ? require('./tools/RemoteTriggerTool/RemoteTriggerTool.js').RemoteTriggerTool
39
+ : null
40
+ const MonitorTool = feature('MONITOR_TOOL')
41
+ ? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
42
+ : null
43
+ const SendUserFileTool = feature('KAIROS')
44
+ ? require('./tools/SendUserFileTool/SendUserFileTool.js').SendUserFileTool
45
+ : null
46
+ const PushNotificationTool =
47
+ feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION')
48
+ ? require('./tools/PushNotificationTool/PushNotificationTool.js')
49
+ .PushNotificationTool
50
+ : null
51
+ const SubscribePRTool = feature('KAIROS_GITHUB_WEBHOOKS')
52
+ ? require('./tools/SubscribePRTool/SubscribePRTool.js').SubscribePRTool
53
+ : null
54
+ /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
55
+ import { TaskOutputTool } from './tools/TaskOutputTool/TaskOutputTool.js'
56
+ import { WebSearchTool } from './tools/WebSearchTool/WebSearchTool.js'
57
+ import { TodoWriteTool } from './tools/TodoWriteTool/TodoWriteTool.js'
58
+ import { ExitPlanModeV2Tool } from './tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
59
+ import { TestingPermissionTool } from './tools/testing/TestingPermissionTool.js'
60
+ import { GrepTool } from './tools/GrepTool/GrepTool.js'
61
+ import { TungstenTool } from './tools/TungstenTool/TungstenTool.js'
62
+ // Lazy require to break circular dependency: tools.ts -> TeamCreateTool/TeamDeleteTool -> ... -> tools.ts
63
+ /* eslint-disable @typescript-eslint/no-require-imports */
64
+ const getTeamCreateTool = () =>
65
+ require('./tools/TeamCreateTool/TeamCreateTool.js')
66
+ .TeamCreateTool as typeof import('./tools/TeamCreateTool/TeamCreateTool.js').TeamCreateTool
67
+ const getTeamDeleteTool = () =>
68
+ require('./tools/TeamDeleteTool/TeamDeleteTool.js')
69
+ .TeamDeleteTool as typeof import('./tools/TeamDeleteTool/TeamDeleteTool.js').TeamDeleteTool
70
+ const getSendMessageTool = () =>
71
+ require('./tools/SendMessageTool/SendMessageTool.js')
72
+ .SendMessageTool as typeof import('./tools/SendMessageTool/SendMessageTool.js').SendMessageTool
73
+ /* eslint-enable @typescript-eslint/no-require-imports */
74
+ import { AskUserQuestionTool } from './tools/AskUserQuestionTool/AskUserQuestionTool.js'
75
+ import { LSPTool } from './tools/LSPTool/LSPTool.js'
76
+ import { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
77
+ import { ReadMcpResourceTool } from './tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
78
+ import { ToolSearchTool } from './tools/ToolSearchTool/ToolSearchTool.js'
79
+ import { EnterPlanModeTool } from './tools/EnterPlanModeTool/EnterPlanModeTool.js'
80
+ import { EnterWorktreeTool } from './tools/EnterWorktreeTool/EnterWorktreeTool.js'
81
+ import { ExitWorktreeTool } from './tools/ExitWorktreeTool/ExitWorktreeTool.js'
82
+ import { ConfigTool } from './tools/ConfigTool/ConfigTool.js'
83
+ import { TaskCreateTool } from './tools/TaskCreateTool/TaskCreateTool.js'
84
+ import { TaskGetTool } from './tools/TaskGetTool/TaskGetTool.js'
85
+ import { TaskUpdateTool } from './tools/TaskUpdateTool/TaskUpdateTool.js'
86
+ import { TaskListTool } from './tools/TaskListTool/TaskListTool.js'
87
+ import uniqBy from 'lodash-es/uniqBy.js'
88
+ import { isToolSearchEnabledOptimistic } from './utils/toolSearch.js'
89
+ import { isTodoV2Enabled } from './utils/tasks.js'
90
+ // Dead code elimination: conditional import for CLAUDE_CODE_VERIFY_PLAN
91
+ /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
92
+ const VerifyPlanExecutionTool =
93
+ process.env.CLAUDE_CODE_VERIFY_PLAN === 'true'
94
+ ? require('./tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.js')
95
+ .VerifyPlanExecutionTool
96
+ : null
97
+ /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
98
+ import { SYNTHETIC_OUTPUT_TOOL_NAME } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
99
+ export {
100
+ ALL_AGENT_DISALLOWED_TOOLS,
101
+ CUSTOM_AGENT_DISALLOWED_TOOLS,
102
+ ASYNC_AGENT_ALLOWED_TOOLS,
103
+ COORDINATOR_MODE_ALLOWED_TOOLS,
104
+ } from './constants/tools.js'
105
+ import { feature } from 'bun:bundle'
106
+ // Dead code elimination: conditional import for OVERFLOW_TEST_TOOL
107
+ /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
108
+ const OverflowTestTool = feature('OVERFLOW_TEST_TOOL')
109
+ ? require('./tools/OverflowTestTool/OverflowTestTool.js').OverflowTestTool
110
+ : null
111
+ const CtxInspectTool = feature('CONTEXT_COLLAPSE')
112
+ ? require('./tools/CtxInspectTool/CtxInspectTool.js').CtxInspectTool
113
+ : null
114
+ const TerminalCaptureTool = feature('TERMINAL_PANEL')
115
+ ? require('./tools/TerminalCaptureTool/TerminalCaptureTool.js')
116
+ .TerminalCaptureTool
117
+ : null
118
+ const WebBrowserTool = feature('WEB_BROWSER_TOOL')
119
+ ? require('./tools/WebBrowserTool/WebBrowserTool.js').WebBrowserTool
120
+ : null
121
+ const coordinatorModeModule = feature('COORDINATOR_MODE')
122
+ ? (require('./coordinator/coordinatorMode.js') as typeof import('./coordinator/coordinatorMode.js'))
123
+ : null
124
+ const SnipTool = feature('HISTORY_SNIP')
125
+ ? require('./tools/SnipTool/SnipTool.js').SnipTool
126
+ : null
127
+ const ListPeersTool = feature('UDS_INBOX')
128
+ ? require('./tools/ListPeersTool/ListPeersTool.js').ListPeersTool
129
+ : null
130
+ const WorkflowTool = feature('WORKFLOW_SCRIPTS')
131
+ ? (() => {
132
+ require('./tools/WorkflowTool/bundled/index.js').initBundledWorkflows()
133
+ return require('./tools/WorkflowTool/WorkflowTool.js').WorkflowTool
134
+ })()
135
+ : null
136
+ /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
137
+ import type { ToolPermissionContext } from './Tool.js'
138
+ import { getDenyRuleForTool } from './utils/permissions/permissions.js'
139
+ import { hasEmbeddedSearchTools } from './utils/embeddedTools.js'
140
+ import { isEnvTruthy } from './utils/envUtils.js'
141
+ import { isPowerShellToolEnabled } from './utils/shell/shellToolUtils.js'
142
+ import { isAgentSwarmsEnabled } from './utils/agentSwarmsEnabled.js'
143
+ import { isWorktreeModeEnabled } from './utils/worktreeModeEnabled.js'
144
+ import {
145
+ REPL_TOOL_NAME,
146
+ REPL_ONLY_TOOLS,
147
+ isReplModeEnabled,
148
+ } from './tools/REPLTool/constants.js'
149
+ export { REPL_ONLY_TOOLS }
150
+ /* eslint-disable @typescript-eslint/no-require-imports */
151
+ const getPowerShellTool = () => {
152
+ if (!isPowerShellToolEnabled()) return null
153
+ return (
154
+ require('./tools/PowerShellTool/PowerShellTool.js') as typeof import('./tools/PowerShellTool/PowerShellTool.js')
155
+ ).PowerShellTool
156
+ }
157
+ /* eslint-enable @typescript-eslint/no-require-imports */
158
+
159
+ /**
160
+ * Predefined tool presets that can be used with --tools flag
161
+ */
162
+ export const TOOL_PRESETS = ['default'] as const
163
+
164
+ export type ToolPreset = (typeof TOOL_PRESETS)[number]
165
+
166
+ export function parseToolPreset(preset: string): ToolPreset | null {
167
+ const presetString = preset.toLowerCase()
168
+ if (!TOOL_PRESETS.includes(presetString as ToolPreset)) {
169
+ return null
170
+ }
171
+ return presetString as ToolPreset
172
+ }
173
+
174
+ /**
175
+ * Get the list of tool names for a given preset
176
+ * Filters out tools that are disabled via isEnabled() check
177
+ * @param preset The preset name
178
+ * @returns Array of tool names
179
+ */
180
+ export function getToolsForDefaultPreset(): string[] {
181
+ const tools = getAllBaseTools()
182
+ const isEnabled = tools.map(tool => tool.isEnabled())
183
+ return tools.filter((_, i) => isEnabled[i]).map(tool => tool.name)
184
+ }
185
+
186
+ /**
187
+ * Get the complete exhaustive list of all tools that could be available
188
+ * in the current environment (respecting process.env flags).
189
+ * This is the source of truth for ALL tools.
190
+ */
191
+ /**
192
+ * 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.
193
+ */
194
+ export function getAllBaseTools(): Tools {
195
+ return [
196
+ AgentTool,
197
+ TaskOutputTool,
198
+ BashTool,
199
+ // Ant-native builds have bfs/ugrep embedded in the bun binary (same ARGV0
200
+ // trick as ripgrep). When available, find/grep in Claude's shell are aliased
201
+ // to these fast tools, so the dedicated Glob/Grep tools are unnecessary.
202
+ ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
203
+ ExitPlanModeV2Tool,
204
+ FileReadTool,
205
+ FileEditTool,
206
+ FileWriteTool,
207
+ NotebookEditTool,
208
+ WebFetchTool,
209
+ TodoWriteTool,
210
+ WebSearchTool,
211
+ TaskStopTool,
212
+ AskUserQuestionTool,
213
+ SkillTool,
214
+ EnterPlanModeTool,
215
+ ...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
216
+ ...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),
217
+ ...(SuggestBackgroundPRTool ? [SuggestBackgroundPRTool] : []),
218
+ ...(WebBrowserTool ? [WebBrowserTool] : []),
219
+ ...(isTodoV2Enabled()
220
+ ? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool]
221
+ : []),
222
+ ...(OverflowTestTool ? [OverflowTestTool] : []),
223
+ ...(CtxInspectTool ? [CtxInspectTool] : []),
224
+ ...(TerminalCaptureTool ? [TerminalCaptureTool] : []),
225
+ ...(isEnvTruthy(process.env.ENABLE_LSP_TOOL) ? [LSPTool] : []),
226
+ ...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
227
+ getSendMessageTool(),
228
+ ...(ListPeersTool ? [ListPeersTool] : []),
229
+ ...(isAgentSwarmsEnabled()
230
+ ? [getTeamCreateTool(), getTeamDeleteTool()]
231
+ : []),
232
+ ...(VerifyPlanExecutionTool ? [VerifyPlanExecutionTool] : []),
233
+ ...(process.env.USER_TYPE === 'ant' && REPLTool ? [REPLTool] : []),
234
+ ...(WorkflowTool ? [WorkflowTool] : []),
235
+ ...(SleepTool ? [SleepTool] : []),
236
+ ...cronTools,
237
+ ...(RemoteTriggerTool ? [RemoteTriggerTool] : []),
238
+ ...(MonitorTool ? [MonitorTool] : []),
239
+ BriefTool,
240
+ ...(SendUserFileTool ? [SendUserFileTool] : []),
241
+ ...(PushNotificationTool ? [PushNotificationTool] : []),
242
+ ...(SubscribePRTool ? [SubscribePRTool] : []),
243
+ ...(getPowerShellTool() ? [getPowerShellTool()] : []),
244
+ ...(SnipTool ? [SnipTool] : []),
245
+ ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
246
+ ListMcpResourcesTool,
247
+ ReadMcpResourceTool,
248
+ // Include ToolSearchTool when tool search might be enabled (optimistic check)
249
+ // The actual decision to defer tools happens at request time in claude.ts
250
+ ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
251
+ ]
252
+ }
253
+
254
+ /**
255
+ * Filters out tools that are blanket-denied by the permission context.
256
+ * A tool is filtered out if there's a deny rule matching its name with no
257
+ * ruleContent (i.e., a blanket deny for that tool).
258
+ *
259
+ * Uses the same matcher as the runtime permission check (step 1a), so MCP
260
+ * server-prefix rules like `mcp__server` strip all tools from that server
261
+ * before the model sees them — not just at call time.
262
+ */
263
+ export function filterToolsByDenyRules<
264
+ T extends {
265
+ name: string
266
+ mcpInfo?: { serverName: string; toolName: string }
267
+ },
268
+ >(tools: readonly T[], permissionContext: ToolPermissionContext): T[] {
269
+ return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
270
+ }
271
+
272
+ export const getTools = (permissionContext: ToolPermissionContext, brainMode?: boolean): Tools => {
273
+ // Brain mode: coordinator only needs Agent + AskUserQuestion
274
+ if (brainMode) {
275
+ return filterToolsByDenyRules([AgentTool, AskUserQuestionTool], permissionContext)
276
+ }
277
+
278
+ // Simple mode: only Bash, Read, and Edit tools
279
+ if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
280
+ // --bare + REPL mode: REPL wraps Bash/Read/Edit/etc inside the VM, so
281
+ // return REPL instead of the raw primitives. Matches the non-bare path
282
+ // below which also hides REPL_ONLY_TOOLS when REPL is enabled.
283
+ if (isReplModeEnabled() && REPLTool) {
284
+ const replSimple: Tool[] = [REPLTool]
285
+ if (
286
+ feature('COORDINATOR_MODE') &&
287
+ coordinatorModeModule?.isCoordinatorMode()
288
+ ) {
289
+ replSimple.push(TaskStopTool, getSendMessageTool())
290
+ }
291
+ return filterToolsByDenyRules(replSimple, permissionContext)
292
+ }
293
+ const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
294
+ // When coordinator mode is also active, include AgentTool and TaskStopTool
295
+ // so the coordinator gets Task+TaskStop (via useMergedTools filtering) and
296
+ // workers get Bash/Read/Edit (via filterToolsForAgent filtering).
297
+ if (
298
+ feature('COORDINATOR_MODE') &&
299
+ coordinatorModeModule?.isCoordinatorMode()
300
+ ) {
301
+ simpleTools.push(AgentTool, TaskStopTool, getSendMessageTool())
302
+ }
303
+ return filterToolsByDenyRules(simpleTools, permissionContext)
304
+ }
305
+
306
+ // Get all base tools and filter out special tools that get added conditionally
307
+ const specialTools = new Set([
308
+ ListMcpResourcesTool.name,
309
+ ReadMcpResourceTool.name,
310
+ SYNTHETIC_OUTPUT_TOOL_NAME,
311
+ ])
312
+
313
+ const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
314
+
315
+ // Filter out tools that are denied by the deny rules
316
+ let allowedTools = filterToolsByDenyRules(tools, permissionContext)
317
+
318
+ // When REPL mode is enabled, hide primitive tools from direct use.
319
+ // They're still accessible inside REPL via the VM context.
320
+ if (isReplModeEnabled()) {
321
+ const replEnabled = allowedTools.some(tool =>
322
+ toolMatchesName(tool, REPL_TOOL_NAME),
323
+ )
324
+ if (replEnabled) {
325
+ allowedTools = allowedTools.filter(
326
+ tool => !REPL_ONLY_TOOLS.has(tool.name),
327
+ )
328
+ }
329
+ }
330
+
331
+ const isEnabled = allowedTools.map(_ => _.isEnabled())
332
+ return allowedTools.filter((_, i) => isEnabled[i])
333
+ }
334
+
335
+ /**
336
+ * Assemble the full tool pool for a given permission context and MCP tools.
337
+ *
338
+ * This is the single source of truth for combining built-in tools with MCP tools.
339
+ * Both REPL.tsx (via useMergedTools hook) and runAgent.ts (for coordinator workers)
340
+ * use this function to ensure consistent tool pool assembly.
341
+ *
342
+ * The function:
343
+ * 1. Gets built-in tools via getTools() (respects mode filtering)
344
+ * 2. Filters MCP tools by deny rules
345
+ * 3. Deduplicates by tool name (built-in tools take precedence)
346
+ *
347
+ * @param permissionContext - Permission context for filtering built-in tools
348
+ * @param mcpTools - MCP tools from appState.mcp.tools
349
+ * @returns Combined, deduplicated array of built-in and MCP tools
350
+ */
351
+ export function assembleToolPool(
352
+ permissionContext: ToolPermissionContext,
353
+ mcpTools: Tools,
354
+ brainMode?: boolean,
355
+ ): Tools {
356
+ const effectiveBrainMode = brainMode ?? false
357
+ const builtInTools = getTools(permissionContext, effectiveBrainMode)
358
+
359
+ // When brainMode is active, skip MCP tools entirely
360
+ const filteredMcpTools = effectiveBrainMode ? [] : filterToolsByDenyRules(mcpTools, permissionContext)
361
+
362
+ // Sort each partition for prompt-cache stability, keeping built-ins as a
363
+ // contiguous prefix. The server's claude_code_system_cache_policy places a
364
+ // global cache breakpoint after the last prefix-matched built-in tool; a flat
365
+ // sort would interleave MCP tools into built-ins and invalidate all downstream
366
+ // cache keys whenever an MCP tool sorts between existing built-ins. uniqBy
367
+ // preserves insertion order, so built-ins win on name conflict.
368
+ // Avoid Array.toSorted (Node 20+) — we support Node 18. builtInTools is
369
+ // readonly so copy-then-sort; filteredMcpTools is a fresh .filter() result.
370
+ const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
371
+ return uniqBy(
372
+ [...builtInTools].sort(byName).concat(filteredMcpTools.sort(byName)),
373
+ 'name',
374
+ )
375
+ }
376
+
377
+ /**
378
+ * Get all tools including both built-in tools and MCP tools.
379
+ *
380
+ * This is the preferred function when you need the complete tools list for:
381
+ * - Tool search threshold calculations (isToolSearchEnabled)
382
+ * - Token counting that includes MCP tools
383
+ * - Any context where MCP tools should be considered
384
+ *
385
+ * Use getTools() only when you specifically need just built-in tools.
386
+ *
387
+ * @param permissionContext - Permission context for filtering built-in tools
388
+ * @param mcpTools - MCP tools from appState.mcp.tools
389
+ * @returns Combined array of built-in and MCP tools
390
+ */
391
+ export function getMergedTools(
392
+ permissionContext: ToolPermissionContext,
393
+ mcpTools: Tools,
394
+ ): Tools {
395
+ const builtInTools = getTools(permissionContext)
396
+ return [...builtInTools, ...mcpTools]
397
+ }