本章总览
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 客户端是不同进程角色。
建议学习步骤
- 阅读源码块 A:installOAuthTokens 与 authLogin 入口
- 阅读源码块 B:mcpServeHandler 与 mcpRemoveHandler
- 阅读源码块 C:plugins handleMarketplaceError 与 validate
- 阅读源码块 D:agentsHandler 分组输出
- 阅读源码块 E:autoModeConfigHandler 与 critique
- 在 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.ts | claude auth login/logout/status | OAuth、API key、JSON status |
| mcp.tsx | claude mcp * | 部分 Ink UI;serve 启 MCP server |
| plugins.ts | claude plugin * / marketplace * | 安装、validate、refresh |
| agents.ts | claude agents | 只读列表 |
| autoMode.ts | claude auto-mode * | JSON dump + LLM critique |
| util.tsx | (内部) | handler 共享辅助 |
边界:REPL 内 /commands 注册在 src/commands/,与 CLI 子命令不同命名空间。
auth.ts:installOAuthTokens 与 authLogin
installOAuthTokens 是所有成功登录路径的汇聚点:
- performLogout({ clearOnboarding: false }) 清旧凭据
- getOauthProfileFromOauthToken 或 tokenAccount fallback → storeOAuthAccountInfo
- saveOAuthTokensIfNeeded + clearOAuthTokenCache
- fetchAndStoreUserRoles(失败仅 log)
- shouldUseClaudeAIAuth → firstTokenDate;否则 createAndStoreApiKey(Console 必须成功)
- 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):
- getAgentDefinitionsWithOverrides(cwd)
- getActiveAgentsFromList + resolveAgentOverrides
- 按 AGENT_SOURCE_GROUPS(built-in / user / project 等)分组
- overriddenBy 显示 shadowed by {winnerSource}
- 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/。
动手练习
- 运行 claude agents,对照 AGENT_SOURCE_GROUPS 源码理解分组
- claude auto-mode config | jq 查看合并后 rules
- 追踪 mcp remove 如何决定 single vs multi scope 交互
- 比较 auth login 的 env refresh token 路径与 browser 路径的 installOAuthTokens 汇合点
本章小结与延伸
handlers 是 CLI 子命令的业务体。入口与 Commander 树见 entrypoints/cli.tsx(P5)。 继续学习: