@luchenglong/cli-bridge 0.2.8 → 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 (35) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/PROVIDER-CONTRACT.md +53 -4
  3. package/README.md +12 -11
  4. package/dynamic/agy.host.js +1 -1
  5. package/dynamic/claude.host.js +1801 -0
  6. package/dynamic/claude.host.template.mjs +108 -0
  7. package/dynamic/codebuddy.host.js +1 -1
  8. package/dynamic/codex.host.js +1955 -0
  9. package/dynamic/codex.host.template.mjs +137 -0
  10. package/dynamic/mimo.host.js +1 -1
  11. package/dynamic/opencode.host.js +1 -1
  12. package/home-plugin/cli-bridge-indicator/lib/client.js +600 -606
  13. package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +2 -3
  14. package/mcp/cli-bridge-mcp.mjs +5 -3
  15. package/mcp/providers/claude.mjs +231 -0
  16. package/mcp/providers/codex.mjs +239 -0
  17. package/package.json +6 -2
  18. package/preset/claude-first/agent.cordis.yml +136 -0
  19. package/preset/claude-first/cli-bridge.mjs +131 -0
  20. package/preset/claude-first/plugin-list.mjs +28 -0
  21. package/preset/claude-first/preset.yml +3 -0
  22. package/preset/codex-first/agent.cordis.yml +136 -0
  23. package/preset/codex-first/cli-bridge.mjs +160 -0
  24. package/preset/codex-first/plugin-list.mjs +28 -0
  25. package/preset/codex-first/preset.yml +3 -0
  26. package/providers/claude-exe.mjs +112 -0
  27. package/providers/claude-models.mjs +334 -0
  28. package/providers/claude-policy.mjs +37 -0
  29. package/providers/claude.mjs +508 -0
  30. package/providers/codex-cli.mjs +471 -0
  31. package/providers/codex-models.mjs +107 -0
  32. package/providers/codex.mjs +544 -0
  33. package/providers/registry.mjs +25 -1
  34. package/scripts/build.mjs +1 -2
  35. package/scripts/verify.mjs +8 -2
@@ -19,13 +19,12 @@ import { dirname, join } from 'node:path'
19
19
  import { existsSync } from 'node:fs'
20
20
  import { fileURLToPath } from 'node:url'
21
21
  import { resolveScheduling } from '../../../core/scheduling.mjs'
22
- import { PROVIDER_IDS } from '../../../providers/registry.mjs'
22
+ import { PROVIDER_IDS, PRESET_DIRS } from '../../../providers/registry.mjs'
23
23
 
24
24
  export const PKG_NAME = '@luchenglong/cli-bridge'
25
25
 
26
- // preset 目录名映射(与 scripts/build.mjs 的 PRESET_DIRS 一致;目录名带 -first 后缀是
26
+ // preset 目录名映射:单一来源见 providers/registry.mjs 的 PRESET_DIRS(勿在此重复硬编码)。
27
27
  // 三个旧包时代的遗留布局,provider id 不带 —— 这里不映射就永远找不到 plugin-list)。
28
- const PRESET_DIRS = { agy: 'agy-first', codebuddy: 'codebuddy-first', mimo: 'mimo-first', opencode: 'opencode-first' }
29
28
 
30
29
  // 【成员预设】0.2.1 起 dsh-tool-subagent 的 config 不再接受 persona/toolFilter(apply()
31
30
  // 里显式 throw),角色人格只能作为独立 Agent Preset 存在、由委派行经 agentPreset 选择。
@@ -28,13 +28,15 @@ import { resolveScheduling } from '../core/scheduling.mjs'
28
28
  import * as agyMcp from './providers/agy.mjs'
29
29
  import * as codebuddyMcp from './providers/codebuddy.mjs'
30
30
  import * as mimoMcp from './providers/mimo.mjs'
31
- import * as opencodeMcp from './providers/opencode.mjs'
31
+ import * as opencodeMcp from './providers/opencode.mjs'
32
+ import * as claudeMcp from './providers/claude.mjs'
33
+ import * as codexMcp from './providers/codex.mjs'
32
34
 
33
35
  const SERVER_NAME = 'cli-bridge-mcp'
34
- const SERVER_VERSION = '0.2.8'
36
+ const SERVER_VERSION = '0.2.9'
35
37
  const PROTOCOL = '2024-11-05'
36
38
 
37
- const PROVIDERS = [agyMcp, codebuddyMcp, mimoMcp, opencodeMcp]
39
+ const PROVIDERS = [agyMcp, codebuddyMcp, mimoMcp, opencodeMcp, claudeMcp, codexMcp]
38
40
 
39
41
  function detectHomeDir() {
40
42
  if (process.env.DSH_HOME) return process.env.DSH_HOME
@@ -0,0 +1,231 @@
1
+ // mcp/providers/claude.mjs — claude MCP 工具面(契约 §4)。
2
+ // framing/生命周期归 keystone 统一服务器 mcp/cli-bridge-mcp.mjs。
3
+ //
4
+ // 与 opencode 面结构同构:直接 spawn claude(原生 exe 直接用;npm 的 .cmd 经 shell:true,
5
+ // Windows 限制;.js/.mjs 目标经 process.execPath 包装,供测试 fixture 使用)。
6
+ // 无 ctx.emit:状态走快照文件 {homeDir}/cli-bridge-mcp-claude.json(bridge-kit
7
+ // buildMcpBridgePayload + 原子写 tmp→rename;写失败静默,绝不影响工具结果)。
8
+ //
9
+ // 工具名/schema 从描述符取(单一来源,防两处漂移),描述与 background 语义按 MCP 面重写:
10
+ // MCP 没有 DSH jobs,background 参数在该形态无意义,故 schema 里裁掉。
11
+ import { spawn as nodeSpawn } from 'node:child_process'
12
+ import { readFileSync } from 'node:fs'
13
+ import { resolve as resolvePath, dirname } from 'node:path'
14
+
15
+ import defineProvider, { resolveExePrefix } from '../../providers/claude.mjs'
16
+ import { splitExeSpec, quoteShellArg } from '../../providers/claude-exe.mjs'
17
+ import { createStatusEngine, createLineStream, buildMcpBridgePayload, isLimited, renderStatus } from '../../core/bridge-kit.mjs'
18
+ import { packageVersion } from '../version.mjs'
19
+
20
+ export const meta = { id: 'claude', label: 'CLAUDE', version: packageVersion() }
21
+
22
+ const SETTINGS_FILE_NAME = 'cli-bridge-settings.json'
23
+
24
+ // 统一设置文件分区读(MCP 子进程无 ctx.get,只读 env/文件)—— 设置面板里的
25
+ // binPath/defaultModel 在 MCP 形态同样生效(与 preset 行为一致)。
26
+ function resolveSettingsFile(env) {
27
+ try {
28
+ if (env.CLI_BRIDGE_SETTINGS_FILE) return env.CLI_BRIDGE_SETTINGS_FILE
29
+ if (env.DSH_HOME) return resolvePath(env.DSH_HOME, SETTINGS_FILE_NAME)
30
+ if (process.platform === 'win32' && env.APPDATA) return resolvePath(env.APPDATA, 'DSH Desktop', 'dsh-home', SETTINGS_FILE_NAME)
31
+ if (env.HOME) return resolvePath(env.HOME, '.dsh', SETTINGS_FILE_NAME)
32
+ } catch (e) { }
33
+ return null
34
+ }
35
+ function readPartition(env) {
36
+ const p = resolveSettingsFile(env)
37
+ if (!p) return { partition: null, path: null, home: null }
38
+ try {
39
+ const all = JSON.parse(readFileSync(p, 'utf8'))
40
+ return { partition: (all && typeof all === 'object' && all.claude) ? all.claude : null, path: p, home: dirname(p) }
41
+ } catch (e) {
42
+ return { partition: null, path: p, home: dirname(p) }
43
+ }
44
+ }
45
+
46
+ const provider = defineProvider({
47
+ env: process.env,
48
+ process,
49
+ nodeChild: { spawn: nodeSpawn },
50
+ readSettings: () => readPartition(process.env)
51
+ })
52
+
53
+ let lastIo = null
54
+ function publishSnapshot(snapshot) {
55
+ const io = lastIo
56
+ if (!io) return
57
+ try {
58
+ const target = io.path.join(io.homeDir, 'cli-bridge-mcp-claude.json')
59
+ const payload = buildMcpBridgePayload(snapshot, process.pid, Date.now())
60
+ const tmp = target + '.' + process.pid + '.tmp'
61
+ try { io.fs.mkdirSync(io.path.dirname(target), { recursive: true }) } catch (e) {}
62
+ io.fs.writeFileSync(tmp, JSON.stringify(payload), 'utf8')
63
+ io.fs.renameSync(tmp, target)
64
+ } catch (e) { /* 灯数据尽力而为,绝不影响工具调用 */ }
65
+ }
66
+ const engine = createStatusEngine({ provider, publish: publishSnapshot })
67
+ provider.engine = engine
68
+
69
+ function exePrefix() { return resolveExePrefix(process.env, '') }
70
+ function resolveCwd(a) {
71
+ if (a && a.cwd) return resolvePath(String(a.cwd))
72
+ return process.env.CLAUDE_MCP_CWD || process.cwd()
73
+ }
74
+
75
+ // ── spawn(实时折叠走 bridge-kit 行流;.cmd 走 shell)─────────────────────────
76
+ function runClaude(args) {
77
+ return new Promise((resolve) => {
78
+ const built = provider.buildArgv(exePrefix(), args, { planActive: false, defaultMode: 'auto' })
79
+ const cwd = resolveCwd(args)
80
+ const cmd = built.argv[0]
81
+ const argv = built.argv.slice(1)
82
+ const isShell = /\.(cmd|bat)$/i.test(cmd)
83
+ const spawnCmd = isShell ? [cmd].concat(argv).map(quoteShellArg).join(' ') : cmd
84
+ let child
85
+ try {
86
+ child = nodeSpawn(spawnCmd, isShell ? [] : argv, { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: isShell })
87
+ } catch (e) {
88
+ resolve({ ok: false, status: 'SPAWN_ERROR', response: '', sessionId: null, durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode: built.mode, backend: null, stderr: String(e && e.message || e) })
89
+ return
90
+ }
91
+ engine.begin(cwd)
92
+ let out = ''
93
+ let err = ''
94
+ let killed = false
95
+ const timer = setTimeout(() => { killed = true; try { child.kill() } catch (e) {} }, (built.timeoutSec + 60) * 1000)
96
+ const stream = createLineStream((ln) => {
97
+ const t = ln.trim()
98
+ if (!t.startsWith('{')) return
99
+ try { engine.foldEvent(JSON.parse(t), cwd) } catch (e) {}
100
+ })
101
+ child.stdout.on('data', (d) => {
102
+ out += d
103
+ if (out.length > 4000000) out = out.slice(-2000000)
104
+ stream.pushChunk(String(d))
105
+ })
106
+ child.stderr.on('data', (d) => { err += d; if (err.length > 1000000) err = err.slice(-500000) })
107
+ child.on('error', (e) => {
108
+ clearTimeout(timer)
109
+ stream.flush()
110
+ const res = { ok: false, status: 'CLAUDE_UNAVAILABLE', response: '', sessionId: null, durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode: built.mode, backend: null, stderr: 'claude spawn failed: ' + String(e && e.message || e) }
111
+ engine.end(res, cwd)
112
+ resolve(res)
113
+ })
114
+ child.on('close', (code) => {
115
+ clearTimeout(timer)
116
+ stream.flush()
117
+ const outcome = { exitCode: killed ? 124 : code }
118
+ let res = provider.buildResult(provider.parseJson(out), outcome, built.mode, err, out, null)
119
+ if (killed && !res.ok) {
120
+ res.status = 'HUNG_TIMEOUT'
121
+ res.stderr = (res.stderr ? res.stderr + ' ' : '') + '[killed by DSH timeout guard after ' + built.timeoutSec + 's; if this was a long-running task raise timeoutSec]'
122
+ }
123
+ engine.end(res, cwd)
124
+ resolve(res)
125
+ })
126
+ })
127
+ }
128
+
129
+ function textResult(res) {
130
+ const head = 'claude ' + (res.ok ? 'OK' : 'FAILED') + ' [status=' + res.status +
131
+ (res.sessionId ? ' session=' + res.sessionId : '') +
132
+ (res.modelUsed ? ' model=' + res.modelUsed : '') +
133
+ (res.durationSeconds != null ? ' ' + res.durationSeconds + 's' : '') + ']'
134
+ let note = ''
135
+ if (!res.ok && isLimited(res)) note = '\n\n[Note: this looks like a rate-limit / network failure. Do NOT retry claude in a loop; finish the task with your own tools, or ask the user.]'
136
+ const hint = provider.failureHint(res)
137
+ const body = res.response || (res.stderr ? '[stderr] ' + res.stderr : (res.rawStdout ? '[raw] ' + res.rawStdout : ''))
138
+ return head + (body ? '\n\n' + body : '') + (hint ? '\n\n' + hint : '') + note
139
+ }
140
+
141
+ // ── 工具面 ────────────────────────────────────────────────────────────────────
142
+ function mcpSchema(name) {
143
+ const src = provider.tools[name].inputSchema
144
+ const clone = JSON.parse(JSON.stringify(src))
145
+ if (clone.properties) delete clone.properties.background
146
+ return clone
147
+ }
148
+
149
+ export function tools(env) {
150
+ const d = provider.tools
151
+ return [
152
+ {
153
+ name: d.run.name,
154
+ description: 'Dispatch a coding/build/debug/investigation task to the local Claude Code CLI and return its final answer. claude runs fully non-interactively (permissions approved, never blocks on a prompt) and streams live steps. Prefer it for implementation, multi-file edits, refactors and debugging; use your own tools for quick read-only lookups and final build/test verification. While it runs, call claude_status to watch what claude is doing live.',
155
+ inputSchema: mcpSchema('run')
156
+ },
157
+ {
158
+ name: d.continue.name,
159
+ description: 'Continue an existing claude session with a follow-up prompt, reusing claude context (passes --resume <sessionId>, or --continue with latest=true).',
160
+ inputSchema: mcpSchema('continue')
161
+ },
162
+ {
163
+ name: d.status.name,
164
+ 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, and the last completed run status + session id. Optional cwd filters to a single project.',
165
+ inputSchema: JSON.parse(JSON.stringify(d.status.inputSchema))
166
+ },
167
+ {
168
+ name: 'claude_models',
169
+ description: 'List the models the current claude account/endpoint really supports, read LIVE from the server (probes it once with a deliberately invalid model id and parses the "The supported API model names are …" answer). Use it to pick a model before claude_run. Returns ok:false with an actionable error when no live roster can be read — it never invents a list and never presents ANTHROPIC_*_MODEL aliases as the usable set.',
170
+ inputSchema: (function () {
171
+ const et = (provider.extraTools || [])[0]
172
+ return JSON.parse(JSON.stringify(et ? et.inputSchema : { type: 'object', properties: {} }))
173
+ })()
174
+ }
175
+ ]
176
+ }
177
+
178
+ export async function call(name, args, io) {
179
+ if (io) lastIo = io
180
+ if (!lastIo) return 'claude error: MCP io not initialized'
181
+ const a = args || {}
182
+ if (name === 'claude_status') {
183
+ const snap = engine.statusSnapshot()
184
+ if (a.cwd) {
185
+ snap.projects = snap.projects.filter((p) => p.cwd === String(a.cwd))
186
+ const g = snap.projects[0]
187
+ if (g) Object.assign(snap, { state: g.state, running: g.running, current: g.current, trail: g.trail, lastStatus: g.lastStatus, lastAt: g.lastAt, lastSessionId: g.lastSessionId, lastBackend: g.lastBackend, fallbackActive: g.fallbackActive, updatedAt: g.updatedAt })
188
+ }
189
+ return renderStatus(snap, provider)[0].text
190
+ }
191
+ if (name === 'claude_models') {
192
+ const et = (provider.extraTools || [])[0]
193
+ const res = await provider.listModels(a)
194
+ if (et && typeof et.render === 'function') return et.render(a, res)[0].text
195
+ return res.ok ? 'claude models: ' + res.models.map((m) => m.id).join(', ') : 'claude_models failed: ' + String(res.error || 'unknown')
196
+ }
197
+ if (!a.prompt || !String(a.prompt).trim()) return 'claude error: prompt is required'
198
+ const mapped = Object.assign({}, a)
199
+ if (name === 'claude_continue') {
200
+ if (!mapped.sessionId && mapped.latest) mapped.continueLatest = true
201
+ }
202
+ const res = await runClaude(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 req of ['claude_run', 'claude_continue', 'claude_status', 'claude_models']) {
210
+ if (names.indexOf(req) < 0) throw new Error('claude mcp: missing tool ' + req)
211
+ }
212
+ for (const t of ts) {
213
+ if (!t.name || !t.description || !t.inputSchema || t.inputSchema.type !== 'object') throw new Error('claude mcp: bad tool def ' + JSON.stringify(t && t.name))
214
+ }
215
+ // MCP 面必须覆盖 descriptor 三件套(名字同源,提示词连续性红线)。
216
+ for (const k of ['run', 'continue', 'status']) {
217
+ if (names.indexOf(provider.tools[k].name) < 0) throw new Error('claude mcp: tools surface missing descriptor name ' + provider.tools[k].name)
218
+ }
219
+ // background 在 MCP 形态无 jobs 语义 → 必须裁掉(防止智能体误以为可后台派发丢结果)。
220
+ if (ts[0].inputSchema.properties && ts[0].inputSchema.properties.background) throw new Error('claude mcp: run schema must not advertise background')
221
+ // argv 契约漂移锁(实测 Claude Code 2.0.76 的 flag 集)。
222
+ const b = provider.buildArgv(['claude'], { prompt: 'x' }, { planActive: false })
223
+ for (const f of ['-p', 'x', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']) {
224
+ if (b.argv.indexOf(f) < 0) throw new Error('claude mcp: argv contract drift: ' + f)
225
+ }
226
+ const b2 = provider.buildArgv(['claude'], { prompt: 'x', mode: 'plan' }, {})
227
+ if (b2.argv.indexOf('--permission-mode') < 0 || b2.argv.indexOf('plan') < 0) throw new Error('claude mcp: plan mode must pass --permission-mode plan')
228
+ const b3 = provider.buildArgv(['claude'], { prompt: 'x', sessionId: 'sid-1' }, {})
229
+ if (b3.argv.indexOf('--resume') < 0 || b3.argv.indexOf('sid-1') < 0) throw new Error('claude mcp: resume must pass --resume <id>')
230
+ return true
231
+ }
@@ -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.9",
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": {