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

本章总览

cli/handlers/ 从 entrypoints/cli.tsx 拆出的子命令实现,采用 dynamic import 懒加载:只有用户运行 claude mcp *、claude plugin *、claude auth * 等时才拉取对应模块,缩短冷启动。auth.ts 处理 OAuth/API key;mcp.tsx 可启动 Ink 对话框或 headless MCP 管理;plugins.ts 覆盖 marketplace 与 validate;agents.ts 列出 agent 定义;autoMode.ts dump/critique classifier 规则。统一退出语义依赖 cli/exit.ts 的 cliError/cliOk。

学完本章你应该能

  • 说明 handlers 与 cli.tsx 的分工:注册 vs 实现
  • 走读 authLogin 与 installOAuthTokens 的后置步骤
  • 理解 mcp.tsx 何时 render Ink vs 纯 stdout
  • 知道 plugins.ts 的 VALID_INSTALLABLE_SCOPES 如何被 cli.tsx re-export
  • 能解释 autoModeCritiqueHandler 如何用 sideQuery 评审规则

核心概念(先读懂这些)

懒加载 = 启动路径隔离

main bundle 不应 import 整个 MCP Ink 树或 plugin marketplace 逻辑。handlers 文件头注释写明 "dynamically imported only when ... runs"。agents.ts 仅 ~70 行,plugin/mcp 则数百行。改子命令行为优先改 handlers,cli.tsx 只保留 Commander 选项定义与 import() 调用。

auth 与 services/oauth 分层

handlers/auth.ts 编排用户可见流程(browser OAuth、env refresh token、status JSON);token 存储、profile、API key 创建委托 services/oauth 与 utils/auth。installOAuthTokens 是共享后置:performLogout(clearOnboarding:false) → storeOAuthAccountInfo → saveOAuthTokensIfNeeded → fetchAndStoreUserRoles → createAndStoreApiKey(Console 用户)。

mcp.tsx 是唯一带 React 的 handler

mcp import 等命令 render MCPServerDesktopImportDialog + KeybindingSetup + AppStateProvider;list/remove/serve 等多为 stdout + cliOk/cliError。mcpServeHandler 动态 import setup.js 与 entrypoints/mcp.js 启动 stdio MCP server——与 REPL 内 MCP 客户端是不同进程角色。

建议学习步骤

  1. 阅读源码块 A:installOAuthTokens 与 authLogin 入口
  2. 阅读源码块 B:mcpServeHandler 与 mcpRemoveHandler
  3. 阅读源码块 C:plugins handleMarketplaceError 与 validate
  4. 阅读源码块 D:agentsHandler 分组输出
  5. 阅读源码块 E:autoModeConfigHandler 与 critique
  6. 在 SourceTree 打开 handlers/util.tsx(共享格式化)

常见误区

注意

auth.ts 部分路径仍直接 process.exit,未统一 cliError——历史原因,新代码应用 exit.ts

注意

plugin validate 的 --cowork 会 setUseCoworkPlugins(true) 影响后续 load

注意

autoModeCritique 需要已有 custom rules,否则 early return 提示

handlers 与 entrypoints 的关系

entrypoints/cli.tsx
  program.command('mcp').command('serve').action(async () => {
    const { mcpServeHandler } = await import('../cli/handlers/mcp.tsx')
    await mcpServeHandler(opts)
  })
  … auth / plugin / agents / auto-mode 同理
文件子命令前缀特点
auth.tsclaude auth login/logout/statusOAuth、API key、JSON status
mcp.tsxclaude mcp *部分 Ink UI;serve 启 MCP server
plugins.tsclaude plugin * / marketplace *安装、validate、refresh
agents.tsclaude agents只读列表
autoMode.tsclaude auto-mode *JSON dump + LLM critique
util.tsx(内部)handler 共享辅助

边界:REPL 内 /commands 注册在 src/commands/,与 CLI 子命令不同命名空间。

auth.ts:installOAuthTokens 与 authLogin

installOAuthTokens 是所有成功登录路径的汇聚点:

  1. performLogout({ clearOnboarding: false }) 清旧凭据
  2. getOauthProfileFromOauthToken 或 tokenAccount fallback → storeOAuthAccountInfo
  3. saveOAuthTokensIfNeeded + clearOAuthTokenCache
  4. fetchAndStoreUserRoles(失败仅 log)
  5. shouldUseClaudeAIAuth → firstTokenDate;否则 createAndStoreApiKey(Console 必须成功)
  6. clearAuthRelatedCaches()

authLogin 分支:

  • --console 与 --claudeai 互斥
  • forceLoginMethod / forceLoginOrgUUID 来自 enterprise settings
  • CLAUDE_CODE_OAUTH_REFRESH_TOKEN 环境变量快路径跳过浏览器
  • 否则走 OAuthService browser flow(文件后部)

auth status 等函数输出 buildAccountProperties JSON,供脚本消费。

源码引用: src/cli/handlers/auth.ts · 第 50–110 行(共 331 行)

  50| export async function installOAuthTokens(tokens: OAuthTokens): Promise<void> {
  51|   // Clear old state before saving new credentials
  52|   await performLogout({ clearOnboarding: false })
  53| 
  54|   // Reuse pre-fetched profile if available, otherwise fetch fresh
  55|   const profile =
  56|     tokens.profile ?? (await getOauthProfileFromOauthToken(tokens.accessToken))
  57|   if (profile) {
  58|     storeOAuthAccountInfo({
  59|       accountUuid: profile.account.uuid,
  60|       emailAddress: profile.account.email,
  61|       organizationUuid: profile.organization.uuid,
  62|       displayName: profile.account.display_name || undefined,
  63|       hasExtraUsageEnabled:
  64|         profile.organization.has_extra_usage_enabled ?? undefined,
  65|       billingType: profile.organization.billing_type ?? undefined,
  66|       subscriptionCreatedAt:
  67|         profile.organization.subscription_created_at ?? undefined,
  68|       accountCreatedAt: profile.account.created_at,
  69|     })
  70|   } else if (tokens.tokenAccount) {
  71|     // Fallback to token exchange account data when profile endpoint fails
  72|     storeOAuthAccountInfo({
  73|       accountUuid: tokens.tokenAccount.uuid,
  74|       emailAddress: tokens.tokenAccount.emailAddress,
  75|       organizationUuid: tokens.tokenAccount.organizationUuid,
  76|     })
  77|   }
  78| 
  79|   const storageResult = saveOAuthTokensIfNeeded(tokens)
  80|   clearOAuthTokenCache()
  81| 
  82|   if (storageResult.warning) {
  83|     logEvent('tengu_oauth_storage_warning', {
  84|       warning:
  85|         storageResult.warning as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  86|     })
  87|   }
  88| 
  89|   // Roles and first-token-date may fail for limited-scope tokens (e.g.
  90|   // inference-only from setup-token). They're not required for core auth.
  91|   await fetchAndStoreUserRoles(tokens.accessToken).catch(err =>
  92|     logForDebugging(String(err), { level: 'error' }),
  93|   )
  94| 
  95|   if (shouldUseClaudeAIAuth(tokens.scopes)) {
  96|     await fetchAndStoreClaudeCodeFirstTokenDate().catch(err =>
  97|       logForDebugging(String(err), { level: 'error' }),
  98|     )
  99|   } else {
 100|     // API key creation is critical for Console users — let it throw.
 101|     const apiKey = await createAndStoreApiKey(tokens.accessToken)
 102|     if (!apiKey) {
 103|       throw new Error(
 104|         'Unable to create API key. The server accepted the request but did not return a key.',
 105|       )
 106|     }
 107|   }
 108| 
 109|   await clearAuthRelatedCaches()
 110| }

源码引用: src/cli/handlers/auth.ts · 第 112–186 行(共 331 行)

 112| export async function authLogin({
 113|   email,
 114|   sso,
 115|   console: useConsole,
 116|   claudeai,
 117| }: {
 118|   email?: string
 119|   sso?: boolean
 120|   console?: boolean
 121|   claudeai?: boolean
 122| }): Promise<void> {
 123|   if (useConsole && claudeai) {
 124|     process.stderr.write(
 125|       'Error: --console and --claudeai cannot be used together.\n',
 126|     )
 127|     process.exit(1)
 128|   }
 129| 
 130|   const settings = getInitialSettings()
 131|   // forceLoginMethod is a hard constraint (enterprise setting) — matches ConsoleOAuthFlow behavior.
 132|   // Without it, --console selects Console; --claudeai (or no flag) selects claude.ai.
 133|   const loginWithClaudeAi = settings.forceLoginMethod
 134|     ? settings.forceLoginMethod === 'claudeai'
 135|     : !useConsole
 136|   const orgUUID = settings.forceLoginOrgUUID
 137| 
 138|   // Fast path: if a refresh token is provided via env var, skip the browser
 139|   // OAuth flow and exchange it directly for tokens.
 140|   const envRefreshToken = process.env.CLAUDE_CODE_OAUTH_REFRESH_TOKEN
 141|   if (envRefreshToken) {
 142|     const envScopes = process.env.CLAUDE_CODE_OAUTH_SCOPES
 143|     if (!envScopes) {
 144|       process.stderr.write(
 145|         'CLAUDE_CODE_OAUTH_SCOPES is required when using CLAUDE_CODE_OAUTH_REFRESH_TOKEN.\n' +
 146|           'Set it to the space-separated scopes the refresh token was issued with\n' +
 147|           '(e.g. "user:inference" or "user:profile user:inference user:sessions:claude_code user:mcp_servers").\n',
 148|       )
 149|       process.exit(1)
 150|     }
 151| 
 152|     const scopes = envScopes.split(/\s+/).filter(Boolean)
 153| 
 154|     try {
 155|       logEvent('tengu_login_from_refresh_token', {})
 156| 
 157|       const tokens = await refreshOAuthToken(envRefreshToken, { scopes })
 158|       await installOAuthTokens(tokens)
 159| 
 160|       const orgResult = await validateForceLoginOrg()
 161|       if (!orgResult.valid) {
 162|         process.stderr.write(orgResult.message + '\n')
 163|         process.exit(1)
 164|       }
 165| 
 166|       // Mark onboarding complete — interactive paths handle this via
 167|       // the Onboarding component, but the env var path skips it.
 168|       saveGlobalConfig(current => {
 169|         if (current.hasCompletedOnboarding) return current
 170|         return { ...current, hasCompletedOnboarding: true }
 171|       })
 172| 
 173|       logEvent('tengu_oauth_success', {
 174|         loginWithClaudeAi: shouldUseClaudeAIAuth(tokens.scopes),
 175|       })
 176|       process.stdout.write('Login successful.\n')
 177|       process.exit(0)
 178|     } catch (err) {
 179|       logError(err)
 180|       const sslHint = getSSLErrorHint(err)
 181|       process.stderr.write(
 182|         `Login failed: ${errorMessage(err)}\n${sslHint ? sslHint + '\n' : ''}`,
 183|       )
 184|       process.exit(1)
 185|     }
 186|   }

mcp.tsx:serve、remove 与 Ink 对话框

mcpServeHandler:

  • stat(cwd) 校验目录
  • dynamic import setup + entrypoints/mcp.startMCPServer
  • 失败 cliError

mcpRemoveHandler:

  • 按 scope 或自动探测 local/project/user 删除
  • sse/http 类型清理 secure storage(clearServerTokensFromLocalStorage)
  • 成功 cliOk 并打印 modified config 路径

Ink 路径(文件后部):MCPServerDesktopImportDialog 用于从 desktop 导入 MCP 配置;依赖 AppStateProvider、KeybindingSetup。

checkMcpServerHealth 并行 connectToServer 显示 ✓ / ! / ✗ 状态,供 list 子命令。

handlers 从 services/mcp/config、client、auth 读写的 scope 与 REPL 相同数据源。

源码引用: src/cli/handlers/mcp.tsx · 第 26–39 行(共 456 行)

  26|   getMcpServerConnectionBatchSize,
  27| } from '../../services/mcp/client.js'
  28| import {
  29|   addMcpConfig,
  30|   getAllMcpConfigs,
  31|   getMcpConfigByName,
  32|   getMcpConfigsByScope,
  33|   removeMcpConfig,
  34| } from '../../services/mcp/config.js'
  35| import type {
  36|   ConfigScope,
  37|   ScopedMcpServerConfig,
  38| } from '../../services/mcp/types.js'
  39| import {

源码引用: src/cli/handlers/mcp.tsx · 第 42–71 行(共 456 行)

  42|   getScopeLabel,
  43| } from '../../services/mcp/utils.js'
  44| import { AppStateProvider } from '../../state/AppState.js'
  45| import {
  46|   getCurrentProjectConfig,
  47|   getGlobalConfig,
  48|   saveCurrentProjectConfig,
  49| } from '../../utils/config.js'
  50| import { isFsInaccessible } from '../../utils/errors.js'
  51| import { gracefulShutdown } from '../../utils/gracefulShutdown.js'
  52| import { safeParseJSON } from '../../utils/json.js'
  53| import { getPlatform } from '../../utils/platform.js'
  54| import { cliError, cliOk } from '../exit.js'
  55| 
  56| async function checkMcpServerHealth(
  57|   name: string,
  58|   server: ScopedMcpServerConfig,
  59| ): Promise<string> {
  60|   try {
  61|     const result = await connectToServer(name, server)
  62|     if (result.type === 'connected') {
  63|       return '✓ Connected'
  64|     } else if (result.type === 'needs-auth') {
  65|       return '! Needs authentication'
  66|     } else {
  67|       return '✗ Failed to connect'
  68|     }
  69|   } catch (_error) {
  70|     return '✗ Connection error'
  71|   }

源码引用: src/cli/handlers/mcp.tsx · 第 74–120 行(共 456 行)

  74| // mcp serve (lines 4512–4532)
  75| export async function mcpServeHandler({
  76|   debug,
  77|   verbose,
  78| }: {
  79|   debug?: boolean
  80|   verbose?: boolean
  81| }): Promise<void> {
  82|   const providedCwd = cwd()
  83|   logEvent('tengu_mcp_start', {})
  84| 
  85|   try {
  86|     await stat(providedCwd)
  87|   } catch (error) {
  88|     if (isFsInaccessible(error)) {
  89|       cliError(`Error: Directory ${providedCwd} does not exist`)
  90|     }
  91|     throw error
  92|   }
  93| 
  94|   try {
  95|     const { setup } = await import('../../setup.js')
  96|     await setup(providedCwd, 'default', false, false, undefined, false)
  97|     const { startMCPServer } = await import('../../entrypoints/mcp.js')
  98|     await startMCPServer(providedCwd, debug ?? false, verbose ?? false)
  99|   } catch (error) {
 100|     cliError(`Error: Failed to start MCP server: ${error}`)
 101|   }
 102| }
 103| 
 104| // mcp remove (lines 4545–4635)
 105| export async function mcpRemoveHandler(
 106|   name: string,
 107|   options: { scope?: string },
 108| ): Promise<void> {
 109|   // Look up config before removing so we can clean up secure storage
 110|   const serverBeforeRemoval = getMcpConfigByName(name)
 111| 
 112|   const cleanupSecureStorage = () => {
 113|     if (
 114|       serverBeforeRemoval &&
 115|       (serverBeforeRemoval.type === 'sse' ||
 116|         serverBeforeRemoval.type === 'http')
 117|     ) {
 118|       clearServerTokensFromLocalStorage(name, serverBeforeRemoval)
 119|       clearMcpClientConfig(name, serverBeforeRemoval)
 120|     }

plugins.ts:marketplace 与 validate

plugins.ts 是最大 handler(800+ 行),覆盖:

  • install / uninstall / enable / disable / update
  • marketplace add / remove / refresh / list
  • plugin validate(manifest + 可选 content files)
  • handleMarketplaceError → logError + cliError

printValidationResult 用 figures 符号打印 errors/warnings 列表。

pluginValidateHandler:

  • --cowork → setUseCoworkPlugins(true)
  • validateManifest;若路径在 .claude-plugin 内则 validatePluginContents
  • 失败 process.exit(1)(非 cliError 的历史路径)

re-export VALID_INSTALLABLE_SCOPES / VALID_UPDATE_SCOPES 供 cli.tsx 选项校验。插件逻辑委托 services/plugins/pluginCliCommands 与 utils/plugins/*。

源码引用: src/cli/handlers/plugins.ts · 第 62–98 行(共 879 行)

  62| // Re-export for main.tsx to reference in option definitions
  63| export { VALID_INSTALLABLE_SCOPES, VALID_UPDATE_SCOPES }
  64| 
  65| /**
  66|  * Helper function to handle marketplace command errors consistently.
  67|  */
  68| export function handleMarketplaceError(error: unknown, action: string): never {
  69|   logError(error)
  70|   cliError(`${figures.cross} Failed to ${action}: ${errorMessage(error)}`)
  71| }
  72| 
  73| function printValidationResult(result: ValidationResult): void {
  74|   if (result.errors.length > 0) {
  75|     // biome-ignore lint/suspicious/noConsole:: intentional console output
  76|     console.log(
  77|       `${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, 'error')}:\n`,
  78|     )
  79|     result.errors.forEach(error => {
  80|       // biome-ignore lint/suspicious/noConsole:: intentional console output
  81|       console.log(`  ${figures.pointer} ${error.path}: ${error.message}`)
  82|     })
  83|     // biome-ignore lint/suspicious/noConsole:: intentional console output
  84|     console.log('')
  85|   }
  86|   if (result.warnings.length > 0) {
  87|     // biome-ignore lint/suspicious/noConsole:: intentional console output
  88|     console.log(
  89|       `${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, 'warning')}:\n`,
  90|     )
  91|     result.warnings.forEach(warning => {
  92|       // biome-ignore lint/suspicious/noConsole:: intentional console output
  93|       console.log(`  ${figures.pointer} ${warning.path}: ${warning.message}`)
  94|     })
  95|     // biome-ignore lint/suspicious/noConsole:: intentional console output
  96|     console.log('')
  97|   }
  98| }

源码引用: src/cli/handlers/plugins.ts · 第 100–120 行(共 879 行)

 100| // plugin validate
 101| export async function pluginValidateHandler(
 102|   manifestPath: string,
 103|   options: { cowork?: boolean },
 104| ): Promise<void> {
 105|   if (options.cowork) setUseCoworkPlugins(true)
 106|   try {
 107|     const result = await validateManifest(manifestPath)
 108| 
 109|     // biome-ignore lint/suspicious/noConsole:: intentional console output
 110|     console.log(`Validating ${result.fileType} manifest: ${result.filePath}\n`)
 111|     printValidationResult(result)
 112| 
 113|     // If this is a plugin manifest located inside a .claude-plugin directory,
 114|     // also validate the plugin's content files (skills, agents, commands,
 115|     // hooks). Works whether the user passed a directory or the plugin.json
 116|     // path directly.
 117|     let contentResults: ValidationResult[] = []
 118|     if (result.fileType === 'plugin') {
 119|       const manifestDir = dirname(result.filePath)
 120|       if (basename(manifestDir) === '.claude-plugin') {

agents.ts:分组列表与 shadow 提示

agentsHandler 只读、无 exit 包装(正常 return):

  1. getAgentDefinitionsWithOverrides(cwd)
  2. getActiveAgentsFromList + resolveAgentOverrides
  3. 按 AGENT_SOURCE_GROUPS(built-in / user / project 等)分组
  4. overriddenBy 显示 shadowed by {winnerSource}
  5. formatAgent:agentType · model · memory

输出示例:

3 active agents

Built-in:
  Explore · sonnet
User:
  (shadowed by project) MyAgent · opus

数据来自 tools/AgentTool/loadAgentsDir 与 agentDisplay,与 AgentTool 运行时同一解析链。

源码引用: src/cli/handlers/agents.ts · 第 20–70 行(共 71 行)

  20| function formatAgent(agent: ResolvedAgent): string {
  21|   const model = resolveAgentModelDisplay(agent)
  22|   const parts = [agent.agentType]
  23|   if (model) {
  24|     parts.push(model)
  25|   }
  26|   if (agent.memory) {
  27|     parts.push(`${agent.memory} memory`)
  28|   }
  29|   return parts.join(' · ')
  30| }
  31| 
  32| export async function agentsHandler(): Promise<void> {
  33|   const cwd = getCwd()
  34|   const { allAgents } = await getAgentDefinitionsWithOverrides(cwd)
  35|   const activeAgents = getActiveAgentsFromList(allAgents)
  36|   const resolvedAgents = resolveAgentOverrides(allAgents, activeAgents)
  37| 
  38|   const lines: string[] = []
  39|   let totalActive = 0
  40| 
  41|   for (const { label, source } of AGENT_SOURCE_GROUPS) {
  42|     const groupAgents = resolvedAgents
  43|       .filter(a => a.source === source)
  44|       .sort(compareAgentsByName)
  45| 
  46|     if (groupAgents.length === 0) continue
  47| 
  48|     lines.push(`${label}:`)
  49|     for (const agent of groupAgents) {
  50|       if (agent.overriddenBy) {
  51|         const winnerSource = getOverrideSourceLabel(agent.overriddenBy)
  52|         lines.push(`  (shadowed by ${winnerSource}) ${formatAgent(agent)}`)
  53|       } else {
  54|         lines.push(`  ${formatAgent(agent)}`)
  55|         totalActive++
  56|       }
  57|     }
  58|     lines.push('')
  59|   }
  60| 
  61|   if (lines.length === 0) {
  62|     // biome-ignore lint/suspicious/noConsole:: intentional console output
  63|     console.log('No agents found.')
  64|   } else {
  65|     // biome-ignore lint/suspicious/noConsole:: intentional console output
  66|     console.log(`${totalActive} active agents\n`)
  67|     // biome-ignore lint/suspicious/noConsole:: intentional console output
  68|     console.log(lines.join('\n').trimEnd())
  69|   }
  70| }

autoMode.ts:defaults、config、critique

autoModeDefaultsHandler — jsonStringify(getDefaultExternalAutoModeRules())

autoModeConfigHandler — 合并用户 settings 与 defaults,REPLACE 语义:非空 user section 整段替换该 section defaults(与 buildYoloSystemPrompt 一致)

autoModeCritiqueHandler:

  • 无 custom rules 时提示运行 auto-mode defaults
  • sideQuery + CRITIQUE_SYSTEM_PROMPT 评审 allow/soft_deny/environment 规则清晰度
  • 可选 --model 覆盖 getMainLoopModel()

用于用户调试 YOLO classifier 配置,不影响 REPL 内 classifier 运行时(那在 utils/permissions/yoloClassifier)。

源码引用: src/cli/handlers/autoMode.ts · 第 24–47 行(共 171 行)

  24| export function autoModeDefaultsHandler(): void {
  25|   writeRules(getDefaultExternalAutoModeRules())
  26| }
  27| 
  28| /**
  29|  * Dump the effective auto mode config: user settings where provided, external
  30|  * defaults otherwise. Per-section REPLACE semantics — matches how
  31|  * buildYoloSystemPrompt resolves the external template (a non-empty user
  32|  * section replaces that section's defaults entirely; an empty/absent section
  33|  * falls through to defaults).
  34|  */
  35| export function autoModeConfigHandler(): void {
  36|   const config = getAutoModeConfig()
  37|   const defaults = getDefaultExternalAutoModeRules()
  38|   writeRules({
  39|     allow: config?.allow?.length ? config.allow : defaults.allow,
  40|     soft_deny: config?.soft_deny?.length
  41|       ? config.soft_deny
  42|       : defaults.soft_deny,
  43|     environment: config?.environment?.length
  44|       ? config.environment
  45|       : defaults.environment,
  46|   })
  47| }

源码引用: src/cli/handlers/autoMode.ts · 第 73–120 行(共 171 行)

  73| export async function autoModeCritiqueHandler(options: {
  74|   model?: string
  75| }): Promise<void> {
  76|   const config = getAutoModeConfig()
  77|   const hasCustomRules =
  78|     (config?.allow?.length ?? 0) > 0 ||
  79|     (config?.soft_deny?.length ?? 0) > 0 ||
  80|     (config?.environment?.length ?? 0) > 0
  81| 
  82|   if (!hasCustomRules) {
  83|     process.stdout.write(
  84|       'No custom auto mode rules found.\n\n' +
  85|         'Add rules to your settings file under autoMode.{allow, soft_deny, environment}.\n' +
  86|         'Run `claude auto-mode defaults` to see the default rules for reference.\n',
  87|     )
  88|     return
  89|   }
  90| 
  91|   const model = options.model
  92|     ? parseUserSpecifiedModel(options.model)
  93|     : getMainLoopModel()
  94| 
  95|   const defaults = getDefaultExternalAutoModeRules()
  96|   const classifierPrompt = buildDefaultExternalSystemPrompt()
  97| 
  98|   const userRulesSummary =
  99|     formatRulesForCritique('allow', config?.allow ?? [], defaults.allow) +
 100|     formatRulesForCritique(
 101|       'soft_deny',
 102|       config?.soft_deny ?? [],
 103|       defaults.soft_deny,
 104|     ) +
 105|     formatRulesForCritique(
 106|       'environment',
 107|       config?.environment ?? [],
 108|       defaults.environment,
 109|     )
 110| 
 111|   process.stdout.write('Analyzing your auto mode rules…\n\n')
 112| 
 113|   let response
 114|   try {
 115|     response = await sideQuery({
 116|       querySource: 'auto_mode_critique',
 117|       model,
 118|       system: CRITIQUE_SYSTEM_PROMPT,
 119|       skipSystemPromptPrefix: true,
 120|       max_tokens: 4096,

exit.ts 在 handlers 中的用法

mcp.tsx、plugins.ts 大量 return cliError(...) / cliOk(...):

  • cliError:可选 stderr 消息 + exit(1) + never 返回
  • cliOk:stdout 消息 + exit(0)

对比 auth.ts 仍用 process.exit 的路径——新 handler 应统一 exit.ts,便于测试 spy。

plugins handleMarketplaceError 模式:logError(error) 然后 cliError 带 figures.cross 前缀,用户看到一致错误格式。

源码引用: src/cli/exit.ts · 第 18–31 行(共 32 行)

  18| /** Write an error message to stderr (if given) and exit with code 1. */
  19| export function cliError(msg?: string): never {
  20|   // biome-ignore lint/suspicious/noConsole: centralized CLI error output
  21|   if (msg) console.error(msg)
  22|   process.exit(1)
  23|   return undefined as never
  24| }
  25| 
  26| /** Write a message to stdout (if given) and exit with code 0. */
  27| export function cliOk(msg?: string): never {
  28|   if (msg) process.stdout.write(msg + '\n')
  29|   process.exit(0)
  30|   return undefined as never
  31| }

源码引用: src/cli/handlers/plugins.ts · 第 68–71 行(共 879 行)

  68| export function handleMarketplaceError(error: unknown, action: string): never {
  69|   logError(error)
  70|   cliError(`${figures.cross} Failed to ${action}: ${errorMessage(error)}`)
  71| }

handlers/util.tsx

handlers/util.tsx 提供 mcp/plugin 子命令共享的格式化、scope 解析、表格输出等小函数(具体导出随版本增减)。阅读 mcp.tsx 顶部 import 可获知当前依赖项。

改 handler 时若发现 cli.tsx 与 handler 选项不同步,以 cli.tsx Commander 定义为准,handler 只消费已解析的 opts 对象。

源码目录

入口对照:entrypoints/cli.tsx 搜索 handlers/ dynamic import。服务层:services/oauth/、services/mcp/、services/plugins/。

动手练习

  1. 运行 claude agents,对照 AGENT_SOURCE_GROUPS 源码理解分组
  2. claude auto-mode config | jq 查看合并后 rules
  3. 追踪 mcp remove 如何决定 single vs multi scope 交互
  4. 比较 auth login 的 env refresh token 路径与 browser 路径的 installOAuthTokens 汇合点

本章小结与延伸

handlers 是 CLI 子命令的业务体。入口与 Commander 树见 entrypoints/cli.tsx(P5)。 继续学习:

  • cli 模块总览
  • exit.ts(Structured IO 章)
Prev
CLI Transports · Session Ingress 传输层
Next
Update & Upload · 自更新与串行上传原语