@luchenglong/cli-bridge 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/PROVIDER-CONTRACT.md +53 -4
- package/README.md +12 -11
- package/docs/HANDOVER.md +9 -0
- package/docs/audits/2026-10-18-preset-0.2.1-contract-fix.md +160 -0
- package/docs/audits/README.md +15 -0
- package/dynamic/agy.host.js +1 -1
- package/dynamic/claude.host.js +1801 -0
- package/dynamic/claude.host.template.mjs +108 -0
- package/dynamic/codebuddy.host.js +1 -1
- package/dynamic/codex.host.js +1955 -0
- package/dynamic/codex.host.template.mjs +137 -0
- package/dynamic/mimo.host.js +1 -1
- package/dynamic/opencode.host.js +1 -1
- package/home-plugin/cli-bridge-indicator/lib/client.js +600 -606
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +87 -9
- package/mcp/cli-bridge-mcp.mjs +6 -4
- package/mcp/providers/claude.mjs +231 -0
- package/mcp/providers/codex.mjs +239 -0
- package/package.json +10 -6
- package/preset/agy-first/agent.cordis.yml +0 -5
- package/preset/agy-first/plugin-list.mjs +4 -1
- package/preset/claude-first/agent.cordis.yml +136 -0
- package/preset/claude-first/cli-bridge.mjs +131 -0
- package/preset/claude-first/plugin-list.mjs +28 -0
- package/preset/claude-first/preset.yml +3 -0
- package/preset/codebuddy-first/agent.cordis.yml +0 -5
- package/preset/codex-first/agent.cordis.yml +136 -0
- package/preset/codex-first/cli-bridge.mjs +160 -0
- package/preset/codex-first/plugin-list.mjs +28 -0
- package/preset/codex-first/preset.yml +3 -0
- package/preset/mimo-first/agent.cordis.yml +1 -7
- package/preset/mimo-first/plugin-list.mjs +18 -12
- package/preset/mimo-member/plugin-list.mjs +49 -0
- package/preset/opencode-first/agent.cordis.yml +0 -5
- package/preset/opencode-first/plugin-list.mjs +4 -2
- package/preset/standard-base.mjs +18 -6
- package/providers/claude-exe.mjs +112 -0
- package/providers/claude-models.mjs +334 -0
- package/providers/claude-policy.mjs +37 -0
- package/providers/claude.mjs +508 -0
- package/providers/codex-cli.mjs +471 -0
- package/providers/codex-models.mjs +107 -0
- package/providers/codex.mjs +544 -0
- package/providers/registry.mjs +25 -1
- package/scripts/build.mjs +1 -2
- package/scripts/verify.mjs +8 -2
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
// providers/claude.mjs — claude provider descriptor(Claude Code CLI;本仓第五家 CLI)。
|
|
2
|
+
//
|
|
3
|
+
// 基线 = 本机实测 Claude Code 2.0.76(`claude` = 原生 claude.exe),端点被用户指向第三方
|
|
4
|
+
// Anthropic 兼容服务(~/.claude/settings.json 的 env: ANTHROPIC_BASE_URL/AUTH_TOKEN +
|
|
5
|
+
// ANTHROPIC_DEFAULT_*_MODEL 别名映射)。写代码时最容易踩的几条事实:
|
|
6
|
+
// • 非交互 = `claude -p "<prompt>"`,prompt 是**单参数**(不是位置参数流,也不是 stdin)。
|
|
7
|
+
// • `--output-format json` → stdout 单行 result 事件;`stream-json` 必须配 `--verbose`,
|
|
8
|
+
// 实测 4 行:system(init) / assistant(thinking) / assistant(text) / result。
|
|
9
|
+
// • ⚠️ 成功判定陷阱:result 事件 `is_error:true` 时退出码**可能是 0 也可能 1**
|
|
10
|
+
// (实测无效模型 = exit 1,但 stdout 仍是合法 JSON 且 `subtype` 竟然还是 "success")。
|
|
11
|
+
// 故 buildResult 只认 `is_error` + `subtype`,绝不只看 exit code。
|
|
12
|
+
// • 续聊 = `--resume <session_id>`;`-c/--continue` 续最近一次。session_id 来自
|
|
13
|
+
// result 事件(json/stream-json 都有)。
|
|
14
|
+
// • 权限:`--permission-mode plan|acceptEdits|...` 或 `--dangerously-skip-permissions`;
|
|
15
|
+
// -p 模式没有 TTY,任何需要交互批准的路径都必须走 bypass,否则会挂到超时。
|
|
16
|
+
//
|
|
17
|
+
// 红线(PROVIDER-CONTRACT.md §8 + §1):
|
|
18
|
+
// - 本文件会被 build 内联进 dynamic 沙箱(new Function):绝不出现 import.meta 字面量;
|
|
19
|
+
// process 只经 deps 注入或 typeof 守卫读取;宿主能力全部走 deps/attach(hostOpts)。
|
|
20
|
+
// - 只允许 import '../core/bridge-kit.mjs' 与同目录片文件(claude-exe / claude-models)。
|
|
21
|
+
// - 模型清单只认服务端权威探测;拿不到 = ok:false + 可操作错误(绝不静默空数组/配置冒充)。
|
|
22
|
+
import { createStatusEngine, createRunner, clampInt, isLimited } from '../core/bridge-kit.mjs'
|
|
23
|
+
import { claudeExeCandidates, claudePathHint, resolveExePrefix, splitExeSpec, CLAUDE_BIN_ENV_VARS } from './claude-exe.mjs'
|
|
24
|
+
import { PROBE_MODEL, parseSupportedModels, probeSupportedModels, probeSupportedModelsSyncCached, readConfiguredModels, listAccountModels, resetModelsCache } from './claude-models.mjs'
|
|
25
|
+
// 单行 re-export(build 内联时整行剥离;多行形态风险见 build.mjs 的 stripModuleSyntax 注释)。
|
|
26
|
+
export { claudeExeCandidates, claudePathHint, resolveExePrefix, splitExeSpec, PROBE_MODEL, parseSupportedModels, probeSupportedModels, readConfiguredModels, listAccountModels, resetModelsCache }
|
|
27
|
+
|
|
28
|
+
function nowIso() { return new Date().toISOString() }
|
|
29
|
+
|
|
30
|
+
// 权限模式的用户可见取值(auto 由 DSH plan 状态决定;其余直通 CLI)。
|
|
31
|
+
const MODE_ENUM = ['auto', 'plan', 'accept-edits', 'bypass']
|
|
32
|
+
|
|
33
|
+
// ── 三工具 schema ─────────────────────────────────────────────────────────────
|
|
34
|
+
const RUN_PARAMS = {
|
|
35
|
+
type: 'object',
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
required: ['prompt'],
|
|
38
|
+
properties: {
|
|
39
|
+
prompt: { type: 'string', description: 'The task instruction for claude. On first dispatch for a topic, include a compact CONTEXT preamble (goal, repo root, key paths, decisions/constraints; point at paths, do not paste file contents — claude reads the repo itself). For follow-ups on the same topic, use claude_continue with latest=true or sessionId (claude keeps session context; state only what is new).' },
|
|
40
|
+
mode: { type: 'string', enum: MODE_ENUM, description: 'Permission mode. auto (default) follows the DSH plan state: plan when the session is in plan mode, otherwise bypass (permissions auto-approved so claude never blocks). plan = read-only planning; accept-edits = allow file edits but still ask for risky commands; bypass = --dangerously-skip-permissions.' },
|
|
41
|
+
model: { type: 'string', description: 'Optional model id or alias (e.g. "sonnet", "opus", or a full id). Which ids actually work is decided by the endpoint, not by this bridge — call claude_models for the authoritative server-side list. Omit to use the model chosen in CLI Bridge settings, else claude\'s own configuration.' },
|
|
42
|
+
maxBudgetUsd: { type: 'number', description: 'Optional hard cap on API spend for this run (passes --max-budget-usd).' },
|
|
43
|
+
cwd: { type: 'string', description: 'Working directory for claude. Defaults to the DSH workspace root.' },
|
|
44
|
+
addDirs: { type: 'array', items: { type: 'string' }, description: 'Extra directories to allow tool access to (--add-dir).' },
|
|
45
|
+
allowedTools: { type: 'array', items: { type: 'string' }, description: 'Optional allow-list of tool names (--allowedTools).' },
|
|
46
|
+
disallowedTools: { type: 'array', items: { type: 'string' }, description: 'Optional deny-list of tool names (--disallowedTools).' },
|
|
47
|
+
systemPrompt: { type: 'string', description: 'Optional replacement system prompt (--system-prompt).' },
|
|
48
|
+
appendSystemPrompt: { type: 'string', description: 'Optional system prompt appended to the default one (--append-system-prompt).' },
|
|
49
|
+
forkSession: { type: 'boolean', description: 'When resuming, create a new session id instead of reusing the original (--fork-session).' },
|
|
50
|
+
timeoutSec: { type: 'integer', description: 'Execution timeout seconds (10-3600, default 600).' },
|
|
51
|
+
background: { type: 'boolean', description: 'Defaults to true: dispatch returns a jobId immediately; collect with job_output, watch progress with claude_status. Set false to block for the final answer.' }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const CONTINUE_PARAMS = {
|
|
55
|
+
type: 'object',
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
required: ['prompt'],
|
|
58
|
+
properties: {
|
|
59
|
+
prompt: { type: 'string', description: 'Follow-up instruction for the ongoing claude session. State only what is new; claude preserves its session context.' },
|
|
60
|
+
sessionId: { type: 'string', description: 'claude session id to resume (from a prior claude_run result; passes --resume).' },
|
|
61
|
+
latest: { type: 'boolean', description: 'Continue the most recent claude conversation instead of a specific id (--continue).' },
|
|
62
|
+
mode: { type: 'string', enum: MODE_ENUM, description: 'Permission mode; default auto.' },
|
|
63
|
+
model: { type: 'string', description: 'Optional model id or alias.' },
|
|
64
|
+
maxBudgetUsd: { type: 'number', description: 'Optional hard cap on API spend for this run.' },
|
|
65
|
+
cwd: { type: 'string', description: 'Working directory for claude.' },
|
|
66
|
+
forkSession: { type: 'boolean', description: 'Create a new session id instead of reusing the original (--fork-session).' },
|
|
67
|
+
timeoutSec: { type: 'integer', description: 'Execution timeout seconds (10-3600, default 600).' },
|
|
68
|
+
background: { type: 'boolean', description: 'Defaults to true: dispatch returns a jobId immediately; collect with job_output. Set false to block for the final answer.' }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const STATUS_PARAMS = { type: 'object', additionalProperties: false, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
|
|
72
|
+
|
|
73
|
+
// claude_models(附加工具):服务端权威模型清单(探测 400 报文,绝不用配置冒充)。
|
|
74
|
+
const MODELS_PARAMS = {
|
|
75
|
+
type: 'object',
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
required: [],
|
|
78
|
+
properties: {
|
|
79
|
+
refresh: { type: 'boolean', description: 'Bypass the 5-minute probe cache and ask the endpoint again.' },
|
|
80
|
+
timeoutMs: { type: 'integer', description: 'Probe timeout in ms (5000-90000, default 30000).' }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const MODELS_DESCRIPTION = 'List the models the current claude account/endpoint really supports, read LIVE from the server. Mechanism: the CLI is run once with a deliberately invalid model id, which makes the endpoint answer HTTP 400 with "The supported API model names are X, Y" — that roster is authoritative for this endpoint+key. Returns { ok, source:"account-live", models:[{id, configuredAs}], count }. The ANTHROPIC_*_MODEL entries in ~/.claude/settings.json are alias mappings, NOT the full supported set, so they are only returned as a diagnostic hint (never as models). If no live roster can be read this returns ok:false with an actionable error — it never invents a list.'
|
|
84
|
+
|
|
85
|
+
// 渲染函数签名遵循 harness 契约 render(args, value)(extraTools[].render 同签名,
|
|
86
|
+
// preset/dynamic/MCP 三处都按两参数调用 —— 只写一个参数会渲染出空壳)。
|
|
87
|
+
function renderModels(args, value) {
|
|
88
|
+
const v = value || {}
|
|
89
|
+
if (!v.ok) {
|
|
90
|
+
const lines = ['claude_models failed: ' + String(v.error || 'unknown')]
|
|
91
|
+
const cfg = v.diagnostic && Array.isArray(v.diagnostic.configured) ? v.diagnostic.configured : []
|
|
92
|
+
if (cfg.length) {
|
|
93
|
+
lines.push('configured aliases (NOT authoritative): ' + cfg.map((m) => m.id).join(', '))
|
|
94
|
+
if (v.diagnostic && v.diagnostic.settingsPath) lines.push(' from ' + v.diagnostic.settingsPath)
|
|
95
|
+
}
|
|
96
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
97
|
+
}
|
|
98
|
+
const list = Array.isArray(v.models) ? v.models : []
|
|
99
|
+
if (!list.length) return [{ type: 'text', text: 'claude_models: server returned an empty roster' }]
|
|
100
|
+
const lines = ['claude models (' + list.length + ') [source=' + (v.source || 'account-live') + ']:']
|
|
101
|
+
for (const m of list.slice(0, 40)) lines.push(' ' + m.id + (m.configuredAs ? ' [configured as ' + m.configuredAs + ']' : ''))
|
|
102
|
+
if (list.length > 40) lines.push(' … and ' + (list.length - 40) + ' more')
|
|
103
|
+
if (v.diagnostic && v.diagnostic.settingsPath) lines.push('settings: ' + v.diagnostic.settingsPath)
|
|
104
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// usage 汇总:input/output + 两个 cache 计数(claude 的 input_tokens 不含 cache 命中)。
|
|
108
|
+
function sumTokens(usage) {
|
|
109
|
+
if (!usage || typeof usage !== 'object') return null
|
|
110
|
+
let total = 0
|
|
111
|
+
let any = false
|
|
112
|
+
for (const k of ['input_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens', 'output_tokens']) {
|
|
113
|
+
if (typeof usage[k] === 'number' && Number.isFinite(usage[k])) { total += usage[k]; any = true }
|
|
114
|
+
}
|
|
115
|
+
return any ? total : null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 本次实际跑的主模型:modelUsage 里按 costUSD(再退 token 量)挑最大者。
|
|
119
|
+
function pickModelUsed(modelUsage) {
|
|
120
|
+
if (!modelUsage || typeof modelUsage !== 'object') return null
|
|
121
|
+
let best = null
|
|
122
|
+
let bestScore = -1
|
|
123
|
+
for (const k of Object.keys(modelUsage)) {
|
|
124
|
+
const u = modelUsage[k] && typeof modelUsage[k] === 'object' ? modelUsage[k] : {}
|
|
125
|
+
const score = (Number(u.costUSD) || 0) * 1e6 + (Number(u.inputTokens) || 0) + (Number(u.outputTokens) || 0)
|
|
126
|
+
if (score > bestScore) { bestScore = score; best = k }
|
|
127
|
+
}
|
|
128
|
+
return best
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default function defineProvider(deps = {}) {
|
|
132
|
+
const env = deps.env || {}
|
|
133
|
+
let host = null
|
|
134
|
+
let settingsRef = { binPath: '', defaultModel: '', maxBudgetUsd: '' }
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 当前生效设置(面板改完即时生效,无需重启)。三源合并,后者覆盖前者:
|
|
138
|
+
* ① settingsRef(attach 时快照/缺省)② deps.readSettings(MCP 形态现读文件)
|
|
139
|
+
* ③ host.getSettings('claude')(preset/dynamic 形态)。
|
|
140
|
+
*/
|
|
141
|
+
function currentSettings() {
|
|
142
|
+
const out = Object.assign({}, settingsRef)
|
|
143
|
+
try {
|
|
144
|
+
if (typeof deps.readSettings === 'function') {
|
|
145
|
+
const r = deps.readSettings()
|
|
146
|
+
if (r && typeof r === 'object') {
|
|
147
|
+
const part = ('partition' in r || 'path' in r || 'home' in r) ? r.partition : r
|
|
148
|
+
if (part && typeof part === 'object') Object.assign(out, part)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch (e) { }
|
|
152
|
+
try {
|
|
153
|
+
if (host && typeof host.getSettings === 'function') {
|
|
154
|
+
const s = host.getSettings('claude')
|
|
155
|
+
if (s && typeof s === 'object') Object.assign(out, s)
|
|
156
|
+
}
|
|
157
|
+
} catch (e) { }
|
|
158
|
+
return out
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const descriptor = {
|
|
162
|
+
id: 'claude', label: 'CLAUDE', presetId: 'claude-first', defaultBackend: null, defaultMode: 'auto',
|
|
163
|
+
exeName: 'claude', binEnvVars: CLAUDE_BIN_ENV_VARS, unavailableStatus: 'CLAUDE_UNAVAILABLE',
|
|
164
|
+
|
|
165
|
+
// 单后端:模型/凭据由 claude 自身配置(~/.claude/settings.json)决定,桥不引入第二后端。
|
|
166
|
+
backends() { return null },
|
|
167
|
+
normalizeBackend() { return null },
|
|
168
|
+
resolveBackend(args, target, preferred) { return null },
|
|
169
|
+
|
|
170
|
+
// 实测 flag 集(Claude Code 2.0.76 `claude --help`)。
|
|
171
|
+
buildArgv(exePrefix, args, opts) {
|
|
172
|
+
const o = opts || {}
|
|
173
|
+
const argv = Array.isArray(exePrefix) ? exePrefix.slice() : [String(exePrefix)]
|
|
174
|
+
argv.push('-p', String(args.prompt))
|
|
175
|
+
// stream-json 才能实时折叠步骤(foldStreamEvent 吃 system/assistant/result 事件);
|
|
176
|
+
// stream=false 时退单行 json(更省字节,仍可解析)。
|
|
177
|
+
const stream = args.stream !== false
|
|
178
|
+
if (stream) argv.push('--output-format', 'stream-json', '--verbose')
|
|
179
|
+
else argv.push('--output-format', 'json')
|
|
180
|
+
if (stream && args.includePartialMessages) argv.push('--include-partial-messages')
|
|
181
|
+
const timeoutSec = clampInt(args.timeoutSec, 600, 10, 3600)
|
|
182
|
+
|
|
183
|
+
// 权限:auto → 跟随 DSH plan 状态;plan 用 CLI 的 plan 模式,其余走 bypass
|
|
184
|
+
//(-p 没有 TTY,任何交互批准都会挂到超时 —— 与 agy/codebuddy/opencode 同语义)。
|
|
185
|
+
let mode = String(args.mode || descriptor.defaultMode || 'auto')
|
|
186
|
+
if (MODE_ENUM.indexOf(mode) < 0) mode = 'auto'
|
|
187
|
+
if (mode === 'auto') mode = o.planActive ? 'plan' : 'bypass'
|
|
188
|
+
if (mode === 'plan') argv.push('--permission-mode', 'plan')
|
|
189
|
+
else if (mode === 'accept-edits') argv.push('--permission-mode', 'acceptEdits')
|
|
190
|
+
else { mode = 'bypass'; argv.push('--dangerously-skip-permissions') }
|
|
191
|
+
|
|
192
|
+
// 模型优先级:调用参数 > 设置面板 defaultModel > kit 注入的 defaultModel > 不传(claude 自决)。
|
|
193
|
+
const model = args.model || currentSettings().defaultModel || o.defaultModel || ''
|
|
194
|
+
if (model) argv.push('--model', String(model))
|
|
195
|
+
|
|
196
|
+
const budgetRaw = args.maxBudgetUsd != null ? args.maxBudgetUsd : currentSettings().maxBudgetUsd
|
|
197
|
+
const budget = budgetRaw == null ? NaN : Number(budgetRaw)
|
|
198
|
+
if (Number.isFinite(budget) && budget > 0) argv.push('--max-budget-usd', String(budget))
|
|
199
|
+
|
|
200
|
+
if (args.systemPrompt) argv.push('--system-prompt', String(args.systemPrompt))
|
|
201
|
+
if (args.appendSystemPrompt) argv.push('--append-system-prompt', String(args.appendSystemPrompt))
|
|
202
|
+
if (Array.isArray(args.allowedTools) && args.allowedTools.length) argv.push('--allowedTools', args.allowedTools.map(String).join(','))
|
|
203
|
+
if (Array.isArray(args.disallowedTools) && args.disallowedTools.length) argv.push('--disallowedTools', args.disallowedTools.map(String).join(','))
|
|
204
|
+
if (Array.isArray(args.addDirs)) for (const d of args.addDirs) { if (d) argv.push('--add-dir', String(d)) }
|
|
205
|
+
if (args.noSessionPersistence) argv.push('--no-session-persistence')
|
|
206
|
+
|
|
207
|
+
if (args.sessionId) argv.push('--resume', String(args.sessionId))
|
|
208
|
+
else if (args.continueLatest) argv.push('--continue')
|
|
209
|
+
if (args.forkSession && (args.sessionId || args.continueLatest)) argv.push('--fork-session')
|
|
210
|
+
return { argv, mode, timeoutSec }
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
// stdout 解析:json = 单行 result;stream-json = 多行事件,取**最后**一个 result。
|
|
214
|
+
// 无 result 事件 → null(buildResult 据此判 PARSE_ERROR,绝不假造成功)。
|
|
215
|
+
parseJson(stdoutText) {
|
|
216
|
+
const trimmed = String(stdoutText || '').trim()
|
|
217
|
+
if (!trimmed) return null
|
|
218
|
+
let last = null
|
|
219
|
+
for (const raw of trimmed.split(/\r?\n/)) {
|
|
220
|
+
const t = raw.trim()
|
|
221
|
+
if (!t.startsWith('{')) continue
|
|
222
|
+
let obj
|
|
223
|
+
try { obj = JSON.parse(t) } catch (e) { continue }
|
|
224
|
+
if (obj && obj.type === 'result') last = obj
|
|
225
|
+
}
|
|
226
|
+
if (last) return last
|
|
227
|
+
try {
|
|
228
|
+
const obj = JSON.parse(trimmed)
|
|
229
|
+
if (obj && obj.type === 'result') return obj
|
|
230
|
+
} catch (e) { }
|
|
231
|
+
return null
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// 统一结果对象。⚠️ 成功判据 = is_error === false(且 subtype 非错误态)**并且** exit 0;
|
|
235
|
+
// 只看 exit code 会把「无效模型」误判成成功(实测该场景 exit 1 + subtype "success"),
|
|
236
|
+
// 反过来也存在 exit 0 + is_error true 的形态,故两者必须同时成立。
|
|
237
|
+
buildResult(parsed, outcome, mode, stderrText, stdoutText, backend) {
|
|
238
|
+
const exitCode = outcome ? outcome.exitCode : null
|
|
239
|
+
const stderr = stderrText ? String(stderrText).slice(-2000) : ''
|
|
240
|
+
if (parsed && typeof parsed === 'object') {
|
|
241
|
+
const isError = parsed.is_error === true
|
|
242
|
+
const subtype = typeof parsed.subtype === 'string' ? parsed.subtype : ''
|
|
243
|
+
const response = typeof parsed.result === 'string' ? parsed.result : ''
|
|
244
|
+
const errorText = isError ? (response || (typeof parsed.error === 'string' ? parsed.error : '')) : ''
|
|
245
|
+
let status
|
|
246
|
+
if (exitCode === 130) status = 'ABORTED'
|
|
247
|
+
else if (isError) status = subtype === 'error_max_turns' ? 'MAX_TURNS' : 'EXEC_ERROR'
|
|
248
|
+
else if (exitCode !== 0) status = 'CLI_ERROR'
|
|
249
|
+
else status = 'SUCCESS'
|
|
250
|
+
const ok = !isError && exitCode === 0 && status === 'SUCCESS'
|
|
251
|
+
const denials = Array.isArray(parsed.permission_denials) ? parsed.permission_denials.length : 0
|
|
252
|
+
return {
|
|
253
|
+
ok,
|
|
254
|
+
status,
|
|
255
|
+
response,
|
|
256
|
+
sessionId: typeof parsed.session_id === 'string' ? parsed.session_id : null,
|
|
257
|
+
durationSeconds: typeof parsed.duration_ms === 'number' ? Math.round(parsed.duration_ms / 100) / 10 : null,
|
|
258
|
+
numTurns: typeof parsed.num_turns === 'number' ? parsed.num_turns : null,
|
|
259
|
+
totalTokens: sumTokens(parsed.usage),
|
|
260
|
+
exitCode, mode, backend: backend || null,
|
|
261
|
+
// is_error 的正文也必须进 stderr:限流/凭据判定与失败指引都读它。
|
|
262
|
+
stderr: (stderr ? stderr + ' ' : '') + errorText,
|
|
263
|
+
subtype: subtype || null,
|
|
264
|
+
modelUsed: pickModelUsed(parsed.modelUsage),
|
|
265
|
+
costUsd: typeof parsed.total_cost_usd === 'number' ? parsed.total_cost_usd : null,
|
|
266
|
+
permissionDenials: denials
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const text = String(stdoutText || '').trim()
|
|
270
|
+
const status = exitCode === 0 ? 'PARSE_ERROR' : 'CLI_ERROR'
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
status,
|
|
274
|
+
response: '',
|
|
275
|
+
sessionId: null,
|
|
276
|
+
durationSeconds: null, numTurns: null, totalTokens: null,
|
|
277
|
+
exitCode, mode, backend: backend || null,
|
|
278
|
+
stderr: (stderr ? stderr + ' ' : '') + (exitCode === 0 ? 'no result event in claude output' : ''),
|
|
279
|
+
subtype: null, modelUsed: null, costUsd: null, permissionDenials: 0,
|
|
280
|
+
rawStdout: text.slice(-2000)
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// 实时步骤折叠(stream-json → project.current/trail)。
|
|
285
|
+
// 事件形态(实测):system/subtype=init 带 model+permissionMode;assistant.message.content
|
|
286
|
+
// 是 Anthropic 内容块数组(thinking / text / tool_use);user.message.content 带 tool_result。
|
|
287
|
+
foldStreamEvent(ev, p, summarizeArgs, maxArgLen, maxTrail) {
|
|
288
|
+
if (!ev || typeof ev !== 'object') return
|
|
289
|
+
const type = ev.type
|
|
290
|
+
if (!type) return
|
|
291
|
+
const cap = maxTrail || 12
|
|
292
|
+
const capLen = maxArgLen || 120
|
|
293
|
+
const push = (entry) => { p.trail.push(entry); while (p.trail.length > cap) p.trail.shift() }
|
|
294
|
+
const blocksOf = (m) => (m && typeof m === 'object' && Array.isArray(m.content) ? m.content : [])
|
|
295
|
+
p.updatedAt = Date.now()
|
|
296
|
+
if (type === 'system') {
|
|
297
|
+
if (ev.subtype === 'init') {
|
|
298
|
+
p.current = { tool: 'session', args: { model: ev.model || '', permissionMode: ev.permissionMode || '' }, stepIndex: 0, since: nowIso() }
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
if (type === 'assistant') {
|
|
304
|
+
for (const b of blocksOf(ev.message)) {
|
|
305
|
+
if (!b || typeof b !== 'object') continue
|
|
306
|
+
if (b.type === 'tool_use') {
|
|
307
|
+
const tool = b.name || 'tool'
|
|
308
|
+
const args = summarizeArgs(b.input)
|
|
309
|
+
const idx = p.trail.length
|
|
310
|
+
push({ stepIndex: idx, state: 'ACTIVE', tool, args: args || null, at: nowIso() })
|
|
311
|
+
p.current = { tool, args: args || null, stepIndex: idx, since: nowIso() }
|
|
312
|
+
} else if (b.type === 'text' && typeof b.text === 'string' && b.text) {
|
|
313
|
+
p.current = { tool: 'agent_response', args: { text_delta: b.text.slice(0, capLen) }, stepIndex: p.trail.length, since: nowIso() }
|
|
314
|
+
} else if (b.type === 'thinking' && typeof b.thinking === 'string' && b.thinking) {
|
|
315
|
+
p.current = { tool: 'reasoning', args: { text_delta: b.thinking.slice(0, capLen) }, stepIndex: p.trail.length, since: nowIso() }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
if (type === 'user') {
|
|
321
|
+
for (const b of blocksOf(ev.message)) {
|
|
322
|
+
if (!b || typeof b !== 'object' || b.type !== 'tool_result') continue
|
|
323
|
+
// 把最近一个仍在 ACTIVE 的步骤收尾(tool_result 不带 tool name,按时间序配对)。
|
|
324
|
+
for (let i = p.trail.length - 1; i >= 0; i--) {
|
|
325
|
+
if (p.trail[i].state === 'ACTIVE') { p.trail[i].state = b.is_error === true ? 'ERROR' : 'DONE'; break }
|
|
326
|
+
}
|
|
327
|
+
p.current = null
|
|
328
|
+
}
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
if (type === 'result') { p.current = null }
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
// 瞬时错误静默重试一次:只认网络/过载类,且明确排除模型名/凭据类(那些重试无用)。
|
|
335
|
+
isTransientCliError(res) {
|
|
336
|
+
if (!res || res.ok) return false
|
|
337
|
+
if (isLimited(res)) return false
|
|
338
|
+
const hay = String(res.stderr || '') + ' ' + String(res.response || '')
|
|
339
|
+
if (/supported API model names|invalid_request_error|model .* not|authentication|invalid api key|401|403/i.test(hay)) return false
|
|
340
|
+
return /overloaded|529|503|ECONNRESET|socket hang up|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|terminated|connection (reset|closed)/i.test(hay)
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
failureHint(res) {
|
|
344
|
+
if (!res || res.ok) return ''
|
|
345
|
+
const st = String(res.status || '')
|
|
346
|
+
if (st === 'CLAUDE_UNAVAILABLE') return '[诊断] 未找到 claude 可执行文件:请安装 Claude Code(或 npm i -g @anthropic-ai/claude-code),或在 CLI Bridge 设置面板填写 claude 可执行文件路径(也可设 CLAUDE_BIN)。'
|
|
347
|
+
if (st === 'PARSE_ERROR') return '[诊断] claude 没有输出 result 事件(进程可能被中断/输出被截断)。若你手动改了 output-format,请改回;否则重试一次,持续失败请检查 CLI 版本(--output-format json 是 2.x 的行为)。'
|
|
348
|
+
const hay = String(res.stderr || '') + ' ' + String(res.response || '')
|
|
349
|
+
const supported = parseSupportedModels(hay)
|
|
350
|
+
if (supported) return '[诊断] 模型名被端点拒绝。该端点当前支持的模型:' + supported.join(', ') + ' —— 用 claude_models 取权威清单,然后在调用参数或设置面板里改 model;字符串 "[1M]" 之类的后缀属于配置里的别名写法,不能直接当 --model 传入。'
|
|
351
|
+
if (/supported API model names|invalid_request_error/i.test(hay)) return '[诊断] 模型名被端点拒绝(400 invalid_request_error):先用 claude_models 确认服务端支持的模型,再传 model。'
|
|
352
|
+
if (res.permissionDenials) return '[诊断] 有 ' + res.permissionDenials + ' 次工具调用被权限拒绝:-p 模式没有交互确认,请用 mode=bypass(默认 auto 在非 plan 模式下就是 bypass),或在 allowedTools 里放行所需工具。'
|
|
353
|
+
if (/authentication_error|invalid x-api-key|invalid api key|401|unauthorized|not logged in|oauth|api key/i.test(hay) && !/rate.?limit|quota|限流|额度/i.test(hay)) {
|
|
354
|
+
return '[诊断] claude 凭据/端点问题:请让用户检查 ~/.claude/settings.json 的 env 块(ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN)或重新登录 claude;这不是限流。'
|
|
355
|
+
}
|
|
356
|
+
if (/rate.?limit|\b429\b|quota|insufficient|credit|balance|额度/i.test(hay)) {
|
|
357
|
+
return '[诊断] 端点报告限流/额度不足:不要循环重试;换 model(用 claude_models 挑一个可用的)或改用 DSH 原生工具完成本任务。'
|
|
358
|
+
}
|
|
359
|
+
return ''
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
isSuccessStatus(st) { return st === 'SUCCESS' },
|
|
363
|
+
|
|
364
|
+
tools: {
|
|
365
|
+
run: { name: 'claude_run', description: 'Dispatch a coding/build/debug/investigation task to the local Claude Code CLI and return its final answer. Prefer this for implementation, edits, refactors, multi-file investigation and debugging in every mode. DSH fully controls claude: it always runs non-interactively with permissions auto-approved (claude never blocks on a prompt), in stream-json mode so live steps reach claude_status. Use read-only native tools only for quick lookups and for final build/test verification. Runs in the background by default (returns a jobId immediately; collect with job_output); set background=false to block for the final answer.', inputSchema: RUN_PARAMS },
|
|
366
|
+
continue: { name: 'claude_continue', description: 'Continue an existing claude session with a follow-up prompt, reusing claude context. Pass sessionId from a prior claude_run result (--resume), or set latest=true to continue the most recent conversation (--continue). Same DSH-controlled, no-prompt execution as claude_run.', inputSchema: CONTINUE_PARAMS },
|
|
367
|
+
status: { name: 'claude_status', description: 'Read a live snapshot of what the local claude agent is currently doing. Returns one section per project (working directory): running count, the current step (tool name + arguments being executed, or agent_response thinking/typing), the recent step trail (tools executed, done/error), and the last completed run status + session id. Optional cwd filters to a single project. Call this to check on an in-flight claude_run/claude_continue without waiting for it to finish.', inputSchema: STATUS_PARAMS }
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
// claude_models:三形态都注册(探测只需 node child_process;拿不到 spawn 能力时
|
|
371
|
+
// logic 明确返回 ok:false,而不是静默缺席或伪造列表)。
|
|
372
|
+
extraTools: [{
|
|
373
|
+
name: 'claude_models',
|
|
374
|
+
description: MODELS_DESCRIPTION,
|
|
375
|
+
inputSchema: MODELS_PARAMS,
|
|
376
|
+
render: renderModels,
|
|
377
|
+
async logic(args) {
|
|
378
|
+
return await descriptor.listModels(args || {})
|
|
379
|
+
}
|
|
380
|
+
}],
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* 列出服务端权威可用模型(三形态共用)。
|
|
384
|
+
* 铁律:只认账号/端点探测;探测失败 = ok:false + 可操作错误 + 配置线索(非权威)。
|
|
385
|
+
*/
|
|
386
|
+
async listModels(args) {
|
|
387
|
+
const a = args || {}
|
|
388
|
+
return await listAccountModels(env, {
|
|
389
|
+
binPath: currentSettings().binPath,
|
|
390
|
+
timeoutMs: clampInt(a.timeoutMs, 30000, 5000, 90000),
|
|
391
|
+
refresh: !!a.refresh
|
|
392
|
+
})
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
// ── 设置分区(契约 §3)─────────────────────────────────────────────────────
|
|
396
|
+
settings: {
|
|
397
|
+
keys: ['binPath', 'defaultModel', 'maxBudgetUsd'],
|
|
398
|
+
defaults: () => ({ binPath: '', defaultModel: '', maxBudgetUsd: '' }),
|
|
399
|
+
normalize: (raw) => {
|
|
400
|
+
const r = raw && typeof raw === 'object' ? raw : {}
|
|
401
|
+
const out = { binPath: '', defaultModel: '', maxBudgetUsd: '' }
|
|
402
|
+
for (const k of ['binPath', 'defaultModel', 'maxBudgetUsd']) {
|
|
403
|
+
if (typeof r[k] === 'string') out[k] = String(r[k]).trim()
|
|
404
|
+
}
|
|
405
|
+
return out
|
|
406
|
+
},
|
|
407
|
+
meta: (env2) => {
|
|
408
|
+
const e = env2 && typeof env2 === 'object' ? env2 : {}
|
|
409
|
+
const s = currentSettings()
|
|
410
|
+
const probe = (function () { try { return probeSupportedModelsSyncCached(e, { binPath: s.binPath }) } catch (err) { return { ok: false, ids: [], error: String(err && err.message || err) } } })()
|
|
411
|
+
const options = (probe.ok ? probe.ids : []).map((id) => ({ value: id, label: id }))
|
|
412
|
+
const exeCandidates = claudeExeCandidates(e, '')
|
|
413
|
+
return {
|
|
414
|
+
titleSuffix: 'Claude Code',
|
|
415
|
+
fields: [
|
|
416
|
+
{
|
|
417
|
+
key: 'binPath', label: 'claude 可执行文件路径', type: 'select', allowCustom: true,
|
|
418
|
+
options: exeCandidates.map((p) => ({ value: p, label: p })),
|
|
419
|
+
placeholder: '留空 = 自动探测本地安装 / PATH',
|
|
420
|
+
note: '检测到 ' + exeCandidates.length + ' 个候选;npm 全局安装给的是 .cmd(模型探测与 MCP 形态经 shell 启动),原生安装给 .exe —— preset 形态建议指向原生 .exe(.cmd 能否执行取决于宿主 subprocess)。也可手动粘贴任意路径(.mjs/.js 经 node 启动)。'
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true,
|
|
424
|
+
options,
|
|
425
|
+
placeholder: '留空 = 由 claude 自身配置(~/.claude/settings.json)决定',
|
|
426
|
+
note: options.length
|
|
427
|
+
? '共 ' + options.length + ' 个服务端权威可用模型(探测所得)。调用时传 model 可覆盖本项。'
|
|
428
|
+
: '未能获取服务端权威模型清单(探测失败:' + String(probe.error || 'unknown').slice(0, 140) + ')—— 下拉为空,但可手动输入模型名。配置里的 ANTHROPIC_*_MODEL 只是别名映射,不作为候选展示。'
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
key: 'maxBudgetUsd', label: '单次最大花费(美元)', type: 'text',
|
|
432
|
+
placeholder: '留空 = 不限制',
|
|
433
|
+
note: '非空时自动附加 --max-budget-usd;调用参数 maxBudgetUsd 可覆盖。'
|
|
434
|
+
}
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
diagnose: (settings, env2) => {
|
|
439
|
+
const s = settings && typeof settings === 'object' ? settings : {}
|
|
440
|
+
const e = env2 && typeof env2 === 'object' ? env2 : {}
|
|
441
|
+
const bits = []
|
|
442
|
+
if (s.binPath) bits.push('bin=设置:' + s.binPath)
|
|
443
|
+
else if (claudePathHint(e)) bits.push('bin=' + claudePathHint(e))
|
|
444
|
+
else bits.push('bin=PATH:claude')
|
|
445
|
+
bits.push(s.defaultModel ? 'model=' + s.defaultModel : 'model=(claude 自决)')
|
|
446
|
+
if (s.maxBudgetUsd) bits.push('maxBudget=$' + s.maxBudgetUsd)
|
|
447
|
+
const cfg = readConfiguredModels(e)
|
|
448
|
+
if (cfg.ok && cfg.models.length) bits.push('settings.json env: ' + cfg.models.length + ' 个别名映射(非权威清单)')
|
|
449
|
+
if (!cfg.ok && cfg.error) bits.push('settings.json: ' + cfg.error.slice(0, 120))
|
|
450
|
+
return bits.join(' · ')
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
// MCP 面版本由宿主注入(mcp/version.mjs 单源派生),描述符不复制 version 字面量。
|
|
455
|
+
mcp: (deps.mcp && typeof deps.mcp === 'object') ? Object.assign({}, deps.mcp) : {}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── 三形态共用的工具逻辑(签名固定:宿主接线见 preset/dynamic/mcp 三处)──────────
|
|
459
|
+
descriptor.runToolLogic = async function (args, exec) {
|
|
460
|
+
const a = args || {}
|
|
461
|
+
const normalized = a.background === undefined ? Object.assign({}, a, { background: true }) : a
|
|
462
|
+
return this.runner.coreExecute(normalized, exec)
|
|
463
|
+
}
|
|
464
|
+
descriptor.continueToolLogic = async function (args, exec) {
|
|
465
|
+
const a = args || {}
|
|
466
|
+
const mapped = {
|
|
467
|
+
prompt: a.prompt, mode: a.mode, model: a.model, maxBudgetUsd: a.maxBudgetUsd,
|
|
468
|
+
cwd: a.cwd, forkSession: a.forkSession, timeoutSec: a.timeoutSec,
|
|
469
|
+
background: a.background === undefined ? true : a.background
|
|
470
|
+
}
|
|
471
|
+
const sid = a.sessionId || null
|
|
472
|
+
if (sid) mapped.sessionId = sid
|
|
473
|
+
else if (a.latest) mapped.continueLatest = true
|
|
474
|
+
return this.runner.coreExecute(mapped, exec)
|
|
475
|
+
}
|
|
476
|
+
descriptor.statusToolLogic = function (args) {
|
|
477
|
+
const a = args || {}
|
|
478
|
+
const snap = this.engine.statusSnapshot()
|
|
479
|
+
if (a.cwd) {
|
|
480
|
+
const key = String(a.cwd)
|
|
481
|
+
snap.projects = snap.projects.filter((p) => p.cwd === key)
|
|
482
|
+
const g = snap.projects[0]
|
|
483
|
+
if (g) {
|
|
484
|
+
Object.assign(snap, {
|
|
485
|
+
state: g.state, running: g.running, current: g.current, trail: g.trail,
|
|
486
|
+
lastStatus: g.lastStatus, lastAt: g.lastAt, lastSessionId: g.lastSessionId,
|
|
487
|
+
lastBackend: g.lastBackend, fallbackActive: g.fallbackActive, updatedAt: g.updatedAt
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return snap
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
descriptor.attach = function (hostOpts) {
|
|
495
|
+
host = hostOpts || {}
|
|
496
|
+
try {
|
|
497
|
+
if (typeof host.getSettings === 'function') {
|
|
498
|
+
const s = host.getSettings('claude')
|
|
499
|
+
if (s && typeof s === 'object') settingsRef = descriptor.settings.normalize(s)
|
|
500
|
+
}
|
|
501
|
+
} catch (e) { }
|
|
502
|
+
this.engine = createStatusEngine({ provider: this, publish: host.publish })
|
|
503
|
+
const runner = createRunner(Object.assign({}, host, { provider: this, engine: this.engine }))
|
|
504
|
+
this.runner = runner
|
|
505
|
+
return runner
|
|
506
|
+
}
|
|
507
|
+
return descriptor
|
|
508
|
+
}
|