@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/PROVIDER-CONTRACT.md +53 -4
  3. package/README.md +12 -11
  4. package/docs/HANDOVER.md +9 -0
  5. package/docs/audits/2026-10-18-preset-0.2.1-contract-fix.md +160 -0
  6. package/docs/audits/README.md +15 -0
  7. package/dynamic/agy.host.js +1 -1
  8. package/dynamic/claude.host.js +1801 -0
  9. package/dynamic/claude.host.template.mjs +108 -0
  10. package/dynamic/codebuddy.host.js +1 -1
  11. package/dynamic/codex.host.js +1955 -0
  12. package/dynamic/codex.host.template.mjs +137 -0
  13. package/dynamic/mimo.host.js +1 -1
  14. package/dynamic/opencode.host.js +1 -1
  15. package/home-plugin/cli-bridge-indicator/lib/client.js +600 -606
  16. package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +87 -9
  17. package/mcp/cli-bridge-mcp.mjs +6 -4
  18. package/mcp/providers/claude.mjs +231 -0
  19. package/mcp/providers/codex.mjs +239 -0
  20. package/package.json +10 -6
  21. package/preset/agy-first/agent.cordis.yml +0 -5
  22. package/preset/agy-first/plugin-list.mjs +4 -1
  23. package/preset/claude-first/agent.cordis.yml +136 -0
  24. package/preset/claude-first/cli-bridge.mjs +131 -0
  25. package/preset/claude-first/plugin-list.mjs +28 -0
  26. package/preset/claude-first/preset.yml +3 -0
  27. package/preset/codebuddy-first/agent.cordis.yml +0 -5
  28. package/preset/codex-first/agent.cordis.yml +136 -0
  29. package/preset/codex-first/cli-bridge.mjs +160 -0
  30. package/preset/codex-first/plugin-list.mjs +28 -0
  31. package/preset/codex-first/preset.yml +3 -0
  32. package/preset/mimo-first/agent.cordis.yml +1 -7
  33. package/preset/mimo-first/plugin-list.mjs +18 -12
  34. package/preset/mimo-member/plugin-list.mjs +49 -0
  35. package/preset/opencode-first/agent.cordis.yml +0 -5
  36. package/preset/opencode-first/plugin-list.mjs +4 -2
  37. package/preset/standard-base.mjs +18 -6
  38. package/providers/claude-exe.mjs +112 -0
  39. package/providers/claude-models.mjs +334 -0
  40. package/providers/claude-policy.mjs +37 -0
  41. package/providers/claude.mjs +508 -0
  42. package/providers/codex-cli.mjs +471 -0
  43. package/providers/codex-models.mjs +107 -0
  44. package/providers/codex.mjs +544 -0
  45. package/providers/registry.mjs +25 -1
  46. package/scripts/build.mjs +1 -2
  47. package/scripts/verify.mjs +8 -2
@@ -0,0 +1,334 @@
1
+ // providers/claude-models.mjs — claude 模型清单探测(账号实时权威源,纯逻辑 + 最小 spawn)。
2
+ //
3
+ // 从 providers/claude.mjs 拆出(规则 §26:单文件 600 行红线)。build 会把
4
+ // providers/claude-*.mjs 按字典序内联进 dynamic 沙箱同一作用域;ESM 形态走 import。
5
+ // ⚠️ 注释里绝不写出口令标记字面量。
6
+ //
7
+ // 为什么必须「探测」而不是读配置(本机 Claude Code 2.0.76 + 第三方 Anthropic 兼容端点实测):
8
+ // 传一个故意无效的模型名 → 服务端返回 400,CLI 把报文塞进 result 事件:
9
+ // {"type":"result","subtype":"success","is_error":true,
10
+ // "result":"API Error: 400 {\"error\":{\"message\":\"The supported API model names are
11
+ // deepseek-flash, deepseek-v4-pro, but you passed __clb_probe_invalid__.\"}}"}
12
+ // → 这条 "The supported API model names are X, Y" 就是该端点/该 key 的权威可用清单。
13
+ // ~/.claude/settings.json 的 env.ANTHROPIC_DEFAULT_*_MODEL 只是「别名映射」,
14
+ // 不是服务端可用全集 —— 红线:绝不把它冒充成可用清单,也绝不静默返回空数组。
15
+ //
16
+ // 落盘/缓存:面板(settings.meta)是同步回调,必须 spawnSync;探测一次约 3-5s,
17
+ // 故结果进 5 分钟缓存(与 codebuddy catalog 同策略),避免每次渲染都打网络。
18
+
19
+ import { claudeExeCandidates, claudeProbeCandidates, splitExeSpec, quoteShellArg } from './claude-exe.mjs'
20
+
21
+ // 故意无效的探测模型名(服务端必然拒绝;保持带前缀,便于日志辨认来源)。
22
+ export const PROBE_MODEL = '__clb_probe_invalid__'
23
+ export const MODELS_TTL_MS = 5 * 60 * 1000
24
+
25
+ // 探测结果缓存:key(后端/端点标识) → { at, ids }
26
+ const modelsCache = new Map()
27
+
28
+ /** 清空探测缓存(测试与「模型变更后刷新」用)。 */
29
+ export function resetModelsCache() { modelsCache.clear(); syncModelsCache.clear() }
30
+
31
+ // 缓存键 = 候选可执行文件集合(同一台机器可能切换 CLAUDE_BIN / 面板路径,键必须跟着变,
32
+ // 否则会把「上一个 exe 的探测结果」错配给新 exe)。
33
+ function candidateKey(env, binPath) {
34
+ const list = claudeProbeCandidates(env, binPath)
35
+ return list.length ? list.join('|') : 'claude'
36
+ }
37
+
38
+ /**
39
+ * 缓存包装:TTL 内直接返回上次成功结果;失败不写缓存(下次仍然重试,绝不把失败固化)。
40
+ * @param {string} key
41
+ * @param {() => Promise<object>|object} load
42
+ * @param {number} [nowMs]
43
+ */
44
+ export function cachedModels(key, load, nowMs) {
45
+ const at = Number(nowMs) || Date.now()
46
+ const hit = modelsCache.get(key)
47
+ if (hit && at - hit.at < MODELS_TTL_MS) return hit.value
48
+ return Promise.resolve().then(load).then((v) => {
49
+ if (v && v.ok && Array.isArray(v.ids) && v.ids.length) modelsCache.set(key, { at, value: v })
50
+ return v
51
+ })
52
+ }
53
+
54
+ /**
55
+ * 从 400 报文里解析「服务端支持的模型名」。
56
+ * 主格式(实测):`The supported API model names are A, B, but you passed Z.`
57
+ * 兼容:句尾直接是句号(无 "but you passed")、JSON 转义形态、大小写差异。
58
+ * @param {string} text
59
+ * @returns {string[]|null} 无该锚点返回 null(调用方必须据此报错,不得伪造清单)
60
+ */
61
+ export function parseSupportedModels(text) {
62
+ const s = String(text || '')
63
+ if (!s) return null
64
+ const anchorRe = /the supported API model names are\s+/i
65
+ const m = anchorRe.exec(s)
66
+ if (!m) return null
67
+ const tail = s.slice(m.index + m[0].length)
68
+ // ① 有 "but you passed" → 这是清单的确定右边界(模型名里可以有 '.',不能按句点切)。
69
+ let body = ''
70
+ const end = /,?\s*but you passed/i.exec(tail)
71
+ if (end) body = tail.slice(0, end.index)
72
+ else {
73
+ // ② 无右边界锚点:截到引号/换行/行尾,再 chomp 单个句点。
74
+ const stop = tail.search(/["\r\n]/)
75
+ body = (stop >= 0 ? tail.slice(0, stop) : tail).replace(/[.\s]+$/, '')
76
+ }
77
+ const ids = []
78
+ for (const raw of body.split(/\s*,\s*|\s+and\s+/i)) {
79
+ const id = String(raw || '').trim().replace(/^["'`[]+|["'`\]]+$/g, '')
80
+ if (!id) continue
81
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:@/+-]*$/.test(id)) continue
82
+ if (id === PROBE_MODEL) continue
83
+ if (ids.indexOf(id) < 0) ids.push(id)
84
+ }
85
+ return ids.length ? ids : null
86
+ }
87
+
88
+ /**
89
+ * 从探测进程的完整输出里挖出模型清单。
90
+ * CLI 的 stdout 是 result 事件 JSON(is_error:true);它的 result 字段里才是 400 报文,
91
+ * 且报文自身是 JSON 字符串(转义)。顺序:raw 文本 → 每行 JSON 的 result 字段。
92
+ * @param {string} stdoutText
93
+ * @param {string} [stderrText]
94
+ * @returns {{ids: string[]|null, errorText: string, sawResult: boolean}}
95
+ */
96
+ export function extractProbeIds(stdoutText, stderrText) {
97
+ const out = String(stdoutText || '')
98
+ const err = String(stderrText || '')
99
+ const lines = out.split(/\r?\n/).filter(Boolean)
100
+ let errorText = ''
101
+ let sawResult = false
102
+ for (const ln of lines) {
103
+ const t = ln.trim()
104
+ if (!t.startsWith('{')) continue
105
+ let obj
106
+ try { obj = JSON.parse(t) } catch (e) { continue }
107
+ if (!obj || obj.type !== 'result') continue
108
+ sawResult = true
109
+ if (typeof obj.result === 'string' && obj.result) errorText = obj.result
110
+ else if (obj.is_error && typeof obj.error === 'string') errorText = obj.error
111
+ }
112
+ if (!errorText) errorText = err || out
113
+ const ids = parseSupportedModels(errorText) || parseSupportedModels(out) || parseSupportedModels(err)
114
+ return { ids: ids || null, errorText: errorText.slice(0, 800), sawResult }
115
+ }
116
+
117
+ /** ~/.claude/settings.json 路径(CLAUDE_CONFIG_DIR 可覆盖配置根,本机实测存在)。 */
118
+ export function claudeConfigDir(env) {
119
+ const e = env || {}
120
+ if (e.CLAUDE_CONFIG_DIR) return String(e.CLAUDE_CONFIG_DIR)
121
+ const home = e.USERPROFILE || e.HOME || ''
122
+ if (!home) return ''
123
+ const path = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:path') : null } catch (x) { return null } })()
124
+ if (!path || typeof path.join !== 'function') return ''
125
+ return path.join(home, '.claude')
126
+ }
127
+
128
+ /**
129
+ * 读 ~/.claude/settings.json 的 env 块里「用户配置的模型名」。
130
+ * ⚠️ 这是别名映射(ANTHROPIC_DEFAULT_OPUS_MODEL 之类),**不是**服务端可用全集:
131
+ * 只作为诊断/解释线索返回,绝不塞进 ok:true 的模型清单。
132
+ * @returns {{ok: boolean, path: string, models: Array<{id: string, key: string}>, error: string}}
133
+ */
134
+ export function readConfiguredModels(env) {
135
+ const dir = claudeConfigDir(env)
136
+ if (!dir) return { ok: false, path: '', models: [], error: 'cannot resolve home directory (no USERPROFILE/HOME/CLAUDE_CONFIG_DIR)' }
137
+ const fs = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:fs') : null } catch (x) { return null } })()
138
+ const path = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:path') : null } catch (x) { return null } })()
139
+ if (!fs || !path) return { ok: false, path: '', models: [], error: 'no fs capability in this host form' }
140
+ const file = path.join(dir, 'settings.json')
141
+ let raw
142
+ try { raw = JSON.parse(fs.readFileSync(file, 'utf8')) } catch (e) {
143
+ return { ok: false, path: file, models: [], error: 'cannot read ' + file + ': ' + String(e && e.message || e) }
144
+ }
145
+ const envBlock = raw && raw.env && typeof raw.env === 'object' ? raw.env : {}
146
+ const models = []
147
+ const seen = Object.create(null)
148
+ for (const key of Object.keys(envBlock)) {
149
+ if (!/^(ANTHROPIC_(MODEL|DEFAULT_[A-Z]+_MODEL|DEFAULT_[A-Z]+_MODEL_NAME|SMALL_FAST_MODEL))$/i.test(key)) continue
150
+ const v = envBlock[key]
151
+ if (typeof v !== 'string' || !v.trim()) continue
152
+ const id = v.trim()
153
+ if (seen[id]) continue
154
+ seen[id] = true
155
+ models.push({ id, key })
156
+ }
157
+ return { ok: true, path: file, models, error: '' }
158
+ }
159
+
160
+ /** 探测 argv(不含 exe 前缀):固定参数集,绝不拼装用户 prompt。 */
161
+ export function probeArgv() {
162
+ return ['-p', 'hi', '--model', PROBE_MODEL, '--output-format', 'json', '--no-session-persistence']
163
+ }
164
+
165
+ /** 探测失败时的可操作文案(区分「没找到 CLI」与「CLI 跑了但服务端没报清单」)。 */
166
+ function probeFailureText(reasons, tried) {
167
+ const head = '无法获取服务端权威模型清单(已尝试探测 ' + tried + ' 个候选可执行文件)'
168
+ const body = reasons.join('; ')
169
+ return head + (body ? ': ' + body : '') +
170
+ '。请确认 claude CLI 已安装且端点/凭据可用(claude 在 ~/.claude/settings.json 的 env 里读 ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN),或在设置面板填写正确的可执行文件路径后重试。'
171
+ }
172
+
173
+ /**
174
+ * 同步探测(供 settings.meta 面板下拉;无 spawnSync 能力 → 明确 ok:false)。
175
+ * @param {object} env
176
+ * @param {{binPath?: string, timeoutMs?: number}} [opts]
177
+ * @returns {{ok: boolean, ids: string[], mode: string, exe?: string, error?: string}}
178
+ */
179
+ export function probeSupportedModelsSync(env, opts) {
180
+ const o = opts || {}
181
+ const child = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:child_process') : null } catch (x) { return null } })()
182
+ if (!child || typeof child.spawnSync !== 'function') {
183
+ return { ok: false, ids: [], mode: 'account-live', error: 'no spawnSync capability in this host form' }
184
+ }
185
+ const cands = claudeProbeCandidates(env, o.binPath)
186
+ const probeList = cands.length ? cands : ['claude']
187
+ const timeout = Math.max(3000, Math.min(Number(o.timeoutMs) || 12000, 30000))
188
+ const reasons = []
189
+ const proc = globalThis.process
190
+ for (const exe of probeList) {
191
+ const spec = splitExeSpec(exe)
192
+ const argv = spec.prefixArgs.concat(probeArgv())
193
+ const cmd = spec.shell ? [spec.cmd].concat(argv).map(quoteShellArg).join(' ') : spec.cmd
194
+ const r = child.spawnSync(cmd, spec.shell ? [] : argv, {
195
+ encoding: 'utf8', timeout, windowsHide: true, shell: !!spec.shell,
196
+ env: Object.assign({}, (proc && proc.env) || {}, {})
197
+ })
198
+ if (!r || r.error) { reasons.push(exe + ': ' + String((r && r.error && (r.error.code || r.error.message)) || 'spawn failed')); continue }
199
+ const got = extractProbeIds(r.stdout, r.stderr)
200
+ if (got.ids && got.ids.length) return { ok: true, ids: got.ids, mode: 'account-live', exe }
201
+ const head = String(got.errorText || '').replace(/\s+/g, ' ').trim().slice(0, 160)
202
+ reasons.push(exe + ': exit ' + r.status + ',未解析出可用模型清单' + (head ? ' — ' + head : ''))
203
+ }
204
+ return { ok: false, ids: [], mode: 'account-live', error: probeFailureText(reasons, probeList.length) }
205
+ }
206
+
207
+ // 同步探测缓存(面板每次渲染都会取,避免反复 spawnSync 打网络)。
208
+ const syncModelsCache = new Map()
209
+ // 失败结果的反向缓存 TTL:成功缓存 5 分钟;失败只缓存 60 秒。
210
+ // 为什么必须有反向缓存:面板是同步回调,CLI 缺失/未登录时一次 spawnSync 可能阻塞数秒,
211
+ // 每次渲染都重探会让设置面板卡顿。60 秒足够让用户装好 CLI/登录后立刻看到新结果,
212
+ // 又不会把「当时读不到」长期固化成假象(状态照旧 ok:false + 可操作错误,语义不变)。
213
+ const NEGATIVE_TTL_MS = 60 * 1000
214
+
215
+ /**
216
+ * 带缓存的同步探测(settings.meta 专用;面板是同步回调,只能 spawnSync)。
217
+ * 成功/失败都进缓存(TTL 不同),失败语义保持不变:ok:false + 可操作错误。
218
+ * @returns {{ok: boolean, ids: string[], mode: string, exe?: string, error?: string}}
219
+ */
220
+ export function probeSupportedModelsSyncCached(env, opts) {
221
+ const o = opts || {}
222
+ const key = 'sync:claude:' + candidateKey(env, o.binPath)
223
+ const at = Date.now()
224
+ const hit = syncModelsCache.get(key)
225
+ if (hit) {
226
+ const ttl = hit.value && hit.value.ok ? MODELS_TTL_MS : NEGATIVE_TTL_MS
227
+ if (at - hit.at < ttl) return hit.value
228
+ }
229
+ const v = probeSupportedModelsSync(env, o)
230
+ syncModelsCache.set(key, { at, value: v })
231
+ return v
232
+ }
233
+
234
+ /**
235
+ * 异步探测(MCP / preset / dynamic 三形态共用):起一次真实 CLI 调用,解析 400 报文。
236
+ * 铁律:只认探测结果;失败 = ok:false + 可操作错误,绝不回落到配置文件/静态表。
237
+ * @param {object} env
238
+ * @param {{binPath?: string, timeoutMs?: number}} [opts]
239
+ * @returns {Promise<{ok: boolean, ids: string[], mode: string, exe?: string, error?: string}>}
240
+ */
241
+ export function probeSupportedModels(env, opts) {
242
+ const o = opts || {}
243
+ const child = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:child_process') : null } catch (x) { return null } })()
244
+ if (!child || typeof child.spawn !== 'function') {
245
+ return Promise.resolve({ ok: false, ids: [], mode: 'account-live', error: 'no spawn capability in this host form' })
246
+ }
247
+ const cands = claudeProbeCandidates(env, o.binPath)
248
+ const probeList = cands.length ? cands : ['claude']
249
+ const proc = globalThis.process
250
+ const env2 = Object.assign({}, (proc && proc.env) || {})
251
+ return new Promise((resolve) => {
252
+ let idx = 0
253
+ const reasons = []
254
+ const attempt = () => {
255
+ if (idx >= probeList.length) {
256
+ resolve({ ok: false, ids: [], mode: 'account-live', error: probeFailureText(reasons, probeList.length) })
257
+ return
258
+ }
259
+ const exe = probeList[idx++]
260
+ const spec = splitExeSpec(exe)
261
+ const argv = spec.prefixArgs.concat(probeArgv())
262
+ let c
263
+ try {
264
+ const spawnCmd = spec.shell ? [spec.cmd].concat(argv).map(quoteShellArg).join(' ') : spec.cmd
265
+ c = child.spawn(spawnCmd, spec.shell ? [] : argv, {
266
+ stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: !!spec.shell, env: env2
267
+ })
268
+ } catch (e) { reasons.push(exe + ': spawn threw ' + String(e && e.message || e)); attempt(); return }
269
+ let out = ''
270
+ let err = ''
271
+ const timer = setTimeout(() => { try { c.kill() } catch (e) { } }, Math.max(5000, Math.min(Number(o.timeoutMs) || 30000, 90000)))
272
+ c.stdout.on('data', (d) => { out += d; if (out.length > 400000) out = out.slice(-200000) })
273
+ c.stderr.on('data', (d) => { err += d; if (err.length > 200000) err = err.slice(-100000) })
274
+ c.on('error', (e) => {
275
+ clearTimeout(timer)
276
+ reasons.push(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
277
+ attempt()
278
+ })
279
+ c.on('close', (code) => {
280
+ clearTimeout(timer)
281
+ const got = extractProbeIds(out, err)
282
+ if (got.ids && got.ids.length) { resolve({ ok: true, ids: got.ids, mode: 'account-live', exe }); return }
283
+ const head = String(got.errorText || '').replace(/\s+/g, ' ').trim().slice(0, 160)
284
+ reasons.push(exe + ': exit ' + code + ',未解析出可用模型清单' + (head ? ' — ' + head : ''))
285
+ attempt()
286
+ })
287
+ }
288
+ attempt()
289
+ })
290
+ }
291
+
292
+ /**
293
+ * 组装 claude_models 的返回值(三形态共用;工具逻辑与 MCP 面共用这一份,杜绝两处漂移)。
294
+ * 成功 = 服务端清单(source: account-live);失败 = ok:false + configured 诊断线索
295
+ * (配置里那串名字只作「你可能想用哪个」的提示,明确标注非权威)。
296
+ * @param {object} env
297
+ * @param {{binPath?: string, timeoutMs?: number, refresh?: boolean}} [opts]
298
+ */
299
+ export async function listAccountModels(env, opts) {
300
+ const o = opts || {}
301
+ const key = 'claude:' + candidateKey(env, o.binPath)
302
+ if (o.refresh) modelsCache.delete(key)
303
+ const probe = await cachedModels(key, () => probeSupportedModels(env, o))
304
+ const cfg = readConfiguredModels(env)
305
+ const configured = cfg.models || []
306
+ if (!probe.ok) {
307
+ return {
308
+ ok: false,
309
+ source: 'account-live',
310
+ models: [],
311
+ count: 0,
312
+ error: String(probe.error || 'probe failed'),
313
+ diagnostic: {
314
+ configured: configured,
315
+ settingsPath: cfg.path,
316
+ note: 'ANTHROPIC_*_MODEL 只是用户配置的别名映射,不是服务端可用全集;因此不作为 models 返回。'
317
+ }
318
+ }
319
+ }
320
+ const ids = probe.ids.slice()
321
+ const byId = new Map(configured.map((m) => [m.id, m]))
322
+ const byIdLoose = new Map(configured.map((m) => [String(m.id).toLowerCase().replace(/\[.*\]$/, ''), m]))
323
+ return {
324
+ ok: true,
325
+ source: 'account-live',
326
+ models: ids.map((id) => {
327
+ const hit = byId.get(id) || byIdLoose.get(String(id).toLowerCase())
328
+ return { id, source: 'account-live', configuredAs: hit ? hit.key : null }
329
+ }),
330
+ count: ids.length,
331
+ ext: { exe: probe.exe || null, mode: probe.mode },
332
+ diagnostic: { settingsPath: cfg.path, configured: configured }
333
+ }
334
+ }
@@ -0,0 +1,37 @@
1
+ // providers/claude-policy.mjs — claude 提示词策略段(preset 与 dynamic 两种宿主唯一来源)。
2
+ //
3
+ // 为什么单独成片:同一段策略文本要在 preset/claude-first/cli-bridge.mjs(ESM 导入)与
4
+ // dynamic/claude.host.template.mjs(沙箱内联,禁 import)两处出现;各写一份必然漂移
5
+ // (历史上 agy/opencode 两处文案已经出现过不一致)。本文件被 scripts/build.mjs 作为
6
+ // providers/claude-*.mjs 之一内联进沙箱同一作用域,故两形态共用同一个常量。
7
+ // ⚠️ 注释里绝不写出口令标记字面量(build 全局替换会把注释劈开)。
8
+
9
+ export const POLICY_TEXT = [
10
+ 'claude-first execution policy (local Claude Code CLI bridge).',
11
+ '',
12
+ 'Local agent CLI "claude" handles coding tasks under full DSH control (non-interactive -p mode, permissions auto-approved so claude never blocks on a prompt). By default, claude runs in the background and returns a jobId immediately; collect results via job_output and watch live progress via claude_status anytime.',
13
+ '',
14
+ 'Decision table for tool selection:',
15
+ '- Single read/glob/grep or short (<30s) command -> Native tools.',
16
+ '- Implementing, editing, refactoring, debugging, builds, multi-file investigation -> claude_run.',
17
+ '- Follow-ups on the same topic -> claude_continue (with latest=true or sessionId; claude preserves session context via --resume).',
18
+ '- Final build/test verification of claude output -> Native tools.',
19
+ '',
20
+ 'Context protocol (one topic = one claude session):',
21
+ '- First dispatch on a topic: provide the task with a compact CONTEXT preamble (point to paths, do not paste file contents):',
22
+ ' CONTEXT: 2-6 lines — goal; repo root; key paths; decisions/constraints; current state.',
23
+ ' TASK: the complete instruction.',
24
+ '- Follow-ups: call claude_continue and state only what is new.',
25
+ '',
26
+ 'Execution & Fallback protocol:',
27
+ '- Dispatch defaults to background=true (set background=false only to block). Collect via job_output.',
28
+ '- mode=auto follows the DSH plan state: in plan mode claude runs with --permission-mode plan (read-only), otherwise permissions are bypassed so long edits/builds never stall on an approval prompt.',
29
+ '- On rate-limit or network failure, a confirmation dialog opens in BOTH foreground and background while in flight.',
30
+ '- If result has fallback=true (status FALLBACK_TO_DSH), the user chose DSH-local: finish with native tools and NEVER call claude again for this task.',
31
+ '- Never loop claude calls; never ask claude to call back into DSH.',
32
+ '',
33
+ 'Model & credentials:',
34
+ '- Which model ids actually work is decided by the ENDPOINT, not by this bridge: call claude_models to read the authoritative server-side roster (it probes the endpoint), then pass model. Never trust ANTHROPIC_*_MODEL entries in ~/.claude/settings.json as the usable set — they are alias mappings only.',
35
+ '- Omit the model unless you have a reason: claude then uses its own configuration (or the model chosen in CLI Bridge settings). A rejected model surfaces as a run failure with the supported list in the diagnostic hint — fix the model, do not retry blindly.',
36
+ '- Credentials are owned by claude, not by this bridge: base URL and auth token live in ~/.claude/settings.json (env block). On auth/401 failures tell the user to check that file or re-login; do not retry in a loop.'
37
+ ].join('\n')