@xortex/xcode 3.0.8 → 3.1.1

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