bingocode 1.1.185 → 1.1.187

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/src/commands.ts CHANGED
@@ -1,752 +1,754 @@
1
- // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
2
- import addDir from './commands/add-dir/index.js'
3
- import autofixPr from './commands/autofix-pr/index.js'
4
- import backfillSessions from './commands/backfill-sessions/index.js'
5
- import btw from './commands/btw/index.js'
6
- import goodClaude from './commands/good-claude/index.js'
7
- import issue from './commands/issue/index.js'
8
- import feedback from './commands/feedback/index.js'
9
- import clear from './commands/clear/index.js'
10
- import color from './commands/color/index.js'
11
- import commit from './commands/commit.js'
12
- import copy from './commands/copy/index.js'
13
- import desktop from './commands/desktop/index.js'
14
- import commitPushPr from './commands/commit-push-pr.js'
15
- import compact from './commands/compact/index.js'
16
- import config from './commands/config/index.js'
17
- import { context, contextNonInteractive } from './commands/context/index.js'
18
- import cost from './commands/cost/index.js'
19
- import diff from './commands/diff/index.js'
20
- import ctx_viz from './commands/ctx_viz/index.js'
21
- import doctor from './commands/doctor/index.js'
22
- import memory from './commands/memory/index.js'
23
- import help from './commands/help/index.js'
24
- import ide from './commands/ide/index.js'
25
- import init from './commands/init.js'
26
- import initVerifiers from './commands/init-verifiers.js'
27
- import keybindings from './commands/keybindings/index.js'
28
- import login from './commands/login/index.js'
29
- import logout from './commands/logout/index.js'
30
- import installGitHubApp from './commands/install-github-app/index.js'
31
- import installSlackApp from './commands/install-slack-app/index.js'
32
- import breakCache from './commands/break-cache/index.js'
33
- import mcp from './commands/mcp/index.js'
34
- import mobile from './commands/mobile/index.js'
35
- import onboarding from './commands/onboarding/index.js'
36
- import pr_comments from './commands/pr_comments/index.js'
37
- import releaseNotes from './commands/release-notes/index.js'
38
- import rename from './commands/rename/index.js'
39
- import resume from './commands/resume/index.js'
40
- import review, { ultrareview } from './commands/review.js'
41
- import session from './commands/session/index.js'
42
- import share from './commands/share/index.js'
43
- import skills from './commands/skills/index.js'
44
- import status from './commands/status/index.js'
45
- import tasks from './commands/tasks/index.js'
46
- import teleport from './commands/teleport/index.js'
47
- /* eslint-disable @typescript-eslint/no-require-imports */
48
- const agentsPlatform =
49
- process.env.USER_TYPE === 'ant'
50
- ? require('./commands/agents-platform/index.js').default
51
- : null
52
- /* eslint-enable @typescript-eslint/no-require-imports */
53
- import securityReview from './commands/security-review.js'
54
- import bughunter from './commands/bughunter/index.js'
55
- import terminalSetup from './commands/terminalSetup/index.js'
56
- import usage from './commands/usage/index.js'
57
- import theme from './commands/theme/index.js'
58
- import vim from './commands/vim/index.js'
59
- import { feature } from 'bun:bundle'
60
- // Dead code elimination: conditional imports
61
- /* eslint-disable @typescript-eslint/no-require-imports */
62
- const proactive =
63
- feature('PROACTIVE') || feature('KAIROS')
64
- ? require('./commands/proactive.js').default
65
- : null
66
- const briefCommand =
67
- feature('KAIROS') || feature('KAIROS_BRIEF')
68
- ? require('./commands/brief.js').default
69
- : null
70
- const assistantCommand = feature('KAIROS')
71
- ? require('./commands/assistant/index.js').default
72
- : null
73
- const bridge = feature('BRIDGE_MODE')
74
- ? require('./commands/bridge/index.js').default
75
- : null
76
- const remoteControlServerCommand =
77
- feature('DAEMON') && feature('BRIDGE_MODE')
78
- ? require('./commands/remoteControlServer/index.js').default
79
- : null
80
- const voiceCommand = feature('VOICE_MODE')
81
- ? require('./commands/voice/index.js').default
82
- : null
83
- const forceSnip = feature('HISTORY_SNIP')
84
- ? require('./commands/force-snip.js').default
85
- : null
86
- const workflowsCmd = feature('WORKFLOW_SCRIPTS')
87
- ? (
88
- require('./commands/workflows/index.js') as typeof import('./commands/workflows/index.js')
89
- ).default
90
- : null
91
- const webCmd = feature('CCR_REMOTE_SETUP')
92
- ? (
93
- require('./commands/remote-setup/index.js') as typeof import('./commands/remote-setup/index.js')
94
- ).default
95
- : null
96
- const clearSkillIndexCache = feature('EXPERIMENTAL_SKILL_SEARCH')
97
- ? (
98
- require('./services/skillSearch/localSearch.js') as typeof import('./services/skillSearch/localSearch.js')
99
- ).clearSkillIndexCache
100
- : null
101
- const subscribePr = feature('KAIROS_GITHUB_WEBHOOKS')
102
- ? require('./commands/subscribe-pr.js').default
103
- : null
104
- const ultraplan = feature('ULTRAPLAN')
105
- ? require('./commands/ultraplan.js').default
106
- : null
107
- const torch = feature('TORCH') ? require('./commands/torch.js').default : null
108
- const peersCmd = feature('UDS_INBOX')
109
- ? (
110
- require('./commands/peers/index.js') as typeof import('./commands/peers/index.js')
111
- ).default
112
- : null
113
- const forkCmd = feature('FORK_SUBAGENT')
114
- ? (
115
- require('./commands/fork/index.js') as typeof import('./commands/fork/index.js')
116
- ).default
117
- : null
118
- const buddy = (
119
- require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js')
120
- ).default
121
- /* eslint-enable @typescript-eslint/no-require-imports */
122
- import thinkback from './commands/thinkback/index.js'
123
- import thinkbackPlay from './commands/thinkback-play/index.js'
124
- import permissions from './commands/permissions/index.js'
125
- import plan from './commands/plan/index.js'
126
- import fast from './commands/fast/index.js'
127
- import passes from './commands/passes/index.js'
128
- import privacySettings from './commands/privacy-settings/index.js'
129
- import hooks from './commands/hooks/index.js'
130
- import files from './commands/files/index.js'
131
- import branch from './commands/branch/index.js'
132
- import agents from './commands/agents/index.js'
133
- import plugin from './commands/plugin/index.js'
134
- import reloadPlugins from './commands/reload-plugins/index.js'
135
- import rewind from './commands/rewind/index.js'
136
- import heapDump from './commands/heapdump/index.js'
137
- import mockLimits from './commands/mock-limits/index.js'
138
- import bridgeKick from './commands/bridge-kick.js'
139
- import version from './commands/version.js'
140
- import summary from './commands/summary/index.js'
141
- import {
142
- resetLimits,
143
- resetLimitsNonInteractive,
144
- } from './commands/reset-limits/index.js'
145
- import antTrace from './commands/ant-trace/index.js'
146
- import perfIssue from './commands/perf-issue/index.js'
147
- import sandboxToggle from './commands/sandbox-toggle/index.js'
148
- import chrome from './commands/chrome/index.js'
149
- import stickers from './commands/stickers/index.js'
150
- import advisor from './commands/advisor.js'
151
- import { logError } from './utils/log.js'
152
- import { toError } from './utils/errors.js'
153
- import { logForDebugging } from './utils/debug.js'
154
- import {
155
- getSkillDirCommands,
156
- clearSkillCaches,
157
- getDynamicSkills,
158
- } from './skills/loadSkillsDir.js'
159
- import { getBundledSkills } from './skills/bundledSkills.js'
160
- import { getBuiltinPluginSkillCommands } from './plugins/builtinPlugins.js'
161
- import {
162
- getPluginCommands,
163
- clearPluginCommandCache,
164
- getPluginSkills,
165
- clearPluginSkillsCache,
166
- } from './utils/plugins/loadPluginCommands.js'
167
- import memoize from 'lodash-es/memoize.js'
168
- import { isUsing3PServices, isClaudeAISubscriber } from './utils/auth.js'
169
- import { isFirstPartyAnthropicBaseUrl } from './utils/model/providers.js'
170
- import env from './commands/env/index.js'
171
- import exit from './commands/exit/index.js'
172
- import exportCommand from './commands/export/index.js'
173
- import model from './commands/model/index.js'
174
- import tag from './commands/tag/index.js'
175
- import outputStyle from './commands/output-style/index.js'
176
- import remoteEnv from './commands/remote-env/index.js'
177
- import upgrade from './commands/upgrade/index.js'
178
- import {
179
- extraUsage,
180
- extraUsageNonInteractive,
181
- } from './commands/extra-usage/index.js'
182
- import rateLimitOptions from './commands/rate-limit-options/index.js'
183
- import statusline from './commands/statusline.js'
184
- import effort from './commands/effort/index.js'
185
- import stats from './commands/stats/index.js'
186
- // insights.ts is 113KB (3200 lines, includes diffLines/html rendering). Lazy
187
- // shim defers the heavy module until /insights is actually invoked.
188
- const usageReport: Command = {
189
- type: 'prompt',
190
- name: 'insights',
191
- description: 'Generate a report analyzing your Claude Code sessions',
192
- contentLength: 0,
193
- progressMessage: 'analyzing your sessions',
194
- source: 'builtin',
195
- async getPromptForCommand(args, context) {
196
- const real = (await import('./commands/insights.js')).default
197
- if (real.type !== 'prompt') throw new Error('unreachable')
198
- return real.getPromptForCommand(args, context)
199
- },
200
- }
201
- import oauthRefresh from './commands/oauth-refresh/index.js'
202
- import debugToolCall from './commands/debug-tool-call/index.js'
203
- import { getSettingSourceName } from './utils/settings/constants.js'
204
- import {
205
- type Command,
206
- getCommandName,
207
- isCommandEnabled,
208
- } from './types/command.js'
209
-
210
- // Re-export types from the centralized location
211
- export type {
212
- Command,
213
- CommandBase,
214
- CommandResultDisplay,
215
- LocalCommandResult,
216
- LocalJSXCommandContext,
217
- PromptCommand,
218
- ResumeEntrypoint,
219
- } from './types/command.js'
220
- export { getCommandName, isCommandEnabled } from './types/command.js'
221
-
222
- // Commands that get eliminated from the external build
223
- export const INTERNAL_ONLY_COMMANDS = [
224
- backfillSessions,
225
- breakCache,
226
- bughunter,
227
- commit,
228
- commitPushPr,
229
- ctx_viz,
230
- goodClaude,
231
- issue,
232
- initVerifiers,
233
- ...(forceSnip ? [forceSnip] : []),
234
- mockLimits,
235
- bridgeKick,
236
- version,
237
- ...(ultraplan ? [ultraplan] : []),
238
- ...(subscribePr ? [subscribePr] : []),
239
- resetLimits,
240
- resetLimitsNonInteractive,
241
- onboarding,
242
- share,
243
- summary,
244
- teleport,
245
- antTrace,
246
- perfIssue,
247
- env,
248
- oauthRefresh,
249
- debugToolCall,
250
- agentsPlatform,
251
- autofixPr,
252
- ].filter(Boolean)
253
-
254
- // Declared as a function so that we don't run this until getCommands is called,
255
- // since underlying functions read from config, which can't be read at module initialization time
256
- const COMMANDS = memoize((): Command[] => [
257
- addDir,
258
- advisor,
259
- agents,
260
- branch,
261
- btw,
262
- chrome,
263
- clear,
264
- color,
265
- compact,
266
- config,
267
- copy,
268
- desktop,
269
- context,
270
- contextNonInteractive,
271
- cost,
272
- diff,
273
- doctor,
274
- effort,
275
- exit,
276
- fast,
277
- files,
278
- heapDump,
279
- help,
280
- ide,
281
- init,
282
- keybindings,
283
- installGitHubApp,
284
- installSlackApp,
285
- mcp,
286
- memory,
287
- mobile,
288
- model,
289
- outputStyle,
290
- remoteEnv,
291
- plugin,
292
- pr_comments,
293
- releaseNotes,
294
- reloadPlugins,
295
- rename,
296
- resume,
297
- session,
298
- skills,
299
- stats,
300
- status,
301
- statusline,
302
- stickers,
303
- tag,
304
- theme,
305
- feedback,
306
- review,
307
- ultrareview,
308
- rewind,
309
- securityReview,
310
- terminalSetup,
311
- upgrade,
312
- extraUsage,
313
- extraUsageNonInteractive,
314
- rateLimitOptions,
315
- usage,
316
- usageReport,
317
- vim,
318
- ...(webCmd ? [webCmd] : []),
319
- ...(forkCmd ? [forkCmd] : []),
320
- buddy,
321
- ...(proactive ? [proactive] : []),
322
- ...(briefCommand ? [briefCommand] : []),
323
- ...(assistantCommand ? [assistantCommand] : []),
324
- ...(bridge ? [bridge] : []),
325
- ...(remoteControlServerCommand ? [remoteControlServerCommand] : []),
326
- ...(voiceCommand ? [voiceCommand] : []),
327
- thinkback,
328
- thinkbackPlay,
329
- permissions,
330
- plan,
331
- privacySettings,
332
- hooks,
333
- exportCommand,
334
- sandboxToggle,
335
- ...(!isUsing3PServices() ? [logout, login()] : []),
336
- passes,
337
- ...(peersCmd ? [peersCmd] : []),
338
- tasks,
339
- ...(workflowsCmd ? [workflowsCmd] : []),
340
- ...(torch ? [torch] : []),
341
- ...(process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO
342
- ? INTERNAL_ONLY_COMMANDS
343
- : []),
344
- ])
345
-
346
- export const builtInCommandNames = memoize(
347
- (): Set<string> =>
348
- new Set(COMMANDS().flatMap(_ => [_.name, ...(_.aliases ?? [])])),
349
- )
350
-
351
- async function getSkills(cwd: string): Promise<{
352
- skillDirCommands: Command[]
353
- pluginSkills: Command[]
354
- bundledSkills: Command[]
355
- builtinPluginSkills: Command[]
356
- }> {
357
- try {
358
- const [skillDirCommands, pluginSkills] = await Promise.all([
359
- getSkillDirCommands(cwd).catch(err => {
360
- logError(toError(err))
361
- logForDebugging(
362
- 'Skill directory commands failed to load, continuing without them',
363
- )
364
- return []
365
- }),
366
- getPluginSkills().catch(err => {
367
- logError(toError(err))
368
- logForDebugging('Plugin skills failed to load, continuing without them')
369
- return []
370
- }),
371
- ])
372
- // Bundled skills are registered synchronously at startup
373
- const bundledSkills = getBundledSkills()
374
- // Built-in plugin skills come from enabled built-in plugins
375
- const builtinPluginSkills = getBuiltinPluginSkillCommands()
376
- logForDebugging(
377
- `getSkills returning: ${skillDirCommands.length} skill dir commands, ${pluginSkills.length} plugin skills, ${bundledSkills.length} bundled skills, ${builtinPluginSkills.length} builtin plugin skills`,
378
- )
379
- return {
380
- skillDirCommands,
381
- pluginSkills,
382
- bundledSkills,
383
- builtinPluginSkills,
384
- }
385
- } catch (err) {
386
- // This should never happen since we catch at the Promise level, but defensive
387
- logError(toError(err))
388
- logForDebugging('Unexpected error in getSkills, returning empty')
389
- return {
390
- skillDirCommands: [],
391
- pluginSkills: [],
392
- bundledSkills: [],
393
- builtinPluginSkills: [],
394
- }
395
- }
396
- }
397
-
398
- /* eslint-disable @typescript-eslint/no-require-imports */
399
- const getWorkflowCommands = feature('WORKFLOW_SCRIPTS')
400
- ? (
401
- require('./tools/WorkflowTool/createWorkflowCommand.js') as typeof import('./tools/WorkflowTool/createWorkflowCommand.js')
402
- ).getWorkflowCommands
403
- : null
404
- /* eslint-enable @typescript-eslint/no-require-imports */
405
-
406
- /**
407
- * Filters commands by their declared `availability` (auth/provider requirement).
408
- * Commands without `availability` are treated as universal.
409
- * This runs before `isEnabled()` so that provider-gated commands are hidden
410
- * regardless of feature-flag state.
411
- *
412
- * Not memoized auth state can change mid-session (e.g. after /login),
413
- * so this must be re-evaluated on every getCommands() call.
414
- */
415
- export function meetsAvailabilityRequirement(cmd: Command): boolean {
416
- if (!cmd.availability) return true
417
- for (const a of cmd.availability) {
418
- switch (a) {
419
- case 'claude-ai':
420
- if (isClaudeAISubscriber()) return true
421
- break
422
- case 'console':
423
- // Console API key user = direct 1P API customer (not 3P, not claude.ai).
424
- // Excludes 3P (Bedrock/Vertex/Foundry) who don't set ANTHROPIC_BASE_URL
425
- // and gateway users who proxy through a custom base URL.
426
- if (
427
- !isClaudeAISubscriber() &&
428
- !isUsing3PServices() &&
429
- isFirstPartyAnthropicBaseUrl()
430
- )
431
- return true
432
- break
433
- default: {
434
- const _exhaustive: never = a
435
- void _exhaustive
436
- break
437
- }
438
- }
439
- }
440
- return false
441
- }
442
-
443
- /**
444
- * Loads all command sources (skills, plugins, workflows). Memoized by cwd
445
- * because loading is expensive (disk I/O, dynamic imports).
446
- */
447
- const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
448
- const [
449
- { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills },
450
- pluginCommands,
451
- workflowCommands,
452
- ] = await Promise.all([
453
- getSkills(cwd),
454
- getPluginCommands(),
455
- getWorkflowCommands ? getWorkflowCommands(cwd) : Promise.resolve([]),
456
- ])
457
-
458
- return [
459
- ...bundledSkills,
460
- ...builtinPluginSkills,
461
- ...skillDirCommands,
462
- ...workflowCommands,
463
- ...pluginCommands,
464
- ...pluginSkills,
465
- ...COMMANDS(),
466
- ]
467
- })
468
-
469
- /**
470
- * Returns commands available to the current user. The expensive loading is
471
- * memoized, but availability and isEnabled checks run fresh every call so
472
- * auth changes (e.g. /login) take effect immediately.
473
- */
474
- export async function getCommands(cwd: string): Promise<Command[]> {
475
- const allCommands = await loadAllCommands(cwd)
476
-
477
- // Get dynamic skills discovered during file operations
478
- const dynamicSkills = getDynamicSkills()
479
-
480
- // Build base commands without dynamic skills
481
- const baseCommands = allCommands.filter(
482
- _ => meetsAvailabilityRequirement(_) && isCommandEnabled(_),
483
- )
484
-
485
- if (dynamicSkills.length === 0) {
486
- return baseCommands
487
- }
488
-
489
- // Dedupe dynamic skills - only add if not already present
490
- const baseCommandNames = new Set(baseCommands.map(c => c.name))
491
- const uniqueDynamicSkills = dynamicSkills.filter(
492
- s =>
493
- !baseCommandNames.has(s.name) &&
494
- meetsAvailabilityRequirement(s) &&
495
- isCommandEnabled(s),
496
- )
497
-
498
- if (uniqueDynamicSkills.length === 0) {
499
- return baseCommands
500
- }
501
-
502
- // Insert dynamic skills after plugin skills but before built-in commands
503
- const builtInNames = new Set(COMMANDS().map(c => c.name))
504
- const insertIndex = baseCommands.findIndex(c => builtInNames.has(c.name))
505
-
506
- if (insertIndex === -1) {
507
- return [...baseCommands, ...uniqueDynamicSkills]
508
- }
509
-
510
- return [
511
- ...baseCommands.slice(0, insertIndex),
512
- ...uniqueDynamicSkills,
513
- ...baseCommands.slice(insertIndex),
514
- ]
515
- }
516
-
517
- /**
518
- * Clears only the memoization caches for commands, WITHOUT clearing skill caches.
519
- * Use this when dynamic skills are added to invalidate cached command lists.
520
- */
521
- export function clearCommandMemoizationCaches(): void {
522
- loadAllCommands.cache?.clear?.()
523
- getSkillToolCommands.cache?.clear?.()
524
- getSlashCommandToolSkills.cache?.clear?.()
525
- // getSkillIndex in skillSearch/localSearch.ts is a separate memoization layer
526
- // built ON TOP of getSkillToolCommands/getCommands. Clearing only the inner
527
- // caches is a no-op for the outer — lodash memoize returns the cached result
528
- // without ever reaching the cleared inners. Must clear it explicitly.
529
- clearSkillIndexCache?.()
530
- }
531
-
532
- export function clearCommandsCache(): void {
533
- clearCommandMemoizationCaches()
534
- clearPluginCommandCache()
535
- clearPluginSkillsCache()
536
- clearSkillCaches()
537
- }
538
-
539
- /**
540
- * Filter AppState.mcp.commands to MCP-provided skills (prompt-type,
541
- * model-invocable, loaded from MCP). These live outside getCommands() so
542
- * callers that need MCP skills in their skill index thread them through
543
- * separately.
544
- */
545
- export function getMcpSkillCommands(
546
- mcpCommands: readonly Command[],
547
- ): readonly Command[] {
548
- if (feature('MCP_SKILLS')) {
549
- return mcpCommands.filter(
550
- cmd =>
551
- cmd.type === 'prompt' &&
552
- cmd.loadedFrom === 'mcp' &&
553
- !cmd.disableModelInvocation,
554
- )
555
- }
556
- return []
557
- }
558
-
559
- // SkillTool shows ALL prompt-based commands that the model can invoke
560
- // This includes both skills (from /skills/) and commands (from /commands/)
561
- export const getSkillToolCommands = memoize(
562
- async (cwd: string): Promise<Command[]> => {
563
- const allCommands = await getCommands(cwd)
564
- return allCommands.filter(
565
- cmd =>
566
- cmd.type === 'prompt' &&
567
- !cmd.disableModelInvocation &&
568
- cmd.source !== 'builtin' &&
569
- // Always include skills from /skills/ dirs, bundled skills, and legacy /commands/ entries
570
- // (they all get an auto-derived description from the first line if frontmatter is missing).
571
- // Plugin/MCP commands still require an explicit description to appear in the listing.
572
- (cmd.loadedFrom === 'bundled' ||
573
- cmd.loadedFrom === 'skills' ||
574
- cmd.loadedFrom === 'commands_DEPRECATED' ||
575
- cmd.hasUserSpecifiedDescription ||
576
- cmd.whenToUse),
577
- )
578
- },
579
- )
580
-
581
- // Filters commands to include only skills. Skills are commands that provide
582
- // specialized capabilities for the model to use. They are identified by
583
- // loadedFrom being 'skills', 'plugin', or 'bundled', or having disableModelInvocation set.
584
- export const getSlashCommandToolSkills = memoize(
585
- async (cwd: string): Promise<Command[]> => {
586
- try {
587
- const allCommands = await getCommands(cwd)
588
- return allCommands.filter(
589
- cmd =>
590
- cmd.type === 'prompt' &&
591
- cmd.source !== 'builtin' &&
592
- (cmd.hasUserSpecifiedDescription || cmd.whenToUse) &&
593
- (cmd.loadedFrom === 'skills' ||
594
- cmd.loadedFrom === 'plugin' ||
595
- cmd.loadedFrom === 'bundled' ||
596
- cmd.disableModelInvocation),
597
- )
598
- } catch (error) {
599
- logError(toError(error))
600
- // Return empty array rather than throwing - skills are non-critical
601
- // This prevents skill loading failures from breaking the entire system
602
- logForDebugging('Returning empty skills array due to load failure')
603
- return []
604
- }
605
- },
606
- )
607
-
608
- /**
609
- * Commands that are safe to use in remote mode (--remote).
610
- * These only affect local TUI state and don't depend on local filesystem,
611
- * git, shell, IDE, MCP, or other local execution context.
612
- *
613
- * Used in two places:
614
- * 1. Pre-filtering commands in main.tsx before REPL renders (prevents race with CCR init)
615
- * 2. Preserving local-only commands in REPL's handleRemoteInit after CCR filters
616
- */
617
- export const REMOTE_SAFE_COMMANDS: Set<Command> = new Set([
618
- session, // Shows QR code / URL for remote session
619
- exit, // Exit the TUI
620
- clear, // Clear screen
621
- help, // Show help
622
- theme, // Change terminal theme
623
- color, // Change agent color
624
- vim, // Toggle vim mode
625
- cost, // Show session cost (local cost tracking)
626
- usage, // Show usage info
627
- copy, // Copy last message
628
- btw, // Quick note
629
- feedback, // Send feedback
630
- plan, // Plan mode toggle
631
- keybindings, // Keybinding management
632
- statusline, // Status line toggle
633
- stickers, // Stickers
634
- mobile, // Mobile QR code
635
- ])
636
-
637
- /**
638
- * Builtin commands of type 'local' that ARE safe to execute when received
639
- * over the Remote Control bridge. These produce text output that streams
640
- * back to the mobile/web client and have no terminal-only side effects.
641
- *
642
- * 'local-jsx' commands are blocked by type (they render Ink UI) and
643
- * 'prompt' commands are allowed by type (they expand to text sent to the
644
- * model) this set only gates 'local' commands.
645
- *
646
- * When adding a new 'local' command that should work from mobile, add it
647
- * here. Default is blocked.
648
- */
649
- export const BRIDGE_SAFE_COMMANDS: Set<Command> = new Set(
650
- [
651
- compact, // Shrink context useful mid-session from a phone
652
- clear, // Wipe transcript
653
- cost, // Show session cost
654
- summary, // Summarize conversation
655
- releaseNotes, // Show changelog
656
- files, // List tracked files
657
- ].filter((c): c is Command => c !== null),
658
- )
659
-
660
- /**
661
- * Whether a slash command is safe to execute when its input arrived over the
662
- * Remote Control bridge (mobile/web client).
663
- *
664
- * PR #19134 blanket-blocked all slash commands from bridge inbound because
665
- * `/model` from iOS was popping the local Ink picker. This predicate relaxes
666
- * that with an explicit allowlist: 'prompt' commands (skills) expand to text
667
- * and are safe by construction; 'local' commands need an explicit opt-in via
668
- * BRIDGE_SAFE_COMMANDS; 'local-jsx' commands render Ink UI and stay blocked.
669
- */
670
- export function isBridgeSafeCommand(cmd: Command): boolean {
671
- if (cmd.type === 'local-jsx') return false
672
- if (cmd.type === 'prompt') return true
673
- return BRIDGE_SAFE_COMMANDS.has(cmd)
674
- }
675
-
676
- /**
677
- * Filter commands to only include those safe for remote mode.
678
- * Used to pre-filter commands when rendering the REPL in --remote mode,
679
- * preventing local-only commands from being briefly available before
680
- * the CCR init message arrives.
681
- */
682
- export function filterCommandsForRemoteMode(commands: Command[]): Command[] {
683
- return commands.filter(cmd => REMOTE_SAFE_COMMANDS.has(cmd))
684
- }
685
-
686
- export function findCommand(
687
- commandName: string,
688
- commands: Command[],
689
- ): Command | undefined {
690
- return commands.find(
691
- _ =>
692
- _.name === commandName ||
693
- getCommandName(_) === commandName ||
694
- _.aliases?.includes(commandName),
695
- )
696
- }
697
-
698
- export function hasCommand(commandName: string, commands: Command[]): boolean {
699
- return findCommand(commandName, commands) !== undefined
700
- }
701
-
702
- export function getCommand(commandName: string, commands: Command[]): Command {
703
- const command = findCommand(commandName, commands)
704
- if (!command) {
705
- throw ReferenceError(
706
- `Command ${commandName} not found. Available commands: ${commands
707
- .map(_ => {
708
- const name = getCommandName(_)
709
- return _.aliases ? `${name} (aliases: ${_.aliases.join(', ')})` : name
710
- })
711
- .sort((a, b) => a.localeCompare(b))
712
- .join(', ')}`,
713
- )
714
- }
715
-
716
- return command
717
- }
718
-
719
- /**
720
- * Formats a command's description with its source annotation for user-facing UI.
721
- * Use this in typeahead, help screens, and other places where users need to see
722
- * where a command comes from.
723
- *
724
- * For model-facing prompts (like SkillTool), use cmd.description directly.
725
- */
726
- export function formatDescriptionWithSource(cmd: Command): string {
727
- if (cmd.type !== 'prompt') {
728
- return cmd.description
729
- }
730
-
731
- if (cmd.kind === 'workflow') {
732
- return `${cmd.description} (workflow)`
733
- }
734
-
735
- if (cmd.source === 'plugin') {
736
- const pluginName = cmd.pluginInfo?.pluginManifest.name
737
- if (pluginName) {
738
- return `(${pluginName}) ${cmd.description}`
739
- }
740
- return `${cmd.description} (plugin)`
741
- }
742
-
743
- if (cmd.source === 'builtin' || cmd.source === 'mcp') {
744
- return cmd.description
745
- }
746
-
747
- if (cmd.source === 'bundled') {
748
- return `${cmd.description} (bundled)`
749
- }
750
-
751
- return `${cmd.description} (${getSettingSourceName(cmd.source)})`
752
- }
1
+ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
2
+ import addDir from './commands/add-dir/index.js'
3
+ import autofixPr from './commands/autofix-pr/index.js'
4
+ import backfillSessions from './commands/backfill-sessions/index.js'
5
+ import btw from './commands/btw/index.js'
6
+ import goodClaude from './commands/good-claude/index.js'
7
+ import issue from './commands/issue/index.js'
8
+ import feedback from './commands/feedback/index.js'
9
+ import clear from './commands/clear/index.js'
10
+ import color from './commands/color/index.js'
11
+ import commit from './commands/commit.js'
12
+ import copy from './commands/copy/index.js'
13
+ import desktop from './commands/desktop/index.js'
14
+ import commitPushPr from './commands/commit-push-pr.js'
15
+ import compact from './commands/compact/index.js'
16
+ import config from './commands/config/index.js'
17
+ import { context, contextNonInteractive } from './commands/context/index.js'
18
+ import cost from './commands/cost/index.js'
19
+ import diff from './commands/diff/index.js'
20
+ import ctx_viz from './commands/ctx_viz/index.js'
21
+ import doctor from './commands/doctor/index.js'
22
+ import memory from './commands/memory/index.js'
23
+ import help from './commands/help/index.js'
24
+ import ide from './commands/ide/index.js'
25
+ import init from './commands/init.js'
26
+ import initVerifiers from './commands/init-verifiers.js'
27
+ import keybindings from './commands/keybindings/index.js'
28
+ import login from './commands/login/index.js'
29
+ import logout from './commands/logout/index.js'
30
+ import installGitHubApp from './commands/install-github-app/index.js'
31
+ import installSlackApp from './commands/install-slack-app/index.js'
32
+ import breakCache from './commands/break-cache/index.js'
33
+ import mcp from './commands/mcp/index.js'
34
+ import mobile from './commands/mobile/index.js'
35
+ import onboarding from './commands/onboarding/index.js'
36
+ import pr_comments from './commands/pr_comments/index.js'
37
+ import releaseNotes from './commands/release-notes/index.js'
38
+ import rename from './commands/rename/index.js'
39
+ import resume from './commands/resume/index.js'
40
+ import review, { ultrareview } from './commands/review.js'
41
+ import session from './commands/session/index.js'
42
+ import share from './commands/share/index.js'
43
+ import skills from './commands/skills/index.js'
44
+ import status from './commands/status/index.js'
45
+ import tasks from './commands/tasks/index.js'
46
+ import teleport from './commands/teleport/index.js'
47
+ /* eslint-disable @typescript-eslint/no-require-imports */
48
+ const agentsPlatform =
49
+ process.env.USER_TYPE === 'ant'
50
+ ? require('./commands/agents-platform/index.js').default
51
+ : null
52
+ /* eslint-enable @typescript-eslint/no-require-imports */
53
+ import securityReview from './commands/security-review.js'
54
+ import bughunter from './commands/bughunter/index.js'
55
+ import terminalSetup from './commands/terminalSetup/index.js'
56
+ import usage from './commands/usage/index.js'
57
+ import theme from './commands/theme/index.js'
58
+ import vim from './commands/vim/index.js'
59
+ import { feature } from 'bun:bundle'
60
+ // Dead code elimination: conditional imports
61
+ /* eslint-disable @typescript-eslint/no-require-imports */
62
+ const proactive =
63
+ feature('PROACTIVE') || feature('KAIROS')
64
+ ? require('./commands/proactive.js').default
65
+ : null
66
+ const briefCommand =
67
+ feature('KAIROS') || feature('KAIROS_BRIEF')
68
+ ? require('./commands/brief.js').default
69
+ : null
70
+ const assistantCommand = feature('KAIROS')
71
+ ? require('./commands/assistant/index.js').default
72
+ : null
73
+ const bridge = feature('BRIDGE_MODE')
74
+ ? require('./commands/bridge/index.js').default
75
+ : null
76
+ const remoteControlServerCommand =
77
+ feature('DAEMON') && feature('BRIDGE_MODE')
78
+ ? require('./commands/remoteControlServer/index.js').default
79
+ : null
80
+ const voiceCommand = feature('VOICE_MODE')
81
+ ? require('./commands/voice/index.js').default
82
+ : null
83
+ const forceSnip = feature('HISTORY_SNIP')
84
+ ? require('./commands/force-snip.js').default
85
+ : null
86
+ const workflowsCmd = feature('WORKFLOW_SCRIPTS')
87
+ ? (
88
+ require('./commands/workflows/index.js') as typeof import('./commands/workflows/index.js')
89
+ ).default
90
+ : null
91
+ const webCmd = feature('CCR_REMOTE_SETUP')
92
+ ? (
93
+ require('./commands/remote-setup/index.js') as typeof import('./commands/remote-setup/index.js')
94
+ ).default
95
+ : null
96
+ const clearSkillIndexCache = feature('EXPERIMENTAL_SKILL_SEARCH')
97
+ ? (
98
+ require('./services/skillSearch/localSearch.js') as typeof import('./services/skillSearch/localSearch.js')
99
+ ).clearSkillIndexCache
100
+ : null
101
+ const subscribePr = feature('KAIROS_GITHUB_WEBHOOKS')
102
+ ? require('./commands/subscribe-pr.js').default
103
+ : null
104
+ const ultraplan = feature('ULTRAPLAN')
105
+ ? require('./commands/ultraplan.js').default
106
+ : null
107
+ const torch = feature('TORCH') ? require('./commands/torch.js').default : null
108
+ const peersCmd = feature('UDS_INBOX')
109
+ ? (
110
+ require('./commands/peers/index.js') as typeof import('./commands/peers/index.js')
111
+ ).default
112
+ : null
113
+ const forkCmd = feature('FORK_SUBAGENT')
114
+ ? (
115
+ require('./commands/fork/index.js') as typeof import('./commands/fork/index.js')
116
+ ).default
117
+ : null
118
+ const buddy = (
119
+ require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js')
120
+ ).default
121
+ /* eslint-enable @typescript-eslint/no-require-imports */
122
+ import thinkback from './commands/thinkback/index.js'
123
+ import thinkbackPlay from './commands/thinkback-play/index.js'
124
+ import permissions from './commands/permissions/index.js'
125
+ import plan from './commands/plan/index.js'
126
+ import fast from './commands/fast/index.js'
127
+ import exec from './commands/exec/index.js'
128
+ import passes from './commands/passes/index.js'
129
+ import privacySettings from './commands/privacy-settings/index.js'
130
+ import hooks from './commands/hooks/index.js'
131
+ import files from './commands/files/index.js'
132
+ import branch from './commands/branch/index.js'
133
+ import agents from './commands/agents/index.js'
134
+ import plugin from './commands/plugin/index.js'
135
+ import reloadPlugins from './commands/reload-plugins/index.js'
136
+ import rewind from './commands/rewind/index.js'
137
+ import heapDump from './commands/heapdump/index.js'
138
+ import mockLimits from './commands/mock-limits/index.js'
139
+ import bridgeKick from './commands/bridge-kick.js'
140
+ import version from './commands/version.js'
141
+ import summary from './commands/summary/index.js'
142
+ import {
143
+ resetLimits,
144
+ resetLimitsNonInteractive,
145
+ } from './commands/reset-limits/index.js'
146
+ import antTrace from './commands/ant-trace/index.js'
147
+ import perfIssue from './commands/perf-issue/index.js'
148
+ import sandboxToggle from './commands/sandbox-toggle/index.js'
149
+ import chrome from './commands/chrome/index.js'
150
+ import stickers from './commands/stickers/index.js'
151
+ import advisor from './commands/advisor.js'
152
+ import { logError } from './utils/log.js'
153
+ import { toError } from './utils/errors.js'
154
+ import { logForDebugging } from './utils/debug.js'
155
+ import {
156
+ getSkillDirCommands,
157
+ clearSkillCaches,
158
+ getDynamicSkills,
159
+ } from './skills/loadSkillsDir.js'
160
+ import { getBundledSkills } from './skills/bundledSkills.js'
161
+ import { getBuiltinPluginSkillCommands } from './plugins/builtinPlugins.js'
162
+ import {
163
+ getPluginCommands,
164
+ clearPluginCommandCache,
165
+ getPluginSkills,
166
+ clearPluginSkillsCache,
167
+ } from './utils/plugins/loadPluginCommands.js'
168
+ import memoize from 'lodash-es/memoize.js'
169
+ import { isUsing3PServices, isClaudeAISubscriber } from './utils/auth.js'
170
+ import { isFirstPartyAnthropicBaseUrl } from './utils/model/providers.js'
171
+ import env from './commands/env/index.js'
172
+ import exit from './commands/exit/index.js'
173
+ import exportCommand from './commands/export/index.js'
174
+ import model from './commands/model/index.js'
175
+ import tag from './commands/tag/index.js'
176
+ import outputStyle from './commands/output-style/index.js'
177
+ import remoteEnv from './commands/remote-env/index.js'
178
+ import upgrade from './commands/upgrade/index.js'
179
+ import {
180
+ extraUsage,
181
+ extraUsageNonInteractive,
182
+ } from './commands/extra-usage/index.js'
183
+ import rateLimitOptions from './commands/rate-limit-options/index.js'
184
+ import statusline from './commands/statusline.js'
185
+ import effort from './commands/effort/index.js'
186
+ import stats from './commands/stats/index.js'
187
+ // insights.ts is 113KB (3200 lines, includes diffLines/html rendering). Lazy
188
+ // shim defers the heavy module until /insights is actually invoked.
189
+ const usageReport: Command = {
190
+ type: 'prompt',
191
+ name: 'insights',
192
+ description: 'Generate a report analyzing your Claude Code sessions',
193
+ contentLength: 0,
194
+ progressMessage: 'analyzing your sessions',
195
+ source: 'builtin',
196
+ async getPromptForCommand(args, context) {
197
+ const real = (await import('./commands/insights.js')).default
198
+ if (real.type !== 'prompt') throw new Error('unreachable')
199
+ return real.getPromptForCommand(args, context)
200
+ },
201
+ }
202
+ import oauthRefresh from './commands/oauth-refresh/index.js'
203
+ import debugToolCall from './commands/debug-tool-call/index.js'
204
+ import { getSettingSourceName } from './utils/settings/constants.js'
205
+ import {
206
+ type Command,
207
+ getCommandName,
208
+ isCommandEnabled,
209
+ } from './types/command.js'
210
+
211
+ // Re-export types from the centralized location
212
+ export type {
213
+ Command,
214
+ CommandBase,
215
+ CommandResultDisplay,
216
+ LocalCommandResult,
217
+ LocalJSXCommandContext,
218
+ PromptCommand,
219
+ ResumeEntrypoint,
220
+ } from './types/command.js'
221
+ export { getCommandName, isCommandEnabled } from './types/command.js'
222
+
223
+ // Commands that get eliminated from the external build
224
+ export const INTERNAL_ONLY_COMMANDS = [
225
+ backfillSessions,
226
+ breakCache,
227
+ bughunter,
228
+ commit,
229
+ commitPushPr,
230
+ ctx_viz,
231
+ goodClaude,
232
+ issue,
233
+ initVerifiers,
234
+ ...(forceSnip ? [forceSnip] : []),
235
+ mockLimits,
236
+ bridgeKick,
237
+ version,
238
+ ...(ultraplan ? [ultraplan] : []),
239
+ ...(subscribePr ? [subscribePr] : []),
240
+ resetLimits,
241
+ resetLimitsNonInteractive,
242
+ onboarding,
243
+ share,
244
+ summary,
245
+ teleport,
246
+ antTrace,
247
+ perfIssue,
248
+ env,
249
+ oauthRefresh,
250
+ debugToolCall,
251
+ agentsPlatform,
252
+ autofixPr,
253
+ ].filter(Boolean)
254
+
255
+ // Declared as a function so that we don't run this until getCommands is called,
256
+ // since underlying functions read from config, which can't be read at module initialization time
257
+ const COMMANDS = memoize((): Command[] => [
258
+ addDir,
259
+ advisor,
260
+ agents,
261
+ branch,
262
+ btw,
263
+ chrome,
264
+ clear,
265
+ color,
266
+ compact,
267
+ config,
268
+ copy,
269
+ desktop,
270
+ context,
271
+ contextNonInteractive,
272
+ cost,
273
+ diff,
274
+ doctor,
275
+ effort,
276
+ exit,
277
+ fast,
278
+ exec,
279
+ files,
280
+ heapDump,
281
+ help,
282
+ ide,
283
+ init,
284
+ keybindings,
285
+ installGitHubApp,
286
+ installSlackApp,
287
+ mcp,
288
+ memory,
289
+ mobile,
290
+ model,
291
+ outputStyle,
292
+ remoteEnv,
293
+ plugin,
294
+ pr_comments,
295
+ releaseNotes,
296
+ reloadPlugins,
297
+ rename,
298
+ resume,
299
+ session,
300
+ skills,
301
+ stats,
302
+ status,
303
+ statusline,
304
+ stickers,
305
+ tag,
306
+ theme,
307
+ feedback,
308
+ review,
309
+ ultrareview,
310
+ rewind,
311
+ securityReview,
312
+ terminalSetup,
313
+ upgrade,
314
+ extraUsage,
315
+ extraUsageNonInteractive,
316
+ rateLimitOptions,
317
+ usage,
318
+ usageReport,
319
+ vim,
320
+ ...(webCmd ? [webCmd] : []),
321
+ ...(forkCmd ? [forkCmd] : []),
322
+ buddy,
323
+ ...(proactive ? [proactive] : []),
324
+ ...(briefCommand ? [briefCommand] : []),
325
+ ...(assistantCommand ? [assistantCommand] : []),
326
+ ...(bridge ? [bridge] : []),
327
+ ...(remoteControlServerCommand ? [remoteControlServerCommand] : []),
328
+ ...(voiceCommand ? [voiceCommand] : []),
329
+ thinkback,
330
+ thinkbackPlay,
331
+ permissions,
332
+ plan,
333
+ privacySettings,
334
+ hooks,
335
+ exportCommand,
336
+ sandboxToggle,
337
+ ...(!isUsing3PServices() ? [logout, login()] : []),
338
+ passes,
339
+ ...(peersCmd ? [peersCmd] : []),
340
+ tasks,
341
+ ...(workflowsCmd ? [workflowsCmd] : []),
342
+ ...(torch ? [torch] : []),
343
+ ...(process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO
344
+ ? INTERNAL_ONLY_COMMANDS
345
+ : []),
346
+ ])
347
+
348
+ export const builtInCommandNames = memoize(
349
+ (): Set<string> =>
350
+ new Set(COMMANDS().flatMap(_ => [_.name, ...(_.aliases ?? [])])),
351
+ )
352
+
353
+ async function getSkills(cwd: string): Promise<{
354
+ skillDirCommands: Command[]
355
+ pluginSkills: Command[]
356
+ bundledSkills: Command[]
357
+ builtinPluginSkills: Command[]
358
+ }> {
359
+ try {
360
+ const [skillDirCommands, pluginSkills] = await Promise.all([
361
+ getSkillDirCommands(cwd).catch(err => {
362
+ logError(toError(err))
363
+ logForDebugging(
364
+ 'Skill directory commands failed to load, continuing without them',
365
+ )
366
+ return []
367
+ }),
368
+ getPluginSkills().catch(err => {
369
+ logError(toError(err))
370
+ logForDebugging('Plugin skills failed to load, continuing without them')
371
+ return []
372
+ }),
373
+ ])
374
+ // Bundled skills are registered synchronously at startup
375
+ const bundledSkills = getBundledSkills()
376
+ // Built-in plugin skills come from enabled built-in plugins
377
+ const builtinPluginSkills = getBuiltinPluginSkillCommands()
378
+ logForDebugging(
379
+ `getSkills returning: ${skillDirCommands.length} skill dir commands, ${pluginSkills.length} plugin skills, ${bundledSkills.length} bundled skills, ${builtinPluginSkills.length} builtin plugin skills`,
380
+ )
381
+ return {
382
+ skillDirCommands,
383
+ pluginSkills,
384
+ bundledSkills,
385
+ builtinPluginSkills,
386
+ }
387
+ } catch (err) {
388
+ // This should never happen since we catch at the Promise level, but defensive
389
+ logError(toError(err))
390
+ logForDebugging('Unexpected error in getSkills, returning empty')
391
+ return {
392
+ skillDirCommands: [],
393
+ pluginSkills: [],
394
+ bundledSkills: [],
395
+ builtinPluginSkills: [],
396
+ }
397
+ }
398
+ }
399
+
400
+ /* eslint-disable @typescript-eslint/no-require-imports */
401
+ const getWorkflowCommands = feature('WORKFLOW_SCRIPTS')
402
+ ? (
403
+ require('./tools/WorkflowTool/createWorkflowCommand.js') as typeof import('./tools/WorkflowTool/createWorkflowCommand.js')
404
+ ).getWorkflowCommands
405
+ : null
406
+ /* eslint-enable @typescript-eslint/no-require-imports */
407
+
408
+ /**
409
+ * Filters commands by their declared `availability` (auth/provider requirement).
410
+ * Commands without `availability` are treated as universal.
411
+ * This runs before `isEnabled()` so that provider-gated commands are hidden
412
+ * regardless of feature-flag state.
413
+ *
414
+ * Not memoized — auth state can change mid-session (e.g. after /login),
415
+ * so this must be re-evaluated on every getCommands() call.
416
+ */
417
+ export function meetsAvailabilityRequirement(cmd: Command): boolean {
418
+ if (!cmd.availability) return true
419
+ for (const a of cmd.availability) {
420
+ switch (a) {
421
+ case 'claude-ai':
422
+ if (isClaudeAISubscriber()) return true
423
+ break
424
+ case 'console':
425
+ // Console API key user = direct 1P API customer (not 3P, not claude.ai).
426
+ // Excludes 3P (Bedrock/Vertex/Foundry) who don't set ANTHROPIC_BASE_URL
427
+ // and gateway users who proxy through a custom base URL.
428
+ if (
429
+ !isClaudeAISubscriber() &&
430
+ !isUsing3PServices() &&
431
+ isFirstPartyAnthropicBaseUrl()
432
+ )
433
+ return true
434
+ break
435
+ default: {
436
+ const _exhaustive: never = a
437
+ void _exhaustive
438
+ break
439
+ }
440
+ }
441
+ }
442
+ return false
443
+ }
444
+
445
+ /**
446
+ * Loads all command sources (skills, plugins, workflows). Memoized by cwd
447
+ * because loading is expensive (disk I/O, dynamic imports).
448
+ */
449
+ const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
450
+ const [
451
+ { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills },
452
+ pluginCommands,
453
+ workflowCommands,
454
+ ] = await Promise.all([
455
+ getSkills(cwd),
456
+ getPluginCommands(),
457
+ getWorkflowCommands ? getWorkflowCommands(cwd) : Promise.resolve([]),
458
+ ])
459
+
460
+ return [
461
+ ...bundledSkills,
462
+ ...builtinPluginSkills,
463
+ ...skillDirCommands,
464
+ ...workflowCommands,
465
+ ...pluginCommands,
466
+ ...pluginSkills,
467
+ ...COMMANDS(),
468
+ ]
469
+ })
470
+
471
+ /**
472
+ * Returns commands available to the current user. The expensive loading is
473
+ * memoized, but availability and isEnabled checks run fresh every call so
474
+ * auth changes (e.g. /login) take effect immediately.
475
+ */
476
+ export async function getCommands(cwd: string): Promise<Command[]> {
477
+ const allCommands = await loadAllCommands(cwd)
478
+
479
+ // Get dynamic skills discovered during file operations
480
+ const dynamicSkills = getDynamicSkills()
481
+
482
+ // Build base commands without dynamic skills
483
+ const baseCommands = allCommands.filter(
484
+ _ => meetsAvailabilityRequirement(_) && isCommandEnabled(_),
485
+ )
486
+
487
+ if (dynamicSkills.length === 0) {
488
+ return baseCommands
489
+ }
490
+
491
+ // Dedupe dynamic skills - only add if not already present
492
+ const baseCommandNames = new Set(baseCommands.map(c => c.name))
493
+ const uniqueDynamicSkills = dynamicSkills.filter(
494
+ s =>
495
+ !baseCommandNames.has(s.name) &&
496
+ meetsAvailabilityRequirement(s) &&
497
+ isCommandEnabled(s),
498
+ )
499
+
500
+ if (uniqueDynamicSkills.length === 0) {
501
+ return baseCommands
502
+ }
503
+
504
+ // Insert dynamic skills after plugin skills but before built-in commands
505
+ const builtInNames = new Set(COMMANDS().map(c => c.name))
506
+ const insertIndex = baseCommands.findIndex(c => builtInNames.has(c.name))
507
+
508
+ if (insertIndex === -1) {
509
+ return [...baseCommands, ...uniqueDynamicSkills]
510
+ }
511
+
512
+ return [
513
+ ...baseCommands.slice(0, insertIndex),
514
+ ...uniqueDynamicSkills,
515
+ ...baseCommands.slice(insertIndex),
516
+ ]
517
+ }
518
+
519
+ /**
520
+ * Clears only the memoization caches for commands, WITHOUT clearing skill caches.
521
+ * Use this when dynamic skills are added to invalidate cached command lists.
522
+ */
523
+ export function clearCommandMemoizationCaches(): void {
524
+ loadAllCommands.cache?.clear?.()
525
+ getSkillToolCommands.cache?.clear?.()
526
+ getSlashCommandToolSkills.cache?.clear?.()
527
+ // getSkillIndex in skillSearch/localSearch.ts is a separate memoization layer
528
+ // built ON TOP of getSkillToolCommands/getCommands. Clearing only the inner
529
+ // caches is a no-op for the outer — lodash memoize returns the cached result
530
+ // without ever reaching the cleared inners. Must clear it explicitly.
531
+ clearSkillIndexCache?.()
532
+ }
533
+
534
+ export function clearCommandsCache(): void {
535
+ clearCommandMemoizationCaches()
536
+ clearPluginCommandCache()
537
+ clearPluginSkillsCache()
538
+ clearSkillCaches()
539
+ }
540
+
541
+ /**
542
+ * Filter AppState.mcp.commands to MCP-provided skills (prompt-type,
543
+ * model-invocable, loaded from MCP). These live outside getCommands() so
544
+ * callers that need MCP skills in their skill index thread them through
545
+ * separately.
546
+ */
547
+ export function getMcpSkillCommands(
548
+ mcpCommands: readonly Command[],
549
+ ): readonly Command[] {
550
+ if (feature('MCP_SKILLS')) {
551
+ return mcpCommands.filter(
552
+ cmd =>
553
+ cmd.type === 'prompt' &&
554
+ cmd.loadedFrom === 'mcp' &&
555
+ !cmd.disableModelInvocation,
556
+ )
557
+ }
558
+ return []
559
+ }
560
+
561
+ // SkillTool shows ALL prompt-based commands that the model can invoke
562
+ // This includes both skills (from /skills/) and commands (from /commands/)
563
+ export const getSkillToolCommands = memoize(
564
+ async (cwd: string): Promise<Command[]> => {
565
+ const allCommands = await getCommands(cwd)
566
+ return allCommands.filter(
567
+ cmd =>
568
+ cmd.type === 'prompt' &&
569
+ !cmd.disableModelInvocation &&
570
+ cmd.source !== 'builtin' &&
571
+ // Always include skills from /skills/ dirs, bundled skills, and legacy /commands/ entries
572
+ // (they all get an auto-derived description from the first line if frontmatter is missing).
573
+ // Plugin/MCP commands still require an explicit description to appear in the listing.
574
+ (cmd.loadedFrom === 'bundled' ||
575
+ cmd.loadedFrom === 'skills' ||
576
+ cmd.loadedFrom === 'commands_DEPRECATED' ||
577
+ cmd.hasUserSpecifiedDescription ||
578
+ cmd.whenToUse),
579
+ )
580
+ },
581
+ )
582
+
583
+ // Filters commands to include only skills. Skills are commands that provide
584
+ // specialized capabilities for the model to use. They are identified by
585
+ // loadedFrom being 'skills', 'plugin', or 'bundled', or having disableModelInvocation set.
586
+ export const getSlashCommandToolSkills = memoize(
587
+ async (cwd: string): Promise<Command[]> => {
588
+ try {
589
+ const allCommands = await getCommands(cwd)
590
+ return allCommands.filter(
591
+ cmd =>
592
+ cmd.type === 'prompt' &&
593
+ cmd.source !== 'builtin' &&
594
+ (cmd.hasUserSpecifiedDescription || cmd.whenToUse) &&
595
+ (cmd.loadedFrom === 'skills' ||
596
+ cmd.loadedFrom === 'plugin' ||
597
+ cmd.loadedFrom === 'bundled' ||
598
+ cmd.disableModelInvocation),
599
+ )
600
+ } catch (error) {
601
+ logError(toError(error))
602
+ // Return empty array rather than throwing - skills are non-critical
603
+ // This prevents skill loading failures from breaking the entire system
604
+ logForDebugging('Returning empty skills array due to load failure')
605
+ return []
606
+ }
607
+ },
608
+ )
609
+
610
+ /**
611
+ * Commands that are safe to use in remote mode (--remote).
612
+ * These only affect local TUI state and don't depend on local filesystem,
613
+ * git, shell, IDE, MCP, or other local execution context.
614
+ *
615
+ * Used in two places:
616
+ * 1. Pre-filtering commands in main.tsx before REPL renders (prevents race with CCR init)
617
+ * 2. Preserving local-only commands in REPL's handleRemoteInit after CCR filters
618
+ */
619
+ export const REMOTE_SAFE_COMMANDS: Set<Command> = new Set([
620
+ session, // Shows QR code / URL for remote session
621
+ exit, // Exit the TUI
622
+ clear, // Clear screen
623
+ help, // Show help
624
+ theme, // Change terminal theme
625
+ color, // Change agent color
626
+ vim, // Toggle vim mode
627
+ cost, // Show session cost (local cost tracking)
628
+ usage, // Show usage info
629
+ copy, // Copy last message
630
+ btw, // Quick note
631
+ feedback, // Send feedback
632
+ plan, // Plan mode toggle
633
+ keybindings, // Keybinding management
634
+ statusline, // Status line toggle
635
+ stickers, // Stickers
636
+ mobile, // Mobile QR code
637
+ ])
638
+
639
+ /**
640
+ * Builtin commands of type 'local' that ARE safe to execute when received
641
+ * over the Remote Control bridge. These produce text output that streams
642
+ * back to the mobile/web client and have no terminal-only side effects.
643
+ *
644
+ * 'local-jsx' commands are blocked by type (they render Ink UI) and
645
+ * 'prompt' commands are allowed by type (they expand to text sent to the
646
+ * model) this set only gates 'local' commands.
647
+ *
648
+ * When adding a new 'local' command that should work from mobile, add it
649
+ * here. Default is blocked.
650
+ */
651
+ export const BRIDGE_SAFE_COMMANDS: Set<Command> = new Set(
652
+ [
653
+ compact, // Shrink context — useful mid-session from a phone
654
+ clear, // Wipe transcript
655
+ cost, // Show session cost
656
+ summary, // Summarize conversation
657
+ releaseNotes, // Show changelog
658
+ files, // List tracked files
659
+ ].filter((c): c is Command => c !== null),
660
+ )
661
+
662
+ /**
663
+ * Whether a slash command is safe to execute when its input arrived over the
664
+ * Remote Control bridge (mobile/web client).
665
+ *
666
+ * PR #19134 blanket-blocked all slash commands from bridge inbound because
667
+ * `/model` from iOS was popping the local Ink picker. This predicate relaxes
668
+ * that with an explicit allowlist: 'prompt' commands (skills) expand to text
669
+ * and are safe by construction; 'local' commands need an explicit opt-in via
670
+ * BRIDGE_SAFE_COMMANDS; 'local-jsx' commands render Ink UI and stay blocked.
671
+ */
672
+ export function isBridgeSafeCommand(cmd: Command): boolean {
673
+ if (cmd.type === 'local-jsx') return false
674
+ if (cmd.type === 'prompt') return true
675
+ return BRIDGE_SAFE_COMMANDS.has(cmd)
676
+ }
677
+
678
+ /**
679
+ * Filter commands to only include those safe for remote mode.
680
+ * Used to pre-filter commands when rendering the REPL in --remote mode,
681
+ * preventing local-only commands from being briefly available before
682
+ * the CCR init message arrives.
683
+ */
684
+ export function filterCommandsForRemoteMode(commands: Command[]): Command[] {
685
+ return commands.filter(cmd => REMOTE_SAFE_COMMANDS.has(cmd))
686
+ }
687
+
688
+ export function findCommand(
689
+ commandName: string,
690
+ commands: Command[],
691
+ ): Command | undefined {
692
+ return commands.find(
693
+ _ =>
694
+ _.name === commandName ||
695
+ getCommandName(_) === commandName ||
696
+ _.aliases?.includes(commandName),
697
+ )
698
+ }
699
+
700
+ export function hasCommand(commandName: string, commands: Command[]): boolean {
701
+ return findCommand(commandName, commands) !== undefined
702
+ }
703
+
704
+ export function getCommand(commandName: string, commands: Command[]): Command {
705
+ const command = findCommand(commandName, commands)
706
+ if (!command) {
707
+ throw ReferenceError(
708
+ `Command ${commandName} not found. Available commands: ${commands
709
+ .map(_ => {
710
+ const name = getCommandName(_)
711
+ return _.aliases ? `${name} (aliases: ${_.aliases.join(', ')})` : name
712
+ })
713
+ .sort((a, b) => a.localeCompare(b))
714
+ .join(', ')}`,
715
+ )
716
+ }
717
+
718
+ return command
719
+ }
720
+
721
+ /**
722
+ * Formats a command's description with its source annotation for user-facing UI.
723
+ * Use this in typeahead, help screens, and other places where users need to see
724
+ * where a command comes from.
725
+ *
726
+ * For model-facing prompts (like SkillTool), use cmd.description directly.
727
+ */
728
+ export function formatDescriptionWithSource(cmd: Command): string {
729
+ if (cmd.type !== 'prompt') {
730
+ return cmd.description
731
+ }
732
+
733
+ if (cmd.kind === 'workflow') {
734
+ return `${cmd.description} (workflow)`
735
+ }
736
+
737
+ if (cmd.source === 'plugin') {
738
+ const pluginName = cmd.pluginInfo?.pluginManifest.name
739
+ if (pluginName) {
740
+ return `(${pluginName}) ${cmd.description}`
741
+ }
742
+ return `${cmd.description} (plugin)`
743
+ }
744
+
745
+ if (cmd.source === 'builtin' || cmd.source === 'mcp') {
746
+ return cmd.description
747
+ }
748
+
749
+ if (cmd.source === 'bundled') {
750
+ return `${cmd.description} (bundled)`
751
+ }
752
+
753
+ return `${cmd.description} (${getSettingSourceName(cmd.source)})`
754
+ }