Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

commands.ts(约 755 行)是 Claude Code 斜杠命令的「总注册表」:静态 import 数十个内置命令、memoize 组装 COMMANDS() 数组、异步 loadAllCommands 合并 skills/plugins/workflows,并导出 getCommands、findCommand、getSkillToolCommands 等供 REPL typeahead、SkillTool 与 processSlashCommand 使用。本章要求你能从用户输入 /help 反查到命令池构建与 availability 过滤的完整链路。

学完本章你应该能

  • 说明 COMMANDS memoize 与 feature-gated require 的条件导入
  • 解释 loadAllCommands 合并顺序与 dynamic skills 插入点
  • 描述 meetsAvailabilityRequirement 与 isCommandEnabled 的分工
  • 理解 getSkillToolCommands 与 getSlashCommandToolSkills 的过滤差异
  • 能在 processSlashCommand 入口定位 findCommand 与 type 分派

核心概念(先读懂这些)

commands.ts 不在 commands/ 目录内

架构上 commands.ts 位于 src/ 根,commands/ 子目录仅放各命令实现。注册表 import ./commands/xxx/index.js,实现方再 import ../../commands.js 取 Command 类型。这种单向依赖避免 commands/ 子树互相引用注册逻辑。

memoize 与 mid-session 失效

loadAllCommands 按 cwd memoize(磁盘 I/O 昂贵),但 meetsAvailabilityRequirement 不 memoize——用户 /login 后 claude-ai 命令须立即可见。clearCommandMemoizationCaches 在动态 skill 加入时清缓存;clearCommandsCache additionally 清 plugin/skill 磁盘缓存。

三种 type 决定 processSlashCommand 分支

local 直接 call(args, context),compact 返回 { type: 'compact' } 触发 buildPostCompactMessages。local-jsx 通过 setToolJSX 渲染 Ink 组件,onDone 回调写 system 消息。prompt 调用 getPromptForCommand 展开 XML 标签块,可 inline 或 fork 子 Agent。

建议学习步骤

  1. 阅读源码块 A:COMMANDS 数组与 INTERNAL_ONLY
  2. 阅读源码块 B:loadAllCommands 与 getCommands
  3. 阅读源码块 C:meetsAvailabilityRequirement
  4. 阅读源码块 D:Command 类型 union
  5. 阅读源码块 E:getSkillToolCommands 过滤规则
  6. 阅读源码块 F:processSlashCommand 入口
  7. 阅读源码块 G:REMOTE_SAFE 与 isBridgeSafeCommand

常见误区

注意

不要把 utils/bash/commands.ts 与 src/commands.ts 混淆

注意

builtInCommandNames 含 aliases,findCommand 也匹配 aliases

注意

insights 命令用 lazy shim 避免 113KB 模块在启动时加载

在架构中的位置

斜杠命令从输入到执行的典型路径:

用户输入 "/model opus"
  → parseSlashCommand (slashCommandParsing.ts)
  → getCommands(cwd) 获取当前可用池
  → findCommand("model", commands)
  → processSlashCommand 按 cmd.type 分派
  → local-jsx: load() → model.tsx call(onDone, ctx, "opus")
  → onDone 写 system 消息;可选 shouldQuery 继续对话

commands.ts 不负责 UI 或 API 调用,只提供 Command[] 与查找辅助函数。Typeahead、help 屏、SkillTool schema、remote init 过滤均依赖 getCommands 输出。

COMMANDS 数组与条件导入

COMMANDS 声明为 memoize((): Command[] => [...]),延迟到首次 getCommands 才求值——因为底层 isUsing3PServices() 等读 config,不能在模块 init 时执行。

数组包含约 70+ 内置命令(addDir、compact、model、mcp…),末尾 spread 条件块:

  • feature('KAIROS') → proactive、brief、assistant
  • feature('BRIDGE_MODE') → bridge、remoteControlServer
  • USER_TYPE === 'ant' → INTERNAL_ONLY_COMMANDS(backfillSessions、commit、antTrace…)

usageReport(/insights)是内联 lazy shim:getPromptForCommand 内 dynamic import ./commands/insights.js,避免 3200 行 diff 渲染模块拖慢启动。

builtInCommandNames 扁平化 name + aliases 为 Set,供 parseSlashCommand 快速判断是否为内置命令名。

源码引用: src/commands.ts · 第 258–346 行(共 755 行)

 258| const 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| ])

源码引用: src/commands.ts · 第 190–202 行(共 755 行)

 190| const 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| }

源码引用: src/commands.ts · 第 225–254 行(共 755 行)

 225| export 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)

loadAllCommands 与 getCommands

loadAllCommands(cwd) 并行 await:

  1. getSkills — skillDirCommands、pluginSkills、bundledSkills、builtinPluginSkills
  2. getPluginCommands()
  3. getWorkflowCommands(cwd)(WORKFLOW_SCRIPTS feature)

返回顺序:bundled → builtinPlugin → skillDir → workflow → pluginCmd → pluginSkill → COMMANDS()。内置命令 intentionally 在最后,便于 dynamic skill 插入到「plugin 之后、builtin 之前」。

getCommands 流程:

  1. await loadAllCommands
  2. filter meetsAvailabilityRequirement && isCommandEnabled
  3. 若有 getDynamicSkills(),dedupe 后 insertIndex = 第一个内置命令位置,插入 uniqueDynamicSkills

这保证文件 touch 新发现的 skill 出现在 typeahead 靠前位置,但不覆盖已有同名命令。

源码引用: src/commands.ts · 第 353–398 行(共 755 行)

 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| }

源码引用: src/commands.ts · 第 449–517 行(共 755 行)

 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| }

availability 与 auth 过滤

CommandAvailability 仅两种:claude-ai(OAuth 订阅)、console(直连 api.anthropic.com API key,非 3P、非自定义 base URL)。

meetsAvailabilityRequirement 无 availability 字段 → true;否则至少匹配一种 auth 类型。注释强调:这运行在 isEnabled() 之前,provider-gated 命令对 Bedrock 用户完全隐藏,与 feature flag 无关。

典型用法:extra-usage、rate-limit 等仅 1P 用户可见的命令设置 availability: ['claude-ai', 'console']。

login/logout 命令反向 gated:...(!isUsing3PServices() ? [logout, login()] : []) 仅非 3P 环境注册。

源码引用: src/commands.ts · 第 417–443 行(共 755 行)

 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| }

源码引用: src/types/command.ts · 第 154–173 行(共 217 行)

 154| /**
 155|  * Declares which auth/provider environments a command is available in.
 156|  *
 157|  * This is separate from `isEnabled()`:
 158|  *   - `availability` = who can use this (auth/provider requirement, static)
 159|  *   - `isEnabled()`  = is this turned on right now (GrowthBook, platform, env vars)
 160|  *
 161|  * Commands without `availability` are available everywhere.
 162|  * Commands with `availability` are only shown if the user matches at least one
 163|  * of the listed auth types. See meetsAvailabilityRequirement() in commands.ts.
 164|  *
 165|  * Example: `availability: ['claude-ai', 'console']` shows the command to
 166|  * claude.ai subscribers and direct Console API key users (api.anthropic.com),
 167|  * but hides it from Bedrock/Vertex/Foundry users and custom base URL users.
 168|  */
 169| export type CommandAvailability =
 170|   // claude.ai OAuth subscriber (Pro/Max/Team/Enterprise via claude.ai)
 171|   | 'claude-ai'
 172|   // Console API key user (direct api.anthropic.com, not via claude.ai OAuth)
 173|   | 'console'

Command 类型系统

types/command.ts 定义 discriminated union:

PromptCommand — type: 'prompt',含 source、getPromptForCommand、可选 context:'fork'、agent、effort、hooks、paths(文件 touch 后才可见)。

LocalCommand — type: 'local',supportsNonInteractive,call 返回 LocalCommandResult(text | compact | skip)。

LocalJSXCommand — type: 'local-jsx',call 返回 ReactNode,onDone 控制 display:'system'|'user'|'skip'。

CommandBase 共享字段:name、description、aliases、isEnabled、isHidden、immediate、isSensitive、loadedFrom、disableModelInvocation、userInvocable。

getCommandName 支持 userFacingName() 覆盖(plugin 前缀剥离)。isCommandEnabled 默认 true。

源码引用: src/types/command.ts · 第 16–78 行(共 217 行)

  16| export type LocalCommandResult =
  17|   | { type: 'text'; value: string }
  18|   | {
  19|       type: 'compact'
  20|       compactionResult: CompactionResult
  21|       displayText?: string
  22|     }
  23|   | { type: 'skip' } // Skip messages
  24| 
  25| export type PromptCommand = {
  26|   type: 'prompt'
  27|   progressMessage: string
  28|   contentLength: number // Length of command content in characters (used for token estimation)
  29|   argNames?: string[]
  30|   allowedTools?: string[]
  31|   model?: string
  32|   source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
  33|   pluginInfo?: {
  34|     pluginManifest: PluginManifest
  35|     repository: string
  36|   }
  37|   disableNonInteractive?: boolean
  38|   // Hooks to register when this skill is invoked
  39|   hooks?: HooksSettings
  40|   // Base directory for skill resources (used to set CLAUDE_PLUGIN_ROOT environment variable for skill hooks)
  41|   skillRoot?: string
  42|   // Execution context: 'inline' (default) or 'fork' (run as sub-agent)
  43|   // 'inline' = skill content expands into the current conversation
  44|   // 'fork' = skill runs in a sub-agent with separate context and token budget
  45|   context?: 'inline' | 'fork'
  46|   // Agent type to use when forked (e.g., 'Bash', 'general-purpose')
  47|   // Only applicable when context is 'fork'
  48|   agent?: string
  49|   effort?: EffortValue
  50|   // Glob patterns for file paths this skill applies to
  51|   // When set, the skill is only visible after the model touches matching files
  52|   paths?: string[]
  53|   getPromptForCommand(
  54|     args: string,
  55|     context: ToolUseContext,
  56|   ): Promise<ContentBlockParam[]>
  57| }
  58| 
  59| /**
  60|  * The call signature for a local command implementation.
  61|  */
  62| export type LocalCommandCall = (
  63|   args: string,
  64|   context: LocalJSXCommandContext,
  65| ) => Promise<LocalCommandResult>
  66| 
  67| /**
  68|  * Module shape returned by load() for lazy-loaded local commands.
  69|  */
  70| export type LocalCommandModule = {
  71|   call: LocalCommandCall
  72| }
  73| 
  74| type LocalCommand = {
  75|   type: 'local'
  76|   supportsNonInteractive: boolean
  77|   load: () => Promise<LocalCommandModule>
  78| }

源码引用: src/types/command.ts · 第 175–216 行(共 217 行)

 175| export type CommandBase = {
 176|   availability?: CommandAvailability[]
 177|   description: string
 178|   hasUserSpecifiedDescription?: boolean
 179|   /** Defaults to true. Only set when the command has conditional enablement (feature flags, env checks, etc). */
 180|   isEnabled?: () => boolean
 181|   /** Defaults to false. Only set when the command should be hidden from typeahead/help. */
 182|   isHidden?: boolean
 183|   name: string
 184|   aliases?: string[]
 185|   isMcp?: boolean
 186|   argumentHint?: string // Hint text for command arguments (displayed in gray after command)
 187|   whenToUse?: string // From the "Skill" spec. Detailed usage scenarios for when to use this command
 188|   version?: string // Version of the command/skill
 189|   disableModelInvocation?: boolean // Whether to disable this command from being invoked by models
 190|   userInvocable?: boolean // Whether users can invoke this skill by typing /skill-name
 191|   loadedFrom?:
 192|     | 'commands_DEPRECATED'
 193|     | 'skills'
 194|     | 'plugin'
 195|     | 'managed'
 196|     | 'bundled'
 197|     | 'mcp' // Where the command was loaded from
 198|   kind?: 'workflow' // Distinguishes workflow-backed commands (badged in autocomplete)
 199|   immediate?: boolean // If true, command executes immediately without waiting for a stop point (bypasses queue)
 200|   isSensitive?: boolean // If true, args are redacted from the conversation history
 201|   /** Defaults to `name`. Only override when the displayed name differs (e.g. plugin prefix stripping). */
 202|   userFacingName?: () => string
 203| }
 204| 
 205| export type Command = CommandBase &
 206|   (PromptCommand | LocalCommand | LocalJSXCommand)
 207| 
 208| /** Resolves the user-visible name, falling back to `cmd.name` when not overridden. */
 209| export function getCommandName(cmd: CommandBase): string {
 210|   return cmd.userFacingName?.() ?? cmd.name
 211| }
 212| 
 213| /** Resolves whether the command is enabled, defaulting to true. */
 214| export function isCommandEnabled(cmd: CommandBase): boolean {
 215|   return cmd.isEnabled?.() ?? true
 216| }

SkillTool 与 MCP skill 命令

getSkillToolCommands 过滤 prompt 命令供 SkillTool 列表:

  • type === 'prompt' && !disableModelInvocation && source !== 'builtin'
  • loadedFrom 为 bundled/skills/commands_DEPRECATED 时免 description 要求
  • plugin/MCP 须有 hasUserSpecifiedDescription 或 whenToUse

getSlashCommandToolSkills 更严:须 skills/plugin/bundled loadedFrom 或 disableModelInvocation。

getMcpSkillCommands 从 AppState.mcp.commands 筛 MCP 来源的 model-invocable prompt(MCP_SKILLS feature)。这些不在 getCommands 主池,SkillTool 单独 thread。

formatDescriptionWithSource 为 typeahead/help 加 (plugin名) 或 (bundled) 后缀;SkillTool prompt 用原始 description。

源码引用: src/commands.ts · 第 523–608 行(共 755 行)

 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| )

源码引用: src/commands.ts · 第 688–754 行(共 755 行)

 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| }

processSlashCommand 分派入口

utils/processUserInput/processSlashCommand.tsx 是运行时执行中心(900+ 行):

  1. parseSlashCommand 解析命令名与 args
  2. findCommand / getCommand 定位 Command
  3. 按 type 分支:
    • prompt:inline 展开或 executeForkedSlashCommand(子 Agent)
    • local:await load().call(args, context);compact 结果走 buildPostCompactMessages
    • local-jsx:await load().call(onDone, context, args);setToolJSX 渲染

Fork 路径记录 tengu_slash_command_forked 事件;KAIROS assistant 模式下 context:fork 可 fire-and-forget 后台 subagent。

敏感命令 isSensitive 时 args 从 transcript redact。MalformedCommandError 在用户输入非法格式时抛出。

源码引用: src/utils/processUserInput/processSlashCommand.tsx · 第 1–51 行(共 1263 行)

   1| import { feature } from 'bun:bundle'
   2| import type {
   3|   ContentBlockParam,
   4|   TextBlockParam,
   5| } from '@anthropic-ai/sdk/resources'
   6| import { randomUUID } from 'crypto'
   7| import { setPromptId } from 'src/bootstrap/state.js'
   8| import {
   9|   builtInCommandNames,
  10|   type Command,
  11|   type CommandBase,
  12|   findCommand,
  13|   getCommand,
  14|   getCommandName,
  15|   hasCommand,
  16|   type PromptCommand,
  17| } from 'src/commands.js'
  18| import { NO_CONTENT_MESSAGE } from 'src/constants/messages.js'
  19| import type { SetToolJSXFn, ToolUseContext } from 'src/Tool.js'
  20| import type {
  21|   AssistantMessage,
  22|   AttachmentMessage,
  23|   Message,
  24|   NormalizedUserMessage,
  25|   ProgressMessage,
  26|   UserMessage,
  27| } from 'src/types/message.js'
  28| import { addInvokedSkill, getSessionId } from '../../bootstrap/state.js'
  29| import { COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG } from '../../constants/xml.js'
  30| import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
  31| import {
  32|   type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  33|   type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
  34|   logEvent,
  35| } from '../../services/analytics/index.js'
  36| import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js'
  37| import { buildPostCompactMessages } from '../../services/compact/compact.js'
  38| import { resetMicrocompactState } from '../../services/compact/microCompact.js'
  39| import type { Progress as AgentProgress } from '../../tools/AgentTool/AgentTool.js'
  40| import { runAgent } from '../../tools/AgentTool/runAgent.js'
  41| import { renderToolUseProgressMessage } from '../../tools/AgentTool/UI.js'
  42| import type { CommandResultDisplay } from '../../types/command.js'
  43| import { createAbortController } from '../abortController.js'
  44| import { getAgentContext } from '../agentContext.js'
  45| import {
  46|   createAttachmentMessage,
  47|   getAttachmentMessages,
  48| } from '../attachments.js'
  49| import { logForDebugging } from '../debug.js'
  50| import { isEnvTruthy } from '../envUtils.js'
  51| import { AbortError, MalformedCommandError } from '../errors.js'

源码引用: src/utils/processUserInput/processSlashCommand.tsx · 第 309–340 行(共 1263 行)

 309|       model: command.model as ModelAlias | undefined,
 310|       availableTools: context.options.tools,
 311|     })) {
 312|       agentMessages.push(message)
 313|       const normalizedNew = normalizeMessages([message])
 314| 
 315|       // Add progress message for assistant messages (which contain tool uses)
 316|       if (message.type === 'assistant') {
 317|         // Increment token count in spinner for assistant messages
 318|         const contentLength = getAssistantMessageContentLength(message)
 319|         if (contentLength > 0) {
 320|           context.setResponseLength(len => len + contentLength)
 321|         }
 322| 
 323|         const normalizedMsg = normalizedNew[0]
 324|         if (normalizedMsg && normalizedMsg.type === 'assistant') {
 325|           progressMessages.push(createProgressMessage(message))
 326|           updateProgress()
 327|         }
 328|       }
 329| 
 330|       // Add progress message for user messages (which contain tool results)
 331|       if (message.type === 'user') {
 332|         const normalizedMsg = normalizedNew[0]
 333|         if (normalizedMsg && normalizedMsg.type === 'user') {
 334|           progressMessages.push(createProgressMessage(normalizedMsg))
 335|           updateProgress()
 336|         }
 337|       }
 338|     }
 339|   } finally {
 340|     // Clear the progress display

Remote 与 Bridge 安全子集

REMOTE_SAFE_COMMANDS 定义 --remote 模式下 REPL 预过滤白名单:session、exit、clear、help、theme、cost、usage、plan 等——仅影响本地 TUI,不依赖本地 git/shell/MCP。

BRIDGE_SAFE_COMMANDS 允许 mobile/web Remote Control 执行的 local 命令:compact、clear、cost、summary、releaseNotes、files。PR #19134 曾 blanket 阻断 bridge 斜杠;isBridgeSafeCommand 放宽:

  • local-jsx → false(Ink UI 不能远程弹)
  • prompt → true(展开为文本)
  • local → 须在 BRIDGE_SAFE_COMMANDS 内

/model 从 iOS 弹出本地 picker 是 bridge 阻断 local-jsx 的直接动机。compact 列入 bridge-safe 因用户常从手机 mid-session 压缩上下文。

源码引用: src/commands.ts · 第 619–686 行(共 755 行)

 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| }

本章小结与延伸

commands.ts = 命令池的单一真相源。下一章 model-command,读 /model 如何写 AppState.mainLoopModel。 继续学习:

  • model-command
  • mcp-commands
Prev
模块: commands
Next
model-command · /model 模型选择