@luchenglong/cli-bridge 0.2.8 → 0.2.10

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 (50) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/PROVIDER-CONTRACT.md +87 -5
  3. package/README.md +12 -11
  4. package/docs/HANDOVER.md +109 -15
  5. package/dynamic/agy.host.js +1 -1
  6. package/dynamic/claude.host.js +1858 -0
  7. package/dynamic/claude.host.template.mjs +108 -0
  8. package/dynamic/codebuddy.host.js +1427 -1379
  9. package/dynamic/codex.host.js +1930 -0
  10. package/dynamic/codex.host.template.mjs +137 -0
  11. package/dynamic/mimo.host.js +721 -515
  12. package/dynamic/mimo.host.template.mjs +2 -0
  13. package/dynamic/opencode.host.js +741 -585
  14. package/dynamic/opencode.host.template.mjs +2 -0
  15. package/home-plugin/cli-bridge-indicator/lib/client.js +74 -90
  16. package/home-plugin/cli-bridge-indicator/lib/index.mjs +48 -0
  17. package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +2 -3
  18. package/mcp/cli-bridge-mcp.mjs +4 -2
  19. package/mcp/providers/claude.mjs +231 -0
  20. package/mcp/providers/codebuddy.mjs +427 -427
  21. package/mcp/providers/codex.mjs +239 -0
  22. package/package.json +6 -2
  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/codex-first/agent.cordis.yml +136 -0
  28. package/preset/codex-first/cli-bridge.mjs +160 -0
  29. package/preset/codex-first/plugin-list.mjs +28 -0
  30. package/preset/codex-first/preset.yml +3 -0
  31. package/providers/claude-exe.mjs +112 -0
  32. package/providers/claude-models.mjs +338 -0
  33. package/providers/claude-policy.mjs +37 -0
  34. package/providers/claude.mjs +561 -0
  35. package/providers/codebuddy-catalog.mjs +367 -599
  36. package/providers/codebuddy-consts.mjs +72 -72
  37. package/providers/codebuddy-models-source.mjs +254 -0
  38. package/providers/codebuddy-models-tool.mjs +106 -106
  39. package/providers/codebuddy-schemas.mjs +46 -46
  40. package/providers/codebuddy.mjs +600 -578
  41. package/providers/codex-cli.mjs +405 -0
  42. package/providers/codex-models.mjs +107 -0
  43. package/providers/codex.mjs +585 -0
  44. package/providers/mimo-pure.mjs +270 -111
  45. package/providers/mimo.mjs +453 -517
  46. package/providers/opencode-exe.mjs +190 -63
  47. package/providers/opencode.mjs +553 -587
  48. package/providers/registry.mjs +25 -1
  49. package/scripts/build.mjs +261 -262
  50. package/scripts/verify.mjs +8 -2
@@ -0,0 +1,239 @@
1
+ // mcp/providers/codex.mjs — codex MCP 工具面(契约 §4)。
2
+ // framing/生命周期归 keystone 统一服务器 mcp/cli-bridge-mcp.mjs。
3
+ //
4
+ // 与 opencode/agy 面结构同构:直接 spawn codex(CODEX_BIN 覆盖;.js/.mjs 目标经
5
+ // process.execPath 包装,供测试夹具使用)。无 ctx.emit:状态走快照文件
6
+ // {homeDir}/cli-bridge-mcp-codex.json(bridge-kit buildMcpBridgePayload + 原子写 tmp→rename)。
7
+ //
8
+ // codex 面的两处特有事实(实测 codex-cli 0.98.0):
9
+ // • 续聊走 `codex exec resume <thread_id> <prompt>`,且 resume **不接受 -C/-s/-o/-add-dir**;
10
+ // 所以 cwd 只由 spawn 的 cwd 决定,argv 里的沙箱经 -c sandbox_mode 落地。
11
+ // • 模型清单没有 CLI 子命令,只能走 HTTP /models(descriptor 的 codex_models 负责),
12
+ // 这里只做工具面转接,不复制解析逻辑。
13
+ import { spawn as nodeSpawn } from 'node:child_process'
14
+ import { resolve as resolvePath } from 'node:path'
15
+ import { readFileSync } from 'node:fs'
16
+ import { dirname } from 'node:path'
17
+
18
+ import defineProvider, { codexResolveExePrefix, codexExeCandidates } from '../../providers/codex.mjs'
19
+ import { codexShimRejectReason } from '../../providers/codex-cli.mjs'
20
+ import { createStatusEngine, createLineStream, buildMcpBridgePayload, isLimited, renderStatus } from '../../core/bridge-kit.mjs'
21
+ import { packageVersion } from '../version.mjs'
22
+
23
+ export const meta = { id: 'codex', label: 'CODEX', version: packageVersion() }
24
+
25
+ // ── 统一设置文件分区读(MCP 子进程无 ctx.get,只读 env/文件)───────────────────
26
+ // 与 opencode/mimo 面同规则:注入 descriptor 的 readSettings,使面板里指定的
27
+ // binPath/defaultModel/sandboxMode 在 MCP 形态同样生效(三形态行为一致)。
28
+ const SETTINGS_FILE_NAME = 'cli-bridge-settings.json'
29
+
30
+ function resolveSettingsFile(env) {
31
+ try {
32
+ if (env.CLI_BRIDGE_SETTINGS_FILE) return env.CLI_BRIDGE_SETTINGS_FILE
33
+ if (env.DSH_HOME) return resolvePath(env.DSH_HOME, SETTINGS_FILE_NAME)
34
+ if (process.platform === 'win32' && env.APPDATA) return resolvePath(env.APPDATA, 'DSH Desktop', 'dsh-home', SETTINGS_FILE_NAME)
35
+ if (env.HOME) return resolvePath(env.HOME, '.dsh', SETTINGS_FILE_NAME)
36
+ } catch (e) { /* 无 env 能力 */ }
37
+ return null
38
+ }
39
+
40
+ function readPartition(env) {
41
+ const p = resolveSettingsFile(env)
42
+ if (!p) return { partition: null, path: null, home: null }
43
+ try {
44
+ const all = JSON.parse(readFileSync(p, 'utf8'))
45
+ return { partition: (all && typeof all === 'object' && all.codex) ? all.codex : null, path: p, home: dirname(p) }
46
+ } catch (e) {
47
+ return { partition: null, path: p, home: dirname(p) }
48
+ }
49
+ }
50
+
51
+ const provider = defineProvider({
52
+ env: process.env,
53
+ process,
54
+ nodeChild: { spawn: nodeSpawn },
55
+ readSettings: () => readPartition(process.env)
56
+ })
57
+
58
+ let lastIo = null
59
+ function publishSnapshot(snapshot) {
60
+ const io = lastIo
61
+ if (!io) return
62
+ try {
63
+ const target = io.path.join(io.homeDir, 'cli-bridge-mcp-codex.json')
64
+ const payload = buildMcpBridgePayload(snapshot, process.pid, Date.now())
65
+ const tmp = target + '.' + process.pid + '.tmp'
66
+ try { io.fs.mkdirSync(io.path.dirname(target), { recursive: true }) } catch (e) { /* 目录已存在 */ }
67
+ io.fs.writeFileSync(tmp, JSON.stringify(payload), 'utf8')
68
+ io.fs.renameSync(tmp, target)
69
+ } catch (e) { /* 灯数据尽力而为,绝不影响工具调用 */ }
70
+ }
71
+
72
+ // provider.engine 在 attach 后才存在;MCP 形态不 attach,用本模块的 engine。
73
+ const engine = createStatusEngine({ provider, publish: publishSnapshot })
74
+ provider.engine = engine
75
+
76
+ // ── exe / cwd 解析 ────────────────────────────────────────────────────────────
77
+ function exePrefix() {
78
+ return codexResolveExePrefix(process.env, '')
79
+ }
80
+
81
+ function resolveCwd(a) {
82
+ if (a && a.cwd) return resolvePath(String(a.cwd))
83
+ return process.env.CODEX_BRIDGE_CWD || process.cwd()
84
+ }
85
+
86
+ function baseResult(over) {
87
+ return Object.assign({
88
+ ok: false, status: 'ERROR', response: '', sessionId: null, durationSeconds: null,
89
+ numTurns: null, totalTokens: null, exitCode: null, mode: 'auto', backend: null, stderr: ''
90
+ }, over)
91
+ }
92
+
93
+ // ── spawn(实时折叠走 bridge-kit 行流)────────────────────────────────────────
94
+ function runCodex(args) {
95
+ return new Promise((resolve) => {
96
+ const a = args || {}
97
+ const built = provider.buildArgv(exePrefix(), a, { planActive: false, defaultMode: 'auto' })
98
+ const cwd = resolveCwd(a)
99
+ // .cmd/.bat shim:无 shell 的 spawn 解析不了它,走 shell 会二次解释 prompt。
100
+ const shim = codexShimRejectReason(built.argv[0]) || codexShimRejectReason(built.argv[1])
101
+ if (shim) {
102
+ const res = baseResult({ status: 'CODEX_UNAVAILABLE', mode: built.mode, stderr: shim })
103
+ engine.begin(cwd); engine.end(res, cwd)
104
+ resolve(res)
105
+ return
106
+ }
107
+ let child
108
+ try {
109
+ child = nodeSpawn(built.argv[0], built.argv.slice(1), { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
110
+ } catch (e) {
111
+ resolve(baseResult({ status: 'SPAWN_ERROR', mode: built.mode, stderr: String(e && e.message || e) }))
112
+ return
113
+ }
114
+ engine.begin(cwd)
115
+ let out = ''
116
+ let err = ''
117
+ let killed = false
118
+ const timer = setTimeout(() => { killed = true; try { child.kill() } catch (e) { /* 已退出 */ } }, (built.timeoutSec + 60) * 1000)
119
+ const stream = createLineStream((ln) => {
120
+ const t = ln.trim()
121
+ if (t.charAt(0) !== '{') return
122
+ try { engine.foldEvent(JSON.parse(t), cwd) } catch (e) { /* 坏行跳过 */ }
123
+ })
124
+ child.stdout.on('data', (d) => {
125
+ out += d
126
+ if (out.length > 4000000) out = out.slice(-2000000)
127
+ stream.pushChunk(String(d))
128
+ })
129
+ child.stderr.on('data', (d) => { err += d; if (err.length > 1000000) err = err.slice(-500000) })
130
+ child.on('error', (e) => {
131
+ clearTimeout(timer)
132
+ stream.flush()
133
+ const res = baseResult({ status: 'CODEX_UNAVAILABLE', mode: built.mode, stderr: 'codex spawn failed: ' + String(e && e.message || e) })
134
+ engine.end(res, cwd)
135
+ resolve(res)
136
+ })
137
+ child.on('close', (code) => {
138
+ clearTimeout(timer)
139
+ stream.flush()
140
+ const outcome = { exitCode: killed ? 124 : code }
141
+ let res = provider.buildResult(provider.parseJson(out), outcome, built.mode, err, out, null)
142
+ if (killed && !res.ok) {
143
+ res.status = 'HUNG_TIMEOUT'
144
+ res.stderr = (res.stderr ? res.stderr + ' ' : '') + '[killed by DSH timeout guard after ' + built.timeoutSec + 's; if this was a long-running script (build/test) raise timeoutSec]'
145
+ }
146
+ engine.end(res, cwd)
147
+ resolve(res)
148
+ })
149
+ })
150
+ }
151
+
152
+ function textResult(res) {
153
+ if (res.fallback) {
154
+ return 'codex unavailable (' + res.reason + '). Use native tools / the local model for this task; do not retry codex.'
155
+ }
156
+ const limited = !res.ok && isLimited(res)
157
+ const head = 'codex ' + (res.ok ? 'OK' : 'FAILED') + ' [status=' + res.status +
158
+ (res.sessionId ? ' session=' + res.sessionId : '') +
159
+ (res.totalTokens != null ? ' tokens=' + res.totalTokens : '') + ']'
160
+ const hint = typeof provider.failureHint === 'function' ? provider.failureHint(res) : ''
161
+ let note = ''
162
+ if (limited) note = '\n\n[Note: this looks like a rate-limit / network failure. Do NOT retry codex in a loop; finish the task with your own tools, or ask the user.]'
163
+ const body = res.response || (res.stderr ? '[stderr] ' + res.stderr : (res.rawStdout ? '[raw] ' + res.rawStdout : ''))
164
+ return head + (body ? '\n\n' + body : '') + (hint ? '\n\n' + hint : '') + note
165
+ }
166
+
167
+ // ── 工具面(与描述符同源:名字/schema 一律从 descriptor 取,禁止第二份定义)──────
168
+ function toolList(env) {
169
+ const d = provider.tools
170
+ const extra = provider.extraTools || []
171
+ return [d.run, d.continue, d.status].concat(extra.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })))
172
+ .map((t) => Object.assign({}, t))
173
+ }
174
+
175
+ export function tools(env) {
176
+ return toolList(env)
177
+ }
178
+
179
+ export async function call(name, args, io) {
180
+ if (io) lastIo = io
181
+ if (!lastIo) return 'codex error: MCP io not initialized'
182
+ const a = args || {}
183
+ const d = provider.tools
184
+ if (name === d.status.name) {
185
+ const snap = provider.statusToolLogic(a)
186
+ if (a.cwd) {
187
+ // statusToolLogic 已经按 cwd 过滤并升格顶层字段,这里直接渲染。
188
+ }
189
+ return renderStatus(snap, provider)[0].text
190
+ }
191
+ if (name === (provider.extraTools[0] && provider.extraTools[0].name)) {
192
+ const res = await provider.extraTools[0].logic(a)
193
+ const rendered = provider.extraTools[0].render(a, res)
194
+ return rendered[0].text
195
+ }
196
+ if (!a.prompt || !String(a.prompt).trim()) return 'codex error: prompt is required'
197
+ const mapped = Object.assign({}, a)
198
+ if (name === d.continue.name) {
199
+ // continue 的会话语义:sessionId 优先,否则 latest → exec resume --last。
200
+ if (!mapped.sessionId && mapped.latest) mapped.continueLatest = true
201
+ }
202
+ const res = await runCodex(mapped)
203
+ return textResult(res)
204
+ }
205
+
206
+ export function check(env) {
207
+ const ts = tools(env)
208
+ const names = ts.map((t) => t.name)
209
+ for (const t of ts) {
210
+ if (!t.name || !t.description || !t.inputSchema || t.inputSchema.type !== 'object') throw new Error('codex mcp: bad tool def ' + JSON.stringify(t && t.name))
211
+ }
212
+ // MCP 面必须覆盖 descriptor 三件套(名字一致,提示词连续性红线)。
213
+ for (const k of ['run', 'continue', 'status']) {
214
+ if (!names.includes(provider.tools[k].name)) throw new Error('codex mcp: tools surface missing descriptor name ' + provider.tools[k].name)
215
+ }
216
+ // argv 契约漂移锁(实测 codex-cli 0.98.0 的旗集)。
217
+ const b = provider.buildArgv(['codex'], { prompt: 'x' }, { planActive: false })
218
+ for (const f of ['exec', '--json', '--skip-git-repo-check']) {
219
+ if (b.argv.indexOf(f) < 0) throw new Error('codex mcp: argv contract drift: ' + f)
220
+ }
221
+ // ⚠️ codex exec 没有 -a;出现即参数错误(实测 clap: unexpected argument '-a')。
222
+ if (b.argv.indexOf('-a') >= 0 || b.argv.indexOf('--ask-for-approval') >= 0) throw new Error('codex mcp: codex exec rejects -a/--ask-for-approval; use -c approval_policy')
223
+ // resume 子命令不接受 -C/-s/-L:argv 必须改成 -c sandbox_mode。
224
+ const r = provider.buildArgv(['codex'], { prompt: 'x', sessionId: 'sid-1' }, { planActive: false })
225
+ for (const f of ['resume', 'sid-1', '--json']) {
226
+ if (r.argv.indexOf(f) < 0) throw new Error('codex mcp: resume argv contract drift: ' + f)
227
+ }
228
+ for (const f of ['-C', '-s', '--add-dir']) {
229
+ if (r.argv.indexOf(f) >= 0) throw new Error('codex mcp: resume must not emit ' + f + ' (codex exec resume rejects it)')
230
+ }
231
+ if (r.argv.indexOf('-c') < 0 || String(r.argv[r.argv.indexOf('-c') + 1] || '').indexOf('sandbox_mode=') < 0) {
232
+ throw new Error('codex mcp: resume must set the sandbox through -c sandbox_mode')
233
+ }
234
+ // 模型清单在 MCP 面必须点名真实端点而非本地目录(红线:不把本地目录冒充实时)。
235
+ const mt = ts.find((t) => /_models$/.test(t.name))
236
+ if (!mt) throw new Error('codex mcp: missing codex_models tool')
237
+ if (!/live/.test(String(mt.description))) throw new Error('codex mcp: codex_models must document the live source')
238
+ return true
239
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@luchenglong/cli-bridge",
3
- "version": "0.2.8",
4
- "description": "CLI Bridge for DeepSeek Harness (DSH): one package bridging four local coding-CLI agents — agy (Google Antigravity CLI), Tencent CodeBuddy Code, Xiaomi MiMo (MiMoCode / MiMo Desktop), and OpenCode. Each provider keeps its own agent preset (cordis-agy / codebuddy-first / cordis-mimo / opencode-first), tools (agy_run / codebuddy_run / mimo_run / opencode_run + continue + status), rate-limit/network fallback dialog, and zero-dependency MCP server, unified under a single home-level status light, one settings file, and one bundle patch. Merges agy-first-bridge, codebuddy-first-bridge and mimo-first-bridge.",
3
+ "version": "0.2.10",
4
+ "description": "CLI Bridge for DeepSeek Harness (DSH): one package bridging six local coding-CLI agents — agy (Google Antigravity CLI), Tencent CodeBuddy Code, Xiaomi MiMo (MiMoCode), OpenCode, Claude Code, and OpenAI Codex CLI. Each provider keeps its own agent preset (cordis-agy / codebuddy-first / cordis-mimo / opencode-first / claude-first / codex-first), tools (<id>_run / <id>_continue / <id>_status, plus per-provider model listing), rate-limit/network fallback dialog, and zero-dependency MCP server, unified under a single home-level status light, one settings file, and one bundle patch. Merges agy-first-bridge, codebuddy-first-bridge and mimo-first-bridge.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -20,10 +20,14 @@
20
20
  "./providers/codebuddy": "./providers/codebuddy.mjs",
21
21
  "./providers/mimo": "./providers/mimo.mjs",
22
22
  "./providers/opencode": "./providers/opencode.mjs",
23
+ "./providers/claude": "./providers/claude.mjs",
24
+ "./providers/codex": "./providers/codex.mjs",
23
25
  "./preset-agy": "./preset/agy-first/cli-bridge.mjs",
24
26
  "./preset-codebuddy": "./preset/codebuddy-first/cli-bridge.mjs",
25
27
  "./preset-mimo": "./preset/mimo-first/cli-bridge.mjs",
26
28
  "./preset-opencode": "./preset/opencode-first/cli-bridge.mjs",
29
+ "./preset-claude": "./preset/claude-first/cli-bridge.mjs",
30
+ "./preset-codex": "./preset/codex-first/cli-bridge.mjs",
27
31
  "./package.json": "./package.json"
28
32
  },
29
33
  "dsh": {
@@ -0,0 +1,136 @@
1
+ # 生成物 — 由 node scripts/build.mjs 从 preset/claude-first/plugin-list.mjs 生成,勿手改。
2
+ # legacy 目录形态($DSH_HOME/.agent-presets/claude-first/);新 DSH 走声明制
3
+ # (home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs 直接 import 同一源)。
4
+ - id: persona
5
+ name: '@deepseek-ai/dsh-persona'
6
+ config:
7
+ prefix: 'You are a coding agent powered by the {{model}} model.'
8
+ - id: agent-instructions
9
+ name: '@deepseek-ai/dsh-agent-instructions'
10
+ config:
11
+ maxBytes: 65536
12
+ - id: tool-bash
13
+ name: '@deepseek-ai/dsh-tool-bash'
14
+ disabled: !!js 'process.platform === ''win32'''
15
+ - id: tool-pwsh
16
+ name: '@deepseek-ai/dsh-tool-pwsh'
17
+ disabled: !!js 'process.platform !== ''win32'''
18
+ - id: tool-fs
19
+ name: '@deepseek-ai/dsh-tool-fs'
20
+ - id: tool-fs-search
21
+ name: '@deepseek-ai/dsh-tool-fs-search'
22
+ config:
23
+ sampleOverCapGlobResults: false
24
+ - id: tool-jobs
25
+ name: '@deepseek-ai/dsh-tool-jobs'
26
+ - id: skill-filesystem
27
+ name: '@deepseek-ai/dsh-skill-filesystem'
28
+ - id: tool-skill
29
+ name: '@deepseek-ai/dsh-tool-skill'
30
+ - id: command-goal
31
+ name: '@deepseek-ai/dsh-command-goal'
32
+ - id: tool-goal
33
+ name: '@deepseek-ai/dsh-tool-goal'
34
+ - id: planning
35
+ name: 'cordis:group'
36
+ group: true
37
+ isolate:
38
+ planMode: true
39
+ config:
40
+ - id: plan-mode
41
+ name: '@deepseek-ai/dsh-plan-mode'
42
+ config:
43
+ section: |
44
+ You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
45
+
46
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
47
+
48
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
49
+
50
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
51
+
52
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
53
+
54
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
55
+ - id: compaction
56
+ name: 'cordis:group'
57
+ group: true
58
+ isolate:
59
+ compaction: true
60
+ toolResultPruner: true
61
+ config:
62
+ - id: compaction-basic
63
+ name: '@deepseek-ai/dsh-compaction-basic'
64
+ - id: command-compact
65
+ name: '@deepseek-ai/dsh-command-compact'
66
+ - id: tool-result-pruner
67
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
68
+ config:
69
+ thresholdChars: 8192
70
+ headChars: 4096
71
+ tailChars: 1024
72
+ - id: delegation
73
+ name: 'cordis:group'
74
+ group: true
75
+ isolate:
76
+ workflowEngine: true
77
+ config:
78
+ - id: tool-subagent-control
79
+ name: '@deepseek-ai/dsh-tool-subagent-control'
80
+ - id: tool-subagent-list-agents
81
+ name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
82
+ - id: tool-subagent
83
+ name: '@deepseek-ai/dsh-tool-subagent'
84
+ config:
85
+ provider: spawn
86
+ toolName: subagent
87
+ modelSelectionSettings: true
88
+ - id: tool-subagent-fork
89
+ name: '@deepseek-ai/dsh-tool-subagent'
90
+ config:
91
+ provider: fork
92
+ toolName: subagent_fork
93
+ - id: tool-subagent-codex
94
+ name: '@deepseek-ai/dsh-tool-subagent'
95
+ disabled: true
96
+ config:
97
+ provider: codex
98
+ toolName: subagent_codex
99
+ maxDepth: provider-managed
100
+ - id: tool-subagent-claude-code
101
+ name: '@deepseek-ai/dsh-tool-subagent'
102
+ disabled: true
103
+ config:
104
+ provider: claude-code
105
+ toolName: subagent_claude_code
106
+ maxDepth: provider-managed
107
+ - id: workflow-ptc
108
+ name: '@deepseek-ai/dsh-workflow-ptc'
109
+ config:
110
+ provider: spawn
111
+ - id: tool-workflow
112
+ name: '@deepseek-ai/dsh-tool-workflow'
113
+ - id: tool-ralph
114
+ name: '@deepseek-ai/dsh-tool-ralph'
115
+ disabled: true
116
+ config:
117
+ subagentProvider: spawn
118
+ maxRounds: 64
119
+ - id: tool-ask-user
120
+ name: '@deepseek-ai/dsh-tool-ask-user'
121
+ - id: tool-todo
122
+ name: '@deepseek-ai/dsh-tool-todo'
123
+ config:
124
+ allowParallelInProgress: true
125
+ - id: tool-web
126
+ name: '@deepseek-ai/dsh-tool-web'
127
+ config:
128
+ fetch: true
129
+ searchTimeoutMs: 60000
130
+ - id: present
131
+ name: '@deepseek-ai/dsh-tool-present'
132
+ - id: tool-plugin-manager
133
+ name: '@deepseek-ai/dsh-plugin-manager/tools'
134
+ disabled: true
135
+ - id: cli-bridge-claude
136
+ name: '@luchenglong/cli-bridge/preset-claude'
@@ -0,0 +1,131 @@
1
+ // preset/claude-first/cli-bridge.mjs — claude-first preset 的桥接插件(preset 宿主形态)。
2
+ //
3
+ // PROVIDER-CONTRACT.md §5:本模块只做「宿主接线」——构造描述符、attach、注册三工具 +
4
+ // claude_models、心跳/事件发布、claude:policy 提示词段。全部执行/状态语义在
5
+ // providers/claude.mjs + core/bridge-kit.mjs。
6
+ //
7
+ // preset id 'claude-first' 由 plugin-list.mjs 单源声明(本文件不含 id 字面量清单)。
8
+ //
9
+ // 本模块是宿主进程内真 ESM:可 import node: 内建与包内相对路径;禁止 import
10
+ // @deepseek-ai/*(解析根够不到 harness 包,历史论证 —— 契约 §8)。
11
+ import { existsSync } from 'node:fs'
12
+
13
+ import defineProvider, { claudeExeCandidates } from '../../providers/claude.mjs'
14
+ import { POLICY_TEXT } from '../../providers/claude-policy.mjs'
15
+ import { renderResult, renderStatus } from '../../core/bridge-kit.mjs'
16
+ import { isBridgeEnabled } from '../../core/scheduling.mjs'
17
+ import { PROVIDER_IDS } from '../../providers/registry.mjs'
18
+
19
+ export const name = 'cli-bridge-claude'
20
+ export const inject = ['tools', 'subprocess', 'systemPrompt', 'timer']
21
+
22
+ const OUTPUT_SCHEMA = { type: 'object', additionalProperties: true }
23
+
24
+ export function apply(ctx) {
25
+ // 调度门禁:用户在设置面板关掉 claude → 本形态不注册任何工具/政策段/心跳。
26
+ // 每次会话装配时现读文件(改设置下次会话即生效,无需重启宿主);fail-open。
27
+ if (!isBridgeEnabled('claude', PROVIDER_IDS)) return
28
+ const subprocess = ctx.subprocess
29
+ const sandboxPolicy = ctx.get('sandboxPolicy')
30
+
31
+ const provider = defineProvider({ process, env: process.env })
32
+
33
+ // 状态快照 → 家级灯(keystone 听 'claude/status',事件是 app 级广播)。
34
+ function publish(snapshot) {
35
+ try { ctx.emit('claude/status', { snapshot }) } catch (e) { /* ignore */ }
36
+ }
37
+
38
+ // cwd 兜底:显式 cwd 参数 > env CLAUDE_MCP_CWD > 沙箱 workspaceRoot > process.cwd()。
39
+ function getCwdFallback() {
40
+ if (process.env.CLAUDE_MCP_CWD) return String(process.env.CLAUDE_MCP_CWD)
41
+ if (sandboxPolicy && typeof sandboxPolicy.workspaceRoot === 'string' && sandboxPolicy.workspaceRoot) return sandboxPolicy.workspaceRoot
42
+ return process.cwd()
43
+ }
44
+
45
+ // 设置面板分区(每次 getSettings 现读 ctx.get,面板改默认模型后下一次调用即生效)。
46
+ function panelSettings() {
47
+ try {
48
+ const s = ctx.get && ctx.get('cliBridgeSettings')
49
+ return (s && s.claude) || null
50
+ } catch (e) { return null }
51
+ }
52
+
53
+ // exe 解析优先级(与描述符 claudeExeCandidates 同一套):
54
+ // 面板 binPath > env CLAUDE_BIN/CLAUDE_CLI/CLAUDE_CODE_BIN > 已知本地安装 > PATH 名 claude。
55
+ // .js/.mjs 目标经 process.execPath 包装(返回数组前缀,buildArgv 接受)。
56
+ // ⚠️ npm 全局安装会在 Windows 给 claude.cmd —— 本形态经 DSH subprocess 启动,.cmd 能否
57
+ // 执行取决于宿主;探测不到原生 exe 时如实落到 resolveExecutable,失败会明确报
58
+ // CLAUDE_UNAVAILABLE/SPAWN_ERROR(绝不静默改走别的后端)。
59
+ const binPathSetting = (() => {
60
+ const s = panelSettings()
61
+ const v = s ? s.binPath : ''
62
+ return typeof v === 'string' ? v : ''
63
+ })()
64
+ const exeCandidates = claudeExeCandidates(process.env, binPathSetting)
65
+
66
+ async function resolveExe(backend, signal) {
67
+ for (const c of exeCandidates) {
68
+ if (!c) continue
69
+ if (/\.(mjs|cjs|js)$/i.test(c)) return [process.execPath, c]
70
+ try { if (existsSync(c)) return c } catch (e) { }
71
+ }
72
+ return await subprocess.resolveExecutable('claude', undefined, signal)
73
+ }
74
+
75
+ provider.attach({
76
+ ctx, subprocess, resolveExe, getCwdFallback, publish,
77
+ getSettings: () => {
78
+ const s = panelSettings()
79
+ return s && typeof s === 'object' ? s : {}
80
+ }
81
+ })
82
+
83
+ // 宣告 claude 优先模式:家级灯据此决定常驻显示(presetActive)。挂载即宣告 +
84
+ // 每 30s 心跳续租(TTL 75s,契约 §2)。
85
+ function announceMode() {
86
+ try {
87
+ const sid = (ctx.session && ctx.session.id) || ctx.sessionId || null
88
+ ctx.emit('claude/mode', sid ? { active: true, sessionId: sid } : { active: true })
89
+ } catch (e) { /* ignore */ }
90
+ }
91
+ announceMode()
92
+ try { const t = ctx.setInterval ? ctx.setInterval(announceMode, 30000) : null; if (t && ctx.effect) ctx.effect(() => () => { try { t() } catch (e) {} }) } catch (e) {}
93
+
94
+ // ── 工具注册(执行进描述符三逻辑 + extraTools)──────────────────────────────
95
+ ctx.effect(() => ctx.tools.register({
96
+ name: provider.tools.run.name,
97
+ description: provider.tools.run.description,
98
+ parameters: provider.tools.run.inputSchema,
99
+ output: { schema: OUTPUT_SCHEMA, render: (args, value) => renderResult(value, provider) },
100
+ execute: (args, exec) => provider.runToolLogic(args, exec)
101
+ }))
102
+ ctx.effect(() => ctx.tools.register({
103
+ name: provider.tools.continue.name,
104
+ description: provider.tools.continue.description,
105
+ parameters: provider.tools.continue.inputSchema,
106
+ output: { schema: OUTPUT_SCHEMA, render: (args, value) => renderResult(value, provider) },
107
+ execute: (args, exec) => provider.continueToolLogic(args, exec)
108
+ }))
109
+ ctx.effect(() => ctx.tools.register({
110
+ name: provider.tools.status.name,
111
+ description: provider.tools.status.description,
112
+ parameters: provider.tools.status.inputSchema,
113
+ output: { schema: OUTPUT_SCHEMA, render: (args, value) => renderStatus(value, provider) },
114
+ execute: (args) => provider.statusToolLogic(args)
115
+ }))
116
+ for (const et of provider.extraTools || []) {
117
+ ctx.effect(() => ctx.tools.register({
118
+ name: et.name,
119
+ description: et.description,
120
+ parameters: et.inputSchema,
121
+ output: { schema: OUTPUT_SCHEMA, render: et.render || ((args, value) => renderResult(value, provider)) },
122
+ execute: (args, exec) => et.logic(args, exec)
123
+ }))
124
+ }
125
+
126
+ ctx.effect(() => ctx.systemPrompt.section({
127
+ name: 'claude:policy',
128
+ order: (typeof ctx.systemPrompt.getSectionOrder === 'function' ? ctx.systemPrompt.getSectionOrder('TOOL_SUBAGENT') : 2800) + 10,
129
+ text: POLICY_TEXT
130
+ }))
131
+ }
@@ -0,0 +1,28 @@
1
+ // preset/claude-first/plugin-list.mjs — claude-first 预设的单源插件清单(PROVIDER-CONTRACT §5)。
2
+ //
3
+ // = 官方 standard 基线(standard-base.mjs)+ claude 桥接行 + persona 差异。
4
+ // preset id 'claude-first':本包新增 provider,无旧包兼容包袱,id 直接用 provider id + '-first'
5
+ // (与 opencode-first 同构;cordis-agy / cordis-mimo 的 cordis-* 前缀是旧包历史遗留,不可套用)。
6
+ import { standardPluginRows, bridgeRow } from '../standard-base.mjs'
7
+
8
+ export const meta = {
9
+ id: 'claude-first',
10
+ name: 'Claude Code 执行代理',
11
+ description: '完整编码 Agent(standard 全部能力),额外提供 claude_run/claude_continue 工具并注入"claude 优先"策略:各种模式下先把实现、编辑、调试、构建、跨文件排查派给本机 Claude Code CLI(DSH 完全控制、claude 无提示改文件、默认后台执行),原生工具仅用于只读查询与最终验证。模型清单以服务端权威探测为准(claude_models)。',
12
+ order: 50
13
+ }
14
+
15
+ export function buildPlugins(deps) {
16
+ const rows = standardPluginRows()
17
+ // persona 段:prefix 必填;**不设 suffix** —— DSH 只注册 provider/model 两个提示词变量,
18
+ // 写 {{cwd}} 会在组装期抛 unknown prompt variable,使整个预设不可用(工作目录由
19
+ // working-directory 服务以不插值的用户上下文提供)。
20
+ const persona = rows.find((r) => r.id === 'persona')
21
+ if (persona && persona.config) {
22
+ persona.config.prefix = 'You are a coding agent powered by the {{model}} model.'
23
+ delete persona.config.suffix
24
+ }
25
+ // 桥接行:第二参数由 build(scripts/build.mjs)注入裸包说明符,绝不在这里写死路径。
26
+ rows.push(bridgeRow('claude', deps && deps.bridgeName))
27
+ return rows
28
+ }
@@ -0,0 +1,3 @@
1
+ name: Claude Code 执行代理
2
+ description: '完整编码 Agent(standard 全部能力),额外提供 claude_run/claude_continue 工具并注入"claude 优先"策略:各种模式下先把实现、编辑、调试、构建、跨文件排查派给本机 Claude Code CLI(DSH 完全控制、claude 无提示改文件、默认后台执行),原生工具仅用于只读查询与最终验证。模型清单以服务端权威探测为准(claude_models)。'
3
+ order: 50