Download src/commands.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 25.4 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/commands.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/commands.ts
-
curl -L -o commands.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/commands.ts
25.4 kB
| // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered | |
| import addDir from './commands/add-dir/index.js' | |
| import autofixPr from './commands/autofix-pr/index.js' | |
| import backfillSessions from './commands/backfill-sessions/index.js' | |
| import btw from './commands/btw/index.js' | |
| import goodClaude from './commands/good-claude/index.js' | |
| import issue from './commands/issue/index.js' | |
| import feedback from './commands/feedback/index.js' | |
| import clear from './commands/clear/index.js' | |
| import color from './commands/color/index.js' | |
| import commit from './commands/commit.js' | |
| import copy from './commands/copy/index.js' | |
| import desktop from './commands/desktop/index.js' | |
| import commitPushPr from './commands/commit-push-pr.js' | |
| import compact from './commands/compact/index.js' | |
| import config from './commands/config/index.js' | |
| import { context, contextNonInteractive } from './commands/context/index.js' | |
| import cost from './commands/cost/index.js' | |
| import diff from './commands/diff/index.js' | |
| import ctx_viz from './commands/ctx_viz/index.js' | |
| import doctor from './commands/doctor/index.js' | |
| import memory from './commands/memory/index.js' | |
| import help from './commands/help/index.js' | |
| import ide from './commands/ide/index.js' | |
| import init from './commands/init.js' | |
| import initVerifiers from './commands/init-verifiers.js' | |
| import keybindings from './commands/keybindings/index.js' | |
| import login from './commands/login/index.js' | |
| import logout from './commands/logout/index.js' | |
| import installGitHubApp from './commands/install-github-app/index.js' | |
| import installSlackApp from './commands/install-slack-app/index.js' | |
| import breakCache from './commands/break-cache/index.js' | |
| import mcp from './commands/mcp/index.js' | |
| import mobile from './commands/mobile/index.js' | |
| import onboarding from './commands/onboarding/index.js' | |
| import pr_comments from './commands/pr_comments/index.js' | |
| import releaseNotes from './commands/release-notes/index.js' | |
| import rename from './commands/rename/index.js' | |
| import resume from './commands/resume/index.js' | |
| import review, { ultrareview } from './commands/review.js' | |
| import session from './commands/session/index.js' | |
| import share from './commands/share/index.js' | |
| import skills from './commands/skills/index.js' | |
| import status from './commands/status/index.js' | |
| import tasks from './commands/tasks/index.js' | |
| import feishu from './commands/feishu/index.js' | |
| import friend from './commands/friend/index.js' | |
| import goals from './commands/goal/index.js' | |
| import telegram from './commands/telegram/index.js' | |
| import teleport from './commands/teleport/index.js' | |
| /* eslint-disable @typescript-eslint/no-require-imports */ | |
| const agentsPlatform = | |
| process.env.USER_TYPE === 'ant' | |
| ? require('./commands/agents-platform/index.js').default | |
| : null | |
| /* eslint-enable @typescript-eslint/no-require-imports */ | |
| import securityReview from './commands/security-review.js' | |
| import bughunter from './commands/bughunter/index.js' | |
| import terminalSetup from './commands/terminalSetup/index.js' | |
| import usage from './commands/usage/index.js' | |
| import theme from './commands/theme/index.js' | |
| import vim from './commands/vim/index.js' | |
| import { feature } from 'bun:bundle' | |
| // Dead code elimination: conditional imports | |
| /* eslint-disable @typescript-eslint/no-require-imports */ | |
| const proactive = | |
| feature('PROACTIVE') || feature('KAIROS') | |
| ? require('./commands/proactive.js').default | |
| : null | |
| const briefCommand = | |
| feature('KAIROS') || feature('KAIROS_BRIEF') | |
| ? require('./commands/brief.js').default | |
| : null | |
| const assistantCommand = feature('KAIROS') | |
| ? require('./commands/assistant/index.js').default | |
| : null | |
| const bridge = feature('BRIDGE_MODE') | |
| ? require('./commands/bridge/index.js').default | |
| : null | |
| const remoteControlServerCommand = | |
| feature('DAEMON') && feature('BRIDGE_MODE') | |
| ? require('./commands/remoteControlServer/index.js').default | |
| : null | |
| const voiceCommand = feature('VOICE_MODE') | |
| ? require('./commands/voice/index.js').default | |
| : null | |
| const forceSnip = feature('HISTORY_SNIP') | |
| ? require('./commands/force-snip.js').default | |
| : null | |
| const workflowsCmd = feature('WORKFLOW_SCRIPTS') | |
| ? ( | |
| require('./commands/workflows/index.js') as typeof import('./commands/workflows/index.js') | |
| ).default | |
| : null | |
| const webCmd = feature('CCR_REMOTE_SETUP') | |
| ? ( | |
| require('./commands/remote-setup/index.js') as typeof import('./commands/remote-setup/index.js') | |
| ).default | |
| : null | |
| const clearSkillIndexCache = feature('EXPERIMENTAL_SKILL_SEARCH') | |
| ? ( | |
| require('./services/skillSearch/localSearch.js') as typeof import('./services/skillSearch/localSearch.js') | |
| ).clearSkillIndexCache | |
| : null | |
| const subscribePr = feature('KAIROS_GITHUB_WEBHOOKS') | |
| ? require('./commands/subscribe-pr.js').default | |
| : null | |
| const ultraplan = feature('ULTRAPLAN') | |
| ? require('./commands/ultraplan.js').default | |
| : null | |
| const torch = feature('TORCH') ? require('./commands/torch.js').default : null | |
| const peersCmd = feature('UDS_INBOX') | |
| ? ( | |
| require('./commands/peers/index.js') as typeof import('./commands/peers/index.js') | |
| ).default | |
| : null | |
| const forkCmd = feature('FORK_SUBAGENT') | |
| ? ( | |
| require('./commands/fork/index.js') as typeof import('./commands/fork/index.js') | |
| ).default | |
| : null | |
| const buddy = feature('BUDDY') | |
| ? ( | |
| require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js') | |
| ).default | |
| : null | |
| /* eslint-enable @typescript-eslint/no-require-imports */ | |
| import thinkback from './commands/thinkback/index.js' | |
| import thinkbackPlay from './commands/thinkback-play/index.js' | |
| import permissions from './commands/permissions/index.js' | |
| import plan from './commands/plan/index.js' | |
| import fast from './commands/fast/index.js' | |
| import passes from './commands/passes/index.js' | |
| import privacySettings from './commands/privacy-settings/index.js' | |
| import hooks from './commands/hooks/index.js' | |
| import files from './commands/files/index.js' | |
| import branch from './commands/branch/index.js' | |
| import agents from './commands/agents/index.js' | |
| import plugin from './commands/plugin/index.js' | |
| import reloadPlugins from './commands/reload-plugins/index.js' | |
| import rewind from './commands/rewind/index.js' | |
| import heapDump from './commands/heapdump/index.js' | |
| import mockLimits from './commands/mock-limits/index.js' | |
| import bridgeKick from './commands/bridge-kick.js' | |
| import version from './commands/version.js' | |
| import summary from './commands/summary/index.js' | |
| import { | |
| resetLimits, | |
| resetLimitsNonInteractive, | |
| } from './commands/reset-limits/index.js' | |
| import antTrace from './commands/ant-trace/index.js' | |
| import perfIssue from './commands/perf-issue/index.js' | |
| import sandboxToggle from './commands/sandbox-toggle/index.js' | |
| import chrome from './commands/chrome/index.js' | |
| import stickers from './commands/stickers/index.js' | |
| import advisor from './commands/advisor.js' | |
| import { logError } from './utils/log.js' | |
| import { toError } from './utils/errors.js' | |
| import { logForDebugging } from './utils/debug.js' | |
| import { | |
| getSkillDirCommands, | |
| clearSkillCaches, | |
| getDynamicSkills, | |
| } from './skills/loadSkillsDir.js' | |
| import { getBundledSkills } from './skills/bundledSkills.js' | |
| import { getBuiltinPluginSkillCommands } from './plugins/builtinPlugins.js' | |
| import { | |
| getPluginCommands, | |
| clearPluginCommandCache, | |
| getPluginSkills, | |
| clearPluginSkillsCache, | |
| } from './utils/plugins/loadPluginCommands.js' | |
| import memoize from 'lodash-es/memoize.js' | |
| import { isUsing3PServices, isClaudeAISubscriber } from './utils/auth.js' | |
| import { isFirstPartyAnthropicBaseUrl } from './utils/model/providers.js' | |
| import env from './commands/env/index.js' | |
| import exit from './commands/exit/index.js' | |
| import exportCommand from './commands/export/index.js' | |
| import model from './commands/model/index.js' | |
| import tag from './commands/tag/index.js' | |
| import outputStyle from './commands/output-style/index.js' | |
| import remoteEnv from './commands/remote-env/index.js' | |
| import upgrade from './commands/upgrade/index.js' | |
| import { | |
| extraUsage, | |
| extraUsageNonInteractive, | |
| } from './commands/extra-usage/index.js' | |
| import rateLimitOptions from './commands/rate-limit-options/index.js' | |
| import statusline from './commands/statusline.js' | |
| import effort from './commands/effort/index.js' | |
| import stats from './commands/stats/index.js' | |
| // insights.ts is 113KB (3200 lines, includes diffLines/html rendering). Lazy | |
| // shim defers the heavy module until /insights is actually invoked. | |
| const usageReport: Command = { | |
| type: 'prompt', | |
| name: 'insights', | |
| description: 'Generate a report analyzing your Claude Code sessions', | |
| contentLength: 0, | |
| progressMessage: 'analyzing your sessions', | |
| source: 'builtin', | |
| async getPromptForCommand(args, context) { | |
| const real = (await import('./commands/insights.js')).default | |
| if (real.type !== 'prompt') throw new Error('unreachable') | |
| return real.getPromptForCommand(args, context) | |
| }, | |
| } | |
| import oauthRefresh from './commands/oauth-refresh/index.js' | |
| import debugToolCall from './commands/debug-tool-call/index.js' | |
| import { getSettingSourceName } from './utils/settings/constants.js' | |
| import { | |
| type Command, | |
| getCommandName, | |
| isCommandEnabled, | |
| } from './types/command.js' | |
| // Re-export types from the centralized location | |
| export type { | |
| Command, | |
| CommandBase, | |
| CommandResultDisplay, | |
| LocalCommandResult, | |
| LocalJSXCommandContext, | |
| PromptCommand, | |
| ResumeEntrypoint, | |
| } from './types/command.js' | |
| export { getCommandName, isCommandEnabled } from './types/command.js' | |
| // Commands that get eliminated from the external build | |
| export const INTERNAL_ONLY_COMMANDS = [ | |
| backfillSessions, | |
| breakCache, | |
| bughunter, | |
| commit, | |
| commitPushPr, | |
| ctx_viz, | |
| goodClaude, | |
| issue, | |
| initVerifiers, | |
| ...(forceSnip ? [forceSnip] : []), | |
| mockLimits, | |
| bridgeKick, | |
| version, | |
| ...(subscribePr ? [subscribePr] : []), | |
| resetLimits, | |
| resetLimitsNonInteractive, | |
| onboarding, | |
| share, | |
| summary, | |
| teleport, | |
| antTrace, | |
| perfIssue, | |
| env, | |
| oauthRefresh, | |
| debugToolCall, | |
| agentsPlatform, | |
| autofixPr, | |
| ].filter(Boolean) | |
| // Declared as a function so that we don't run this until getCommands is called, | |
| // since underlying functions read from config, which can't be read at module initialization time | |
| const COMMANDS = memoize((): Command[] => [ | |
| addDir, | |
| advisor, | |
| agents, | |
| branch, | |
| btw, | |
| chrome, | |
| clear, | |
| color, | |
| compact, | |
| config, | |
| copy, | |
| desktop, | |
| context, | |
| contextNonInteractive, | |
| cost, | |
| diff, | |
| doctor, | |
| effort, | |
| exit, | |
| fast, | |
| files, | |
| heapDump, | |
| help, | |
| ide, | |
| init, | |
| keybindings, | |
| installGitHubApp, | |
| installSlackApp, | |
| mcp, | |
| memory, | |
| mobile, | |
| model, | |
| outputStyle, | |
| remoteEnv, | |
| plugin, | |
| pr_comments, | |
| releaseNotes, | |
| reloadPlugins, | |
| rename, | |
| resume, | |
| session, | |
| skills, | |
| stats, | |
| status, | |
| statusline, | |
| stickers, | |
| tag, | |
| telegram, | |
| theme, | |
| feedback, | |
| feishu, | |
| friend, | |
| goals, | |
| review, | |
| ultrareview, | |
| rewind, | |
| securityReview, | |
| terminalSetup, | |
| upgrade, | |
| extraUsage, | |
| extraUsageNonInteractive, | |
| rateLimitOptions, | |
| usage, | |
| usageReport, | |
| vim, | |
| ...(webCmd ? [webCmd] : []), | |
| ...(forkCmd ? [forkCmd] : []), | |
| ...(buddy ? [buddy] : []), | |
| ...(proactive ? [proactive] : []), | |
| ...(briefCommand ? [briefCommand] : []), | |
| ...(assistantCommand ? [assistantCommand] : []), | |
| ...(bridge ? [bridge] : []), | |
| ...(remoteControlServerCommand ? [remoteControlServerCommand] : []), | |
| ...(voiceCommand ? [voiceCommand] : []), | |
| thinkback, | |
| thinkbackPlay, | |
| permissions, | |
| plan, | |
| privacySettings, | |
| hooks, | |
| exportCommand, | |
| sandboxToggle, | |
| ...(!isUsing3PServices() ? [logout, login()] : []), | |
| passes, | |
| ...(peersCmd ? [peersCmd] : []), | |
| tasks, | |
| ...(workflowsCmd ? [workflowsCmd] : []), | |
| ...(torch ? [torch] : []), | |
| ...(ultraplan ? [ultraplan] : []), | |
| ...(process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO | |
| ? INTERNAL_ONLY_COMMANDS | |
| : []), | |
| ]) | |
| export const builtInCommandNames = memoize( | |
| (): Set<string> => | |
| new Set(COMMANDS().flatMap(_ => [_.name, ...(_.aliases ?? [])])), | |
| ) | |
| async function getSkills(cwd: string): Promise<{ | |
| skillDirCommands: Command[] | |
| pluginSkills: Command[] | |
| bundledSkills: Command[] | |
| builtinPluginSkills: Command[] | |
| }> { | |
| try { | |
| const [skillDirCommands, pluginSkills] = await Promise.all([ | |
| getSkillDirCommands(cwd).catch(err => { | |
| logError(toError(err)) | |
| logForDebugging( | |
| 'Skill directory commands failed to load, continuing without them', | |
| ) | |
| return [] | |
| }), | |
| getPluginSkills().catch(err => { | |
| logError(toError(err)) | |
| logForDebugging('Plugin skills failed to load, continuing without them') | |
| return [] | |
| }), | |
| ]) | |
| // Bundled skills are registered synchronously at startup | |
| const bundledSkills = getBundledSkills() | |
| // Built-in plugin skills come from enabled built-in plugins | |
| const builtinPluginSkills = getBuiltinPluginSkillCommands() | |
| logForDebugging( | |
| `getSkills returning: ${skillDirCommands.length} skill dir commands, ${pluginSkills.length} plugin skills, ${bundledSkills.length} bundled skills, ${builtinPluginSkills.length} builtin plugin skills`, | |
| ) | |
| return { | |
| skillDirCommands, | |
| pluginSkills, | |
| bundledSkills, | |
| builtinPluginSkills, | |
| } | |
| } catch (err) { | |
| // This should never happen since we catch at the Promise level, but defensive | |
| logError(toError(err)) | |
| logForDebugging('Unexpected error in getSkills, returning empty') | |
| return { | |
| skillDirCommands: [], | |
| pluginSkills: [], | |
| bundledSkills: [], | |
| builtinPluginSkills: [], | |
| } | |
| } | |
| } | |
| /* eslint-disable @typescript-eslint/no-require-imports */ | |
| const getWorkflowCommands = feature('WORKFLOW_SCRIPTS') | |
| ? ( | |
| require('./tools/WorkflowTool/createWorkflowCommand.js') as typeof import('./tools/WorkflowTool/createWorkflowCommand.js') | |
| ).getWorkflowCommands | |
| : null | |
| /* eslint-enable @typescript-eslint/no-require-imports */ | |
| /** | |
| * Filters commands by their declared `availability` (auth/provider requirement). | |
| * Commands without `availability` are treated as universal. | |
| * This runs before `isEnabled()` so that provider-gated commands are hidden | |
| * regardless of feature-flag state. | |
| * | |
| * Not memoized — auth state can change mid-session (e.g. after /login), | |
| * so this must be re-evaluated on every getCommands() call. | |
| */ | |
| export function meetsAvailabilityRequirement(cmd: Command): boolean { | |
| if (!cmd.availability) return true | |
| for (const a of cmd.availability) { | |
| switch (a) { | |
| case 'claude-ai': | |
| if (isClaudeAISubscriber()) return true | |
| break | |
| case 'console': | |
| // Console API key user = direct 1P API customer (not 3P, not claude.ai). | |
| // Excludes 3P (Bedrock/Vertex/Foundry) who don't set ANTHROPIC_BASE_URL | |
| // and gateway users who proxy through a custom base URL. | |
| if ( | |
| !isClaudeAISubscriber() && | |
| !isUsing3PServices() && | |
| isFirstPartyAnthropicBaseUrl() | |
| ) | |
| return true | |
| break | |
| default: { | |
| const _exhaustive: never = a | |
| void _exhaustive | |
| break | |
| } | |
| } | |
| } | |
| return false | |
| } | |
| /** | |
| * Loads all command sources (skills, plugins, workflows). Memoized by cwd | |
| * because loading is expensive (disk I/O, dynamic imports). | |
| */ | |
| const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => { | |
| const [ | |
| { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills }, | |
| pluginCommands, | |
| workflowCommands, | |
| ] = await Promise.all([ | |
| getSkills(cwd), | |
| getPluginCommands(), | |
| getWorkflowCommands ? getWorkflowCommands(cwd) : Promise.resolve([]), | |
| ]) | |
| return [ | |
| ...bundledSkills, | |
| ...builtinPluginSkills, | |
| ...skillDirCommands, | |
| ...workflowCommands, | |
| ...pluginCommands, | |
| ...pluginSkills, | |
| ...COMMANDS(), | |
| ] | |
| }) | |
| /** | |
| * Returns commands available to the current user. The expensive loading is | |
| * memoized, but availability and isEnabled checks run fresh every call so | |
| * auth changes (e.g. /login) take effect immediately. | |
| */ | |
| export async function getCommands(cwd: string): Promise<Command[]> { | |
| const allCommands = await loadAllCommands(cwd) | |
| // Get dynamic skills discovered during file operations | |
| const dynamicSkills = getDynamicSkills() | |
| // Build base commands without dynamic skills | |
| const baseCommands = allCommands.filter( | |
| _ => meetsAvailabilityRequirement(_) && isCommandEnabled(_), | |
| ) | |
| if (dynamicSkills.length === 0) { | |
| return baseCommands | |
| } | |
| // Dedupe dynamic skills - only add if not already present | |
| const baseCommandNames = new Set(baseCommands.map(c => c.name)) | |
| const uniqueDynamicSkills = dynamicSkills.filter( | |
| s => | |
| !baseCommandNames.has(s.name) && | |
| meetsAvailabilityRequirement(s) && | |
| isCommandEnabled(s), | |
| ) | |
| if (uniqueDynamicSkills.length === 0) { | |
| return baseCommands | |
| } | |
| // Insert dynamic skills after plugin skills but before built-in commands | |
| const builtInNames = new Set(COMMANDS().map(c => c.name)) | |
| const insertIndex = baseCommands.findIndex(c => builtInNames.has(c.name)) | |
| if (insertIndex === -1) { | |
| return [...baseCommands, ...uniqueDynamicSkills] | |
| } | |
| return [ | |
| ...baseCommands.slice(0, insertIndex), | |
| ...uniqueDynamicSkills, | |
| ...baseCommands.slice(insertIndex), | |
| ] | |
| } | |
| /** | |
| * Clears only the memoization caches for commands, WITHOUT clearing skill caches. | |
| * Use this when dynamic skills are added to invalidate cached command lists. | |
| */ | |
| export function clearCommandMemoizationCaches(): void { | |
| loadAllCommands.cache?.clear?.() | |
| getSkillToolCommands.cache?.clear?.() | |
| getSlashCommandToolSkills.cache?.clear?.() | |
| // getSkillIndex in skillSearch/localSearch.ts is a separate memoization layer | |
| // built ON TOP of getSkillToolCommands/getCommands. Clearing only the inner | |
| // caches is a no-op for the outer — lodash memoize returns the cached result | |
| // without ever reaching the cleared inners. Must clear it explicitly. | |
| clearSkillIndexCache?.() | |
| } | |
| export function clearCommandsCache(): void { | |
| clearCommandMemoizationCaches() | |
| clearPluginCommandCache() | |
| clearPluginSkillsCache() | |
| clearSkillCaches() | |
| } | |
| /** | |
| * Filter AppState.mcp.commands to MCP-provided skills (prompt-type, | |
| * model-invocable, loaded from MCP). These live outside getCommands() so | |
| * callers that need MCP skills in their skill index thread them through | |
| * separately. | |
| */ | |
| export function getMcpSkillCommands( | |
| mcpCommands: readonly Command[], | |
| ): readonly Command[] { | |
| if (feature('MCP_SKILLS')) { | |
| return mcpCommands.filter( | |
| cmd => | |
| cmd.type === 'prompt' && | |
| cmd.loadedFrom === 'mcp' && | |
| !cmd.disableModelInvocation, | |
| ) | |
| } | |
| return [] | |
| } | |
| // SkillTool shows ALL prompt-based commands that the model can invoke | |
| // This includes both skills (from /skills/) and commands (from /commands/) | |
| export const getSkillToolCommands = memoize( | |
| async (cwd: string): Promise<Command[]> => { | |
| const allCommands = await getCommands(cwd) | |
| return allCommands.filter( | |
| cmd => | |
| cmd.type === 'prompt' && | |
| !cmd.disableModelInvocation && | |
| cmd.source !== 'builtin' && | |
| // Always include skills from /skills/ dirs, bundled skills, and legacy /commands/ entries | |
| // (they all get an auto-derived description from the first line if frontmatter is missing). | |
| // Plugin/MCP commands still require an explicit description to appear in the listing. | |
| (cmd.loadedFrom === 'bundled' || | |
| cmd.loadedFrom === 'skills' || | |
| cmd.loadedFrom === 'commands_DEPRECATED' || | |
| cmd.hasUserSpecifiedDescription || | |
| cmd.whenToUse), | |
| ) | |
| }, | |
| ) | |
| // Filters commands to include only skills. Skills are commands that provide | |
| // specialized capabilities for the model to use. They are identified by | |
| // loadedFrom being 'skills', 'plugin', or 'bundled', or having disableModelInvocation set. | |
| export const getSlashCommandToolSkills = memoize( | |
| async (cwd: string): Promise<Command[]> => { | |
| try { | |
| const allCommands = await getCommands(cwd) | |
| return allCommands.filter( | |
| cmd => | |
| cmd.type === 'prompt' && | |
| cmd.source !== 'builtin' && | |
| (cmd.hasUserSpecifiedDescription || cmd.whenToUse) && | |
| (cmd.loadedFrom === 'skills' || | |
| cmd.loadedFrom === 'plugin' || | |
| cmd.loadedFrom === 'bundled' || | |
| cmd.disableModelInvocation), | |
| ) | |
| } catch (error) { | |
| logError(toError(error)) | |
| // Return empty array rather than throwing - skills are non-critical | |
| // This prevents skill loading failures from breaking the entire system | |
| logForDebugging('Returning empty skills array due to load failure') | |
| return [] | |
| } | |
| }, | |
| ) | |
| /** | |
| * Commands that are safe to use in remote mode (--remote). | |
| * These only affect local TUI state and don't depend on local filesystem, | |
| * git, shell, IDE, MCP, or other local execution context. | |
| * | |
| * Used in two places: | |
| * 1. Pre-filtering commands in main.tsx before REPL renders (prevents race with CCR init) | |
| * 2. Preserving local-only commands in REPL's handleRemoteInit after CCR filters | |
| */ | |
| export const REMOTE_SAFE_COMMANDS: Set<Command> = new Set([ | |
| session, // Shows QR code / URL for remote session | |
| exit, // Exit the TUI | |
| clear, // Clear screen | |
| help, // Show help | |
| theme, // Change terminal theme | |
| color, // Change agent color | |
| vim, // Toggle vim mode | |
| cost, // Show session cost (local cost tracking) | |
| usage, // Show usage info | |
| copy, // Copy last message | |
| btw, // Quick note | |
| feedback, // Send feedback | |
| plan, // Plan mode toggle | |
| keybindings, // Keybinding management | |
| statusline, // Status line toggle | |
| stickers, // Stickers | |
| mobile, // Mobile QR code | |
| ]) | |
| /** | |
| * Builtin commands of type 'local' that ARE safe to execute when received | |
| * over the Remote Control bridge. These produce text output that streams | |
| * back to the mobile/web client and have no terminal-only side effects. | |
| * | |
| * 'local-jsx' commands are blocked by type (they render Ink UI) and | |
| * 'prompt' commands are allowed by type (they expand to text sent to the | |
| * model) — this set only gates 'local' commands. | |
| * | |
| * When adding a new 'local' command that should work from mobile, add it | |
| * here. Default is blocked. | |
| */ | |
| export const BRIDGE_SAFE_COMMANDS: Set<Command> = new Set( | |
| [ | |
| compact, // Shrink context — useful mid-session from a phone | |
| clear, // Wipe transcript | |
| cost, // Show session cost | |
| summary, // Summarize conversation | |
| releaseNotes, // Show changelog | |
| files, // List tracked files | |
| ].filter((c): c is Command => c !== null), | |
| ) | |
| /** | |
| * Whether a slash command is safe to execute when its input arrived over the | |
| * Remote Control bridge (mobile/web client). | |
| * | |
| * PR #19134 blanket-blocked all slash commands from bridge inbound because | |
| * `/model` from iOS was popping the local Ink picker. This predicate relaxes | |
| * that with an explicit allowlist: 'prompt' commands (skills) expand to text | |
| * and are safe by construction; 'local' commands need an explicit opt-in via | |
| * BRIDGE_SAFE_COMMANDS; 'local-jsx' commands render Ink UI and stay blocked. | |
| */ | |
| export function isBridgeSafeCommand(cmd: Command): boolean { | |
| if (cmd.type === 'local-jsx') return false | |
| if (cmd.type === 'prompt') return true | |
| return BRIDGE_SAFE_COMMANDS.has(cmd) | |
| } | |
| /** | |
| * Filter commands to only include those safe for remote mode. | |
| * Used to pre-filter commands when rendering the REPL in --remote mode, | |
| * preventing local-only commands from being briefly available before | |
| * the CCR init message arrives. | |
| */ | |
| export function filterCommandsForRemoteMode(commands: Command[]): Command[] { | |
| return commands.filter(cmd => REMOTE_SAFE_COMMANDS.has(cmd)) | |
| } | |
| export function findCommand( | |
| commandName: string, | |
| commands: Command[], | |
| ): Command | undefined { | |
| return commands.find( | |
| _ => | |
| _.name === commandName || | |
| getCommandName(_) === commandName || | |
| _.aliases?.includes(commandName), | |
| ) | |
| } | |
| export function hasCommand(commandName: string, commands: Command[]): boolean { | |
| return findCommand(commandName, commands) !== undefined | |
| } | |
| export function getCommand(commandName: string, commands: Command[]): Command { | |
| const command = findCommand(commandName, commands) | |
| if (!command) { | |
| throw ReferenceError( | |
| `Command ${commandName} not found. Available commands: ${commands | |
| .map(_ => { | |
| const name = getCommandName(_) | |
| return _.aliases ? `${name} (aliases: ${_.aliases.join(', ')})` : name | |
| }) | |
| .sort((a, b) => a.localeCompare(b)) | |
| .join(', ')}`, | |
| ) | |
| } | |
| return command | |
| } | |
| /** | |
| * Formats a command's description with its source annotation for user-facing UI. | |
| * Use this in typeahead, help screens, and other places where users need to see | |
| * where a command comes from. | |
| * | |
| * For model-facing prompts (like SkillTool), use cmd.description directly. | |
| */ | |
| export function formatDescriptionWithSource(cmd: Command): string { | |
| if (cmd.type !== 'prompt') { | |
| return cmd.description | |
| } | |
| if (cmd.kind === 'workflow') { | |
| return `${cmd.description} (workflow)` | |
| } | |
| if (cmd.source === 'plugin') { | |
| const pluginName = cmd.pluginInfo?.pluginManifest.name | |
| if (pluginName) { | |
| return `(${pluginName}) ${cmd.description}` | |
| } | |
| return `${cmd.description} (plugin)` | |
| } | |
| if (cmd.source === 'builtin' || cmd.source === 'mcp') { | |
| return cmd.description | |
| } | |
| if (cmd.source === 'bundled') { | |
| return `${cmd.description} (bundled)` | |
| } | |
| return `${cmd.description} (${getSettingSourceName(cmd.source)})` | |
| } | |