source dump of claude code
at main 754 lines 25 kB view raw
1// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered 2import addDir from './commands/add-dir/index.js' 3import autofixPr from './commands/autofix-pr/index.js' 4import backfillSessions from './commands/backfill-sessions/index.js' 5import btw from './commands/btw/index.js' 6import goodClaude from './commands/good-claude/index.js' 7import issue from './commands/issue/index.js' 8import feedback from './commands/feedback/index.js' 9import clear from './commands/clear/index.js' 10import color from './commands/color/index.js' 11import commit from './commands/commit.js' 12import copy from './commands/copy/index.js' 13import desktop from './commands/desktop/index.js' 14import commitPushPr from './commands/commit-push-pr.js' 15import compact from './commands/compact/index.js' 16import config from './commands/config/index.js' 17import { context, contextNonInteractive } from './commands/context/index.js' 18import cost from './commands/cost/index.js' 19import diff from './commands/diff/index.js' 20import ctx_viz from './commands/ctx_viz/index.js' 21import doctor from './commands/doctor/index.js' 22import memory from './commands/memory/index.js' 23import help from './commands/help/index.js' 24import ide from './commands/ide/index.js' 25import init from './commands/init.js' 26import initVerifiers from './commands/init-verifiers.js' 27import keybindings from './commands/keybindings/index.js' 28import login from './commands/login/index.js' 29import logout from './commands/logout/index.js' 30import installGitHubApp from './commands/install-github-app/index.js' 31import installSlackApp from './commands/install-slack-app/index.js' 32import breakCache from './commands/break-cache/index.js' 33import mcp from './commands/mcp/index.js' 34import mobile from './commands/mobile/index.js' 35import onboarding from './commands/onboarding/index.js' 36import pr_comments from './commands/pr_comments/index.js' 37import releaseNotes from './commands/release-notes/index.js' 38import rename from './commands/rename/index.js' 39import resume from './commands/resume/index.js' 40import review, { ultrareview } from './commands/review.js' 41import session from './commands/session/index.js' 42import share from './commands/share/index.js' 43import skills from './commands/skills/index.js' 44import status from './commands/status/index.js' 45import tasks from './commands/tasks/index.js' 46import teleport from './commands/teleport/index.js' 47/* eslint-disable @typescript-eslint/no-require-imports */ 48const 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 */ 53import securityReview from './commands/security-review.js' 54import bughunter from './commands/bughunter/index.js' 55import terminalSetup from './commands/terminalSetup/index.js' 56import usage from './commands/usage/index.js' 57import theme from './commands/theme/index.js' 58import vim from './commands/vim/index.js' 59import { feature } from 'bun:bundle' 60// Dead code elimination: conditional imports 61/* eslint-disable @typescript-eslint/no-require-imports */ 62const proactive = 63 feature('PROACTIVE') || feature('KAIROS') 64 ? require('./commands/proactive.js').default 65 : null 66const briefCommand = 67 feature('KAIROS') || feature('KAIROS_BRIEF') 68 ? require('./commands/brief.js').default 69 : null 70const assistantCommand = feature('KAIROS') 71 ? require('./commands/assistant/index.js').default 72 : null 73const bridge = feature('BRIDGE_MODE') 74 ? require('./commands/bridge/index.js').default 75 : null 76const remoteControlServerCommand = 77 feature('DAEMON') && feature('BRIDGE_MODE') 78 ? require('./commands/remoteControlServer/index.js').default 79 : null 80const voiceCommand = feature('VOICE_MODE') 81 ? require('./commands/voice/index.js').default 82 : null 83const forceSnip = feature('HISTORY_SNIP') 84 ? require('./commands/force-snip.js').default 85 : null 86const workflowsCmd = feature('WORKFLOW_SCRIPTS') 87 ? ( 88 require('./commands/workflows/index.js') as typeof import('./commands/workflows/index.js') 89 ).default 90 : null 91const 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 96const clearSkillIndexCache = feature('EXPERIMENTAL_SKILL_SEARCH') 97 ? ( 98 require('./services/skillSearch/localSearch.js') as typeof import('./services/skillSearch/localSearch.js') 99 ).clearSkillIndexCache 100 : null 101const subscribePr = feature('KAIROS_GITHUB_WEBHOOKS') 102 ? require('./commands/subscribe-pr.js').default 103 : null 104const ultraplan = feature('ULTRAPLAN') 105 ? require('./commands/ultraplan.js').default 106 : null 107const torch = feature('TORCH') ? require('./commands/torch.js').default : null 108const peersCmd = feature('UDS_INBOX') 109 ? ( 110 require('./commands/peers/index.js') as typeof import('./commands/peers/index.js') 111 ).default 112 : null 113const forkCmd = feature('FORK_SUBAGENT') 114 ? ( 115 require('./commands/fork/index.js') as typeof import('./commands/fork/index.js') 116 ).default 117 : null 118const buddy = feature('BUDDY') 119 ? ( 120 require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js') 121 ).default 122 : null 123/* eslint-enable @typescript-eslint/no-require-imports */ 124import thinkback from './commands/thinkback/index.js' 125import thinkbackPlay from './commands/thinkback-play/index.js' 126import permissions from './commands/permissions/index.js' 127import plan from './commands/plan/index.js' 128import fast from './commands/fast/index.js' 129import passes from './commands/passes/index.js' 130import privacySettings from './commands/privacy-settings/index.js' 131import hooks from './commands/hooks/index.js' 132import files from './commands/files/index.js' 133import branch from './commands/branch/index.js' 134import agents from './commands/agents/index.js' 135import plugin from './commands/plugin/index.js' 136import reloadPlugins from './commands/reload-plugins/index.js' 137import rewind from './commands/rewind/index.js' 138import heapDump from './commands/heapdump/index.js' 139import mockLimits from './commands/mock-limits/index.js' 140import bridgeKick from './commands/bridge-kick.js' 141import version from './commands/version.js' 142import summary from './commands/summary/index.js' 143import { 144 resetLimits, 145 resetLimitsNonInteractive, 146} from './commands/reset-limits/index.js' 147import antTrace from './commands/ant-trace/index.js' 148import perfIssue from './commands/perf-issue/index.js' 149import sandboxToggle from './commands/sandbox-toggle/index.js' 150import chrome from './commands/chrome/index.js' 151import stickers from './commands/stickers/index.js' 152import advisor from './commands/advisor.js' 153import { logError } from './utils/log.js' 154import { toError } from './utils/errors.js' 155import { logForDebugging } from './utils/debug.js' 156import { 157 getSkillDirCommands, 158 clearSkillCaches, 159 getDynamicSkills, 160} from './skills/loadSkillsDir.js' 161import { getBundledSkills } from './skills/bundledSkills.js' 162import { getBuiltinPluginSkillCommands } from './plugins/builtinPlugins.js' 163import { 164 getPluginCommands, 165 clearPluginCommandCache, 166 getPluginSkills, 167 clearPluginSkillsCache, 168} from './utils/plugins/loadPluginCommands.js' 169import memoize from 'lodash-es/memoize.js' 170import { isUsing3PServices, isClaudeAISubscriber } from './utils/auth.js' 171import { isFirstPartyAnthropicBaseUrl } from './utils/model/providers.js' 172import env from './commands/env/index.js' 173import exit from './commands/exit/index.js' 174import exportCommand from './commands/export/index.js' 175import model from './commands/model/index.js' 176import tag from './commands/tag/index.js' 177import outputStyle from './commands/output-style/index.js' 178import remoteEnv from './commands/remote-env/index.js' 179import upgrade from './commands/upgrade/index.js' 180import { 181 extraUsage, 182 extraUsageNonInteractive, 183} from './commands/extra-usage/index.js' 184import rateLimitOptions from './commands/rate-limit-options/index.js' 185import statusline from './commands/statusline.js' 186import effort from './commands/effort/index.js' 187import stats from './commands/stats/index.js' 188// insights.ts is 113KB (3200 lines, includes diffLines/html rendering). Lazy 189// shim defers the heavy module until /insights is actually invoked. 190const usageReport: Command = { 191 type: 'prompt', 192 name: 'insights', 193 description: 'Generate a report analyzing your Claude Code sessions', 194 contentLength: 0, 195 progressMessage: 'analyzing your sessions', 196 source: 'builtin', 197 async getPromptForCommand(args, context) { 198 const real = (await import('./commands/insights.js')).default 199 if (real.type !== 'prompt') throw new Error('unreachable') 200 return real.getPromptForCommand(args, context) 201 }, 202} 203import oauthRefresh from './commands/oauth-refresh/index.js' 204import debugToolCall from './commands/debug-tool-call/index.js' 205import { getSettingSourceName } from './utils/settings/constants.js' 206import { 207 type Command, 208 getCommandName, 209 isCommandEnabled, 210} from './types/command.js' 211 212// Re-export types from the centralized location 213export type { 214 Command, 215 CommandBase, 216 CommandResultDisplay, 217 LocalCommandResult, 218 LocalJSXCommandContext, 219 PromptCommand, 220 ResumeEntrypoint, 221} from './types/command.js' 222export { getCommandName, isCommandEnabled } from './types/command.js' 223 224// Commands that get eliminated from the external build 225export const INTERNAL_ONLY_COMMANDS = [ 226 backfillSessions, 227 breakCache, 228 bughunter, 229 commit, 230 commitPushPr, 231 ctx_viz, 232 goodClaude, 233 issue, 234 initVerifiers, 235 ...(forceSnip ? [forceSnip] : []), 236 mockLimits, 237 bridgeKick, 238 version, 239 ...(ultraplan ? [ultraplan] : []), 240 ...(subscribePr ? [subscribePr] : []), 241 resetLimits, 242 resetLimitsNonInteractive, 243 onboarding, 244 share, 245 summary, 246 teleport, 247 antTrace, 248 perfIssue, 249 env, 250 oauthRefresh, 251 debugToolCall, 252 agentsPlatform, 253 autofixPr, 254].filter(Boolean) 255 256// Declared as a function so that we don't run this until getCommands is called, 257// since underlying functions read from config, which can't be read at module initialization time 258const COMMANDS = memoize((): Command[] => [ 259 addDir, 260 advisor, 261 agents, 262 branch, 263 btw, 264 chrome, 265 clear, 266 color, 267 compact, 268 config, 269 copy, 270 desktop, 271 context, 272 contextNonInteractive, 273 cost, 274 diff, 275 doctor, 276 effort, 277 exit, 278 fast, 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 ? [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 348export const builtInCommandNames = memoize( 349 (): Set<string> => 350 new Set(COMMANDS().flatMap(_ => [_.name, ...(_.aliases ?? [])])), 351) 352 353async 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 */ 401const 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 */ 417export 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 */ 449const 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 */ 476export 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 */ 523export 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 534export 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 */ 547export 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/) 563export 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. 586export 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 */ 619export 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 */ 651export 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 */ 672export 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 */ 684export function filterCommandsForRemoteMode(commands: Command[]): Command[] { 685 return commands.filter(cmd => REMOTE_SAFE_COMMANDS.has(cmd)) 686} 687 688export 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 700export function hasCommand(commandName: string, commands: Command[]): boolean { 701 return findCommand(commandName, commands) !== undefined 702} 703 704export 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 */ 728export 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}