@luchenglong/cli-bridge 0.0.0-stage → 0.2.3

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 (67) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/MCP-POLICY.md +53 -0
  4. package/PROVIDER-CONTRACT.md +235 -0
  5. package/README.en.md +98 -0
  6. package/README.md +151 -2
  7. package/assets/indicator-states.svg +62 -0
  8. package/bin/agy-quota.mjs +223 -0
  9. package/core/bridge-kit.mjs +549 -0
  10. package/core/scheduling.mjs +149 -0
  11. package/docs/ARCHITECTURE.md +117 -0
  12. package/docs/FALLBACK-AND-INDICATOR.md +93 -0
  13. package/docs/HANDOVER.md +226 -0
  14. package/docs/INSTALL.md +118 -0
  15. package/docs/MCP-POLICY.zh.md +49 -0
  16. package/docs/MIGRATION.md +43 -0
  17. package/docs/RELEASE-SOP.md +80 -0
  18. package/docs/history/agy/CHANGELOG.md +326 -0
  19. package/docs/history/codebuddy/CHANGELOG.md +668 -0
  20. package/docs/history/mimo/CHANGELOG.md +346 -0
  21. package/dynamic/agy.host.js +1237 -0
  22. package/dynamic/agy.host.template.mjs +140 -0
  23. package/dynamic/codebuddy.host.js +1787 -0
  24. package/dynamic/codebuddy.host.template.mjs +162 -0
  25. package/dynamic/mimo.host.js +1732 -0
  26. package/dynamic/mimo.host.template.mjs +169 -0
  27. package/dynamic/opencode.host.js +1443 -0
  28. package/dynamic/opencode.host.template.mjs +125 -0
  29. package/home-plugin/cli-bridge-indicator/cordis.patch.yml +31 -0
  30. package/home-plugin/cli-bridge-indicator/lib/client.js +606 -0
  31. package/home-plugin/cli-bridge-indicator/lib/index.mjs +420 -0
  32. package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +137 -0
  33. package/home-plugin/cli-bridge-indicator/lib/settings.mjs +300 -0
  34. package/mcp/agy-mcp-server.mjs +6 -0
  35. package/mcp/cli-bridge-mcp.mjs +185 -0
  36. package/mcp/codebuddy-mcp-server.mjs +6 -0
  37. package/mcp/mimo-mcp-server.mjs +6 -0
  38. package/mcp/providers/agy.mjs +254 -0
  39. package/mcp/providers/codebuddy.mjs +397 -0
  40. package/mcp/providers/mimo.mjs +290 -0
  41. package/mcp/providers/opencode.mjs +236 -0
  42. package/package.json +89 -4
  43. package/preset/agy-first/agent.cordis.yml +141 -0
  44. package/preset/agy-first/cli-bridge.mjs +169 -0
  45. package/preset/agy-first/plugin-list.mjs +26 -0
  46. package/preset/agy-first/preset.yml +3 -0
  47. package/preset/codebuddy-first/agent.cordis.yml +141 -0
  48. package/preset/codebuddy-first/cli-bridge.mjs +188 -0
  49. package/preset/codebuddy-first/plugin-list.mjs +20 -0
  50. package/preset/codebuddy-first/preset.yml +3 -0
  51. package/preset/mimo-first/agent.cordis.yml +143 -0
  52. package/preset/mimo-first/cli-bridge.mjs +236 -0
  53. package/preset/mimo-first/plugin-list.mjs +39 -0
  54. package/preset/mimo-first/preset.yml +3 -0
  55. package/preset/opencode-first/agent.cordis.yml +141 -0
  56. package/preset/opencode-first/cli-bridge.mjs +147 -0
  57. package/preset/opencode-first/plugin-list.mjs +26 -0
  58. package/preset/opencode-first/preset.yml +3 -0
  59. package/preset/standard-base.mjs +102 -0
  60. package/providers/agy.mjs +398 -0
  61. package/providers/codebuddy.mjs +926 -0
  62. package/providers/mimo.mjs +864 -0
  63. package/providers/opencode.mjs +619 -0
  64. package/providers/registry.mjs +39 -0
  65. package/scripts/build.mjs +235 -0
  66. package/scripts/deploy.mjs +61 -0
  67. package/scripts/verify.mjs +148 -0
@@ -0,0 +1,290 @@
1
+ // mcp/providers/mimo.mjs — mimo MCP 工具面(契约 §4;移植自旧 mimo-mcp-server.mjs 的
2
+ // runMimo/runDesktop/handleTool/TOOLS/renderResultText,framing/生命周期归 keystone
3
+ // 统一服务器 mcp/cli-bridge-mcp.mjs)。
4
+ //
5
+ // 旧语义保持:
6
+ // - mimocode:直接 spawn mimo CLI(MIMO_BIN/MIMO_CLI > ~/.mimocode/bin/mimo[.exe] >
7
+ // 'mimo';.mjs/.cjs/.js 目标经 process.execPath 包装,供测试 fixture 使用)。
8
+ // 实时折叠走 bridge-kit 行流 + engine.foldEvent(旧 foldStep 语义)。
9
+ // - mimo-desktop:委托 provider.execute(provider.delegates('mimo-desktop')=true);
10
+ // 非 spawn 走本地 HTTP。begin/end 由本模块做(MCP 无 kit runner)。
11
+ // - cwd 兜底 = env MIMO_MCP_CWD || MIMO_BRIDGE_CWD || process.cwd()(旧 CWD_FALLBACK;
12
+ // 旧仓曾硬编码机器绝对路径,新实现按 agy 定案改 process.cwd() 兜底)。
13
+ // - 无 ctx.emit:状态走快照文件 {homeDir}/cli-bridge-mcp-mimo.json(keystone
14
+ // indicator readMcpFile 只认 'cli-bridge-mcp-<id>.json' 模式,Lead 复核纠错定案)
15
+ // (bridge-kit buildMcpBridgePayload + 原子写 tmp→rename;写失败静默,红线:绝不影响
16
+ // 工具结果)。旧 mcp-live.json 兼容读归 keystone,本面不写旧文件。
17
+ // - 限流(isLimited)触发即落 fallbackResult(旧 handleTool verbatim:run 内部各退出
18
+ // 路径均已 engine.end,此处仅改返回值对象,绝不二次 engine.end —— 防 runs 虚标/计数下溢)。
19
+
20
+ import { spawn as nodeSpawn } from 'node:child_process'
21
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs'
22
+ import { dirname, join as joinPath, resolve as resolvePath } from 'node:path'
23
+
24
+ import defineProvider from '../../providers/mimo.mjs'
25
+ import {
26
+ createStatusEngine, createLineStream, buildMcpBridgePayload,
27
+ fallbackResult, shortLabel, renderStatus
28
+ } from '../../core/bridge-kit.mjs'
29
+
30
+ export const meta = { id: 'mimo', label: 'MIMO', version: '0.2.2' }
31
+
32
+ const BACKEND_DESKTOP = 'mimo-desktop'
33
+ const SETTINGS_FILE_NAME = 'cli-bridge-settings.json'
34
+
35
+ // 统一设置文件分区读(与 preset 同规则;MCP 子进程无 ctx.get,只读 env/文件)。
36
+ function resolveSettingsFile(env) {
37
+ try {
38
+ if (env.CLI_BRIDGE_SETTINGS_FILE) return env.CLI_BRIDGE_SETTINGS_FILE
39
+ if (env.DSH_HOME) return joinPath(env.DSH_HOME, SETTINGS_FILE_NAME)
40
+ if (process.platform === 'win32' && env.APPDATA) return joinPath(env.APPDATA, 'DSH Desktop', 'dsh-home', SETTINGS_FILE_NAME)
41
+ if (env.HOME) return joinPath(env.HOME, '.dsh', SETTINGS_FILE_NAME)
42
+ } catch (e) { }
43
+ return null
44
+ }
45
+ function readPartition(env) {
46
+ const p = resolveSettingsFile(env)
47
+ if (!p) return { partition: null, path: null, home: null }
48
+ try {
49
+ const all = JSON.parse(readFileSync(p, 'utf8'))
50
+ return { partition: (all && typeof all === 'object' && all.mimo) ? all.mimo : null, path: p, home: dirname(p) }
51
+ } catch (e) {
52
+ return { partition: null, path: p, home: dirname(p) }
53
+ }
54
+ }
55
+
56
+ const provider = defineProvider({
57
+ env: process.env,
58
+ process,
59
+ nodeFs: { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync },
60
+ nodeChild: { spawn: nodeSpawn },
61
+ nodePath: { join: joinPath, dirname },
62
+ readSettings: () => readPartition(process.env)
63
+ })
64
+
65
+ let lastIo = null
66
+ function publishSnapshot(snapshot) {
67
+ const io = lastIo
68
+ if (!io) return
69
+ try {
70
+ // 文件名必须 'cli-bridge-mcp-<id>.json':keystone indicator 的 readMcpFile 按此模式
71
+ // 枚举(agy 面同款);写成别的名字 MCP 形态活动永远进不了状态灯(Lead 复核纠错)。
72
+ const target = io.path.join(io.homeDir, 'cli-bridge-mcp-mimo.json')
73
+ const payload = buildMcpBridgePayload(snapshot, process.pid, Date.now())
74
+ const tmp = target + '.' + process.pid + '.tmp'
75
+ try { io.fs.mkdirSync(io.path.dirname(target), { recursive: true }) } catch (e) { }
76
+ io.fs.writeFileSync(tmp, JSON.stringify(payload), 'utf8')
77
+ io.fs.renameSync(tmp, target)
78
+ } catch (e) { /* 灯数据尽力而为,绝不影响工具调用 */ }
79
+ }
80
+ // MCP 形态不 attach(无 ctx.subprocess),用本模块自持 engine。
81
+ const engine = createStatusEngine({ provider, publish: publishSnapshot })
82
+ provider.engine = engine
83
+
84
+ // ── exe / cwd 解析(旧 resolveMimoExe + CWD_FALLBACK verbatim 语义)──────────────
85
+ function exePrefix() {
86
+ const env = process.env
87
+ const candidates = []
88
+ if (env.MIMO_BIN) candidates.push(env.MIMO_BIN)
89
+ if (env.MIMO_CLI) candidates.push(env.MIMO_CLI)
90
+ if (env.USERPROFILE) candidates.push(env.USERPROFILE + '\\.mimocode\\bin\\mimo.exe')
91
+ if (env.HOME) {
92
+ candidates.push(env.HOME + '\\.mimocode\\bin\\mimo.exe')
93
+ candidates.push(env.HOME + '/.mimocode/bin/mimo')
94
+ }
95
+ for (const exe of candidates) {
96
+ try { if (exe && existsSync(exe)) return /\.(mjs|cjs|js)$/i.test(exe) ? [process.execPath, exe] : [exe] } catch (e) { }
97
+ }
98
+ const bin = env.MIMO_BIN || env.MIMO_CLI || ''
99
+ if (bin) return /\.(mjs|cjs|js)$/i.test(bin) ? [process.execPath, bin] : [bin]
100
+ return ['mimo']
101
+ }
102
+ function resolveCwd(a) {
103
+ if (a && a.cwd) return resolvePath(String(a.cwd))
104
+ return process.env.MIMO_MCP_CWD || process.env.MIMO_BRIDGE_CWD || process.cwd()
105
+ }
106
+ function preferredBackendFromEnv() {
107
+ const env = process.env
108
+ return env.MIMO_BRIDGE_BACKEND || env.MIMO_BACKEND || (readPartition(env).partition || {}).preferredBackend || null
109
+ }
110
+
111
+ // ── spawn(旧 runMimo verbatim 语义;实时折叠走 bridge-kit 行流)────────────────
112
+ function runMimo(args) {
113
+ return new Promise((resolve) => {
114
+ const cwd = resolveCwd(args)
115
+ const built = provider.buildArgv(exePrefix(), args, { planActive: false, defaultMode: 'auto', backend: 'mimocode' })
116
+ engine.begin(cwd)
117
+ let child
118
+ try {
119
+ child = nodeSpawn(built.argv[0], built.argv.slice(1), { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
120
+ } catch (e) {
121
+ const res = { ok: false, status: 'MIMO_UNAVAILABLE', response: '', sessionId: null, exitCode: null, mode: built.mode, backend: 'mimocode', stderr: 'mimo spawn failed: ' + String(e && e.message || e), errorMessage: String(e && e.message || e) }
122
+ engine.end(res, cwd)
123
+ resolve(res)
124
+ return
125
+ }
126
+ let out = ''
127
+ let err = ''
128
+ let killed = false
129
+ const timer = setTimeout(() => { killed = true; try { child.kill('SIGTERM') } catch (e) { } }, (built.timeoutSec + 60) * 1000)
130
+ const stream = createLineStream((ln) => {
131
+ const t = ln.trim()
132
+ if (!t.startsWith('{')) return
133
+ try { engine.foldEvent(JSON.parse(t), cwd) } catch (e) { }
134
+ })
135
+ child.stdout.on('data', (d) => {
136
+ out += d
137
+ if (out.length > 4000000) out = out.slice(-2000000)
138
+ stream.pushChunk(String(d))
139
+ })
140
+ child.stderr.on('data', (d) => { err += d; if (err.length > 1000000) err = err.slice(-500000) })
141
+ child.on('error', (e) => {
142
+ clearTimeout(timer)
143
+ stream.flush()
144
+ const res = { ok: false, status: 'MIMO_UNAVAILABLE', response: '', sessionId: null, exitCode: null, mode: built.mode, backend: 'mimocode', stderr: String(e && e.message || e), errorMessage: String(e && e.message || e) }
145
+ engine.end(res, cwd)
146
+ resolve(res)
147
+ })
148
+ child.on('close', (code) => {
149
+ clearTimeout(timer)
150
+ stream.flush()
151
+ const outcome = { exitCode: killed ? 124 : (code == null ? -1 : code) }
152
+ let res = provider.buildResult(provider.parseJson(out), outcome, built.mode, err, out, 'mimocode')
153
+ if (killed && !res.ok) {
154
+ res.status = 'HUNG_TIMEOUT'
155
+ 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]'
156
+ }
157
+ engine.end(res, cwd)
158
+ resolve(res)
159
+ })
160
+ })
161
+ }
162
+
163
+ // ── 文本渲染(旧 renderResultText verbatim;契约 §4:错误也是文本)───────────────
164
+ function textResult(res) {
165
+ if (!res) return '(no result)'
166
+ const b = res.backend ? ' backend=' + res.backend : ''
167
+ if (res.fallback) {
168
+ return 'status=FALLBACK_TO_DSH fallback=true' + b + '\n' + (res.reason ? 'reason=' + res.reason + '\n' : '') +
169
+ 'The user/host chose DSH-local. Finish with native tools and NEVER call mimo again for this task.'
170
+ }
171
+ if (res.ok === false && res.error) return 'mimo_settings/error' + b + ': ' + res.error
172
+ if (!res.ok) {
173
+ const hint = provider.failureHint ? provider.failureHint(res) : ''
174
+ return 'mimo failed status=' + (res.status || 'ERROR') + b + '\n' + shortLabel(res.stderr || res.errorMessage || '') + (hint ? '\n' + hint : '')
175
+ }
176
+ return 'mimo ok status=' + res.status + b + (res.sessionId ? ' sessionId=' + res.sessionId : '') + '\n\n' + String(res.response || '')
177
+ }
178
+
179
+ // ── 工具面(旧 MCP TOOLS verbatim:含 sessionId 关键注释与 mimo_settings)────────
180
+ // 直接复用描述符的 schema/description(三形态单一真源);MCP 面去掉 background 参数
181
+ // (stdio 调用恒为同步阻塞,无宿主 jobs 服务)—— 旧 MCP 的 run/continue 本就无 background。
182
+ function stripBackground(schema) {
183
+ const copy = JSON.parse(JSON.stringify(schema))
184
+ if (copy.properties) delete copy.properties.background
185
+ return copy
186
+ }
187
+
188
+ export function tools(env) {
189
+ const run = { name: provider.tools.run.name, description: provider.tools.run.description, inputSchema: stripBackground(provider.tools.run.inputSchema) }
190
+ const cont = { name: provider.tools.continue.name, description: provider.tools.continue.description, inputSchema: stripBackground(provider.tools.continue.inputSchema) }
191
+ const status = { name: provider.tools.status.name, description: provider.tools.status.description, inputSchema: provider.tools.status.inputSchema }
192
+ const settings = provider.extraTools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))[0]
193
+ return [run, cont, status, settings]
194
+ }
195
+
196
+ export async function call(name, args, io) {
197
+ if (io) lastIo = io
198
+ const a = args || {}
199
+ const runtimeBackend = provider.resolveBackend(a, null, preferredBackendFromEnv())
200
+
201
+ if (name === 'mimo_settings') {
202
+ const v = await provider.extraTools[0].logic(a, null)
203
+ return provider.extraTools[0].render(v)[0].text
204
+ }
205
+
206
+ if (name === 'mimo_status') {
207
+ const snap = await provider.statusToolLogic(a)
208
+ snap.defaultBackend = runtimeBackend
209
+ snap.backend = runtimeBackend
210
+ return renderStatus(snap, provider)[0].text
211
+ }
212
+
213
+ if (name === 'mimo_run' || name === 'mimo_continue') {
214
+ if (!a.prompt || !String(a.prompt).trim()) return 'mimo error: prompt is required'
215
+ const mapped = {
216
+ prompt: a.prompt,
217
+ mode: a.mode,
218
+ model: a.model,
219
+ effort: a.effort,
220
+ cwd: a.cwd,
221
+ timeoutSec: a.timeoutSec,
222
+ title: a.title,
223
+ backend: a.backend,
224
+ background: false, // MCP 恒同步阻塞(无宿主 jobs)
225
+ // 【关键】desktop 后端的 sessionId 必须转发。旧 handleTool 此处曾是白名单转发,
226
+ // 漏了 sessionId(只在 mimo_continue 分支补)→ mimo_run 传 sessionId 被静默丢弃
227
+ // → 永远 newDesktopSessionId 造新 id → 因 Desktop API 不能创建会话,mimo_run 在
228
+ // desktop 后端下完全不可用。
229
+ sessionId: a.sessionId
230
+ }
231
+ if (name === 'mimo_continue') {
232
+ if (a.sessionId) mapped.sessionId = a.sessionId
233
+ else mapped.continueLatest = true
234
+ }
235
+ const backend = provider.resolveBackend(mapped, null, preferredBackendFromEnv())
236
+ let res
237
+ if (backend === BACKEND_DESKTOP) {
238
+ const pf = provider.preflight({ backend })
239
+ if (pf && pf.abort) {
240
+ res = pf.abort
241
+ engine.begin(resolveCwd(mapped))
242
+ engine.end(res, resolveCwd(mapped))
243
+ } else {
244
+ const cwd = resolveCwd(mapped)
245
+ const built = provider.buildArgv(exePrefix(), mapped, { planActive: false, defaultMode: 'auto', backend })
246
+ engine.begin(cwd)
247
+ res = await provider.execute({ args: mapped, cwd, built, backend })
248
+ engine.end(res, cwd)
249
+ }
250
+ } else {
251
+ res = await runMimo(mapped)
252
+ }
253
+ if (!res.ok && provider.isLimited(res)) {
254
+ // runMimo / desktop 分支所有退出路径均已 engine.end;此处仅改返回值对象,
255
+ // 绝不二次 engine.end(防 runs 虚标与 running 计数下溢)。
256
+ res = fallbackResult(res, res.mode, provider)
257
+ res.backend = backend
258
+ }
259
+ return textResult(res)
260
+ }
261
+
262
+ return 'mimo error: unknown tool: ' + name
263
+ }
264
+
265
+ // ── 自检(keystone --check 调用;argv 契约防漂移)───────────────────────────────
266
+ export function check(env) {
267
+ const ts = tools(env || process.env)
268
+ const names = ts.map((t) => t.name)
269
+ for (const req of ['mimo_run', 'mimo_continue', 'mimo_status', 'mimo_settings']) {
270
+ if (!names.includes(req)) throw new Error('mimo mcp: missing tool ' + req)
271
+ }
272
+ for (const t of ts) {
273
+ if (!t.name || !t.description || !t.inputSchema || t.inputSchema.type !== 'object') throw new Error('mimo mcp: bad tool def ' + JSON.stringify(t && t.name))
274
+ }
275
+ // MCP 面必须覆盖描述符三件套 + settings(名字一致,提示词连续性红线)。
276
+ for (const k of ['run', 'continue', 'status']) {
277
+ if (!names.includes(provider.tools[k].name)) throw new Error('mimo mcp: tools surface missing descriptor name ' + provider.tools[k].name)
278
+ }
279
+ // mimocode argv 契约(旧 core verbatim)。
280
+ const b = provider.buildArgv(['mimo'], { prompt: 'x' }, { planActive: false, backend: 'mimocode' })
281
+ for (const f of ['run', '--format', 'json', '--dangerously-skip-permissions', '--agent', 'build']) {
282
+ if (!b.argv.includes(f)) throw new Error('mimo mcp: argv contract drift: ' + f)
283
+ }
284
+ // mimo-desktop 空壳(非 spawn 路径,交 execute)。
285
+ const d = provider.buildArgv(['mimo'], { prompt: 'x' }, { planActive: false, backend: BACKEND_DESKTOP })
286
+ if (d.argv.length !== 0) throw new Error('mimo mcp: desktop buildArgv must be empty shell')
287
+ // delegates 规则:只有 desktop 委托。
288
+ if (provider.delegates(BACKEND_DESKTOP) !== true || provider.delegates('mimocode') !== false) throw new Error('mimo mcp: delegates contract drift')
289
+ return true
290
+ }
@@ -0,0 +1,236 @@
1
+ // mcp/providers/opencode.mjs — opencode MCP 工具面(契约 §4)。
2
+ // framing/生命周期归 keystone 统一服务器 mcp/cli-bridge-mcp.mjs。
3
+ //
4
+ // 与 agy 面的结构同构(旧 MCP 语义保持):直接 spawn opencode(.exe)(win32 用
5
+ // opencode.exe;OPENCODE_BIN 覆盖;.js/.mjs 目标经 process.execPath 包装,供测试
6
+ // fixture 使用)。无 ctx.emit:状态走快照文件 {homeDir}/cli-bridge-mcp-opencode.json
7
+ // (bridge-kit buildMcpBridgePayload + 原子写 tmp→rename;写失败静默)。
8
+ //
9
+ // opencode 无 quota/额度门禁(凭据与额度由 opencode 自身与 provider 决定),
10
+ // 因此本面只有三件套。
11
+ import { spawn as nodeSpawn } from 'node:child_process'
12
+ import { resolve as resolvePath } from 'node:path'
13
+
14
+ import defineProvider, { resolveExePrefix } from '../../providers/opencode.mjs'
15
+ import { createStatusEngine, createLineStream, buildMcpBridgePayload, isLimited, renderStatus } from '../../core/bridge-kit.mjs'
16
+
17
+ export const meta = { id: 'opencode', label: 'OPENCODE', version: '0.2.2' }
18
+
19
+ const provider = defineProvider({
20
+ env: process.env,
21
+ process,
22
+ nodeChild: { spawn: nodeSpawn }
23
+ })
24
+
25
+ let lastIo = null
26
+ function publishSnapshot(snapshot) {
27
+ const io = lastIo
28
+ if (!io) return
29
+ try {
30
+ const target = io.path.join(io.homeDir, 'cli-bridge-mcp-opencode.json')
31
+ const payload = buildMcpBridgePayload(snapshot, process.pid, Date.now())
32
+ const tmp = target + '.' + process.pid + '.tmp'
33
+ try { io.fs.mkdirSync(io.path.dirname(target), { recursive: true }) } catch (e) {}
34
+ io.fs.writeFileSync(tmp, JSON.stringify(payload), 'utf8')
35
+ io.fs.renameSync(tmp, target)
36
+ } catch (e) { /* 灯数据尽力而为,绝不影响工具调用 */ }
37
+ }
38
+ // provider.engine 在 attach 后才存在;MCP 形态不 attach,用本模块的 engine。
39
+ const engine = createStatusEngine({ provider, publish: publishSnapshot })
40
+ provider.engine = engine
41
+
42
+ // ── exe / cwd 解析 ────────────────────────────────────────────────────────────
43
+ // 与描述符同一套优先级(env > 桌面版探测 > PATH 名字);二进制名 opencode-cli.exe
44
+ // (V2 桌面版),不做 opencode.exe 兜底以免误命中不能并存的另一个版本。
45
+ function exePrefix() {
46
+ return resolveExePrefix(process.env, '')
47
+ }
48
+ function resolveCwd(a) {
49
+ if (a && a.cwd) return resolvePath(String(a.cwd))
50
+ return process.env.OPENCODE_MCP_CWD || process.cwd()
51
+ }
52
+
53
+ // ── spawn(实时折叠走 bridge-kit 行流)────────────────────────────────────────
54
+ function runOpencode(args) {
55
+ return new Promise((resolve) => {
56
+ const built = provider.buildArgv(exePrefix(), args, { planActive: false, defaultMode: 'auto' })
57
+ const cwd = resolveCwd(args)
58
+ let child
59
+ try {
60
+ child = nodeSpawn(built.argv[0], built.argv.slice(1), { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
61
+ } catch (e) {
62
+ 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) })
63
+ return
64
+ }
65
+ engine.begin(cwd)
66
+ let out = ''
67
+ let err = ''
68
+ let killed = false
69
+ const timer = setTimeout(() => { killed = true; try { child.kill() } catch (e) {} }, (built.timeoutSec + 60) * 1000)
70
+ const stream = createLineStream((ln) => {
71
+ const t = ln.trim()
72
+ if (!t.startsWith('{')) return
73
+ try { engine.foldEvent(JSON.parse(t), cwd) } catch (e) {}
74
+ })
75
+ child.stdout.on('data', (d) => {
76
+ out += d
77
+ if (out.length > 4000000) out = out.slice(-2000000)
78
+ stream.pushChunk(String(d))
79
+ })
80
+ child.stderr.on('data', (d) => { err += d; if (err.length > 1000000) err = err.slice(-500000) })
81
+ child.on('error', (e) => {
82
+ clearTimeout(timer)
83
+ stream.flush()
84
+ const res = { ok: false, status: 'OPENCODE_UNAVAILABLE', response: '', sessionId: null, durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode: built.mode, backend: null, stderr: 'opencode spawn failed: ' + String(e && e.message || e) }
85
+ engine.end(res, cwd)
86
+ resolve(res)
87
+ })
88
+ child.on('close', (code) => {
89
+ clearTimeout(timer)
90
+ stream.flush()
91
+ const outcome = { exitCode: killed ? 124 : code }
92
+ let res = provider.buildResult(provider.parseJson(out), outcome, built.mode, err, out, null)
93
+ if (killed && !res.ok) {
94
+ res.status = 'HUNG_TIMEOUT'
95
+ 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]'
96
+ }
97
+ engine.end(res, cwd)
98
+ resolve(res)
99
+ })
100
+ })
101
+ }
102
+
103
+ function textResult(res) {
104
+ if (res.fallback) {
105
+ return 'opencode unavailable (' + res.reason + '). Use native tools / the local model for this task; do not retry opencode.'
106
+ }
107
+ const limited = !res.ok && isLimited(res)
108
+ const head = 'opencode ' + (res.ok ? 'OK' : 'FAILED') + ' [status=' + res.status +
109
+ (res.sessionId ? ' session=' + res.sessionId : '') +
110
+ (res.durationSeconds != null ? ' ' + res.durationSeconds + 's' : '') + ']'
111
+ let note = ''
112
+ if (limited) note = '\n\n[Note: this looks like a rate-limit / network failure. Do NOT retry opencode in a loop; finish the task with your own tools, or ask the user.]'
113
+ const body = res.response || (res.stderr ? '[stderr] ' + res.stderr : (res.rawStdout ? '[raw] ' + res.rawStdout : ''))
114
+ return head + (body ? '\n\n' + body : '') + note
115
+ }
116
+
117
+ // ── 工具面 ────────────────────────────────────────────────────────────────────
118
+ const TOOLS = [
119
+ {
120
+ name: 'opencode_run',
121
+ description: 'Dispatch a coding/build/debug/investigation task to the local opencode agent CLI and return its final answer. opencode runs fully non-interactively (permissions auto-approved, never blocks on a prompt). 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 opencode_status to watch what opencode is doing live.',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ prompt: { type: 'string', description: 'The task instruction for opencode. 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 — opencode reads the repo itself). For follow-ups on the same topic, use opencode_continue with latest=true or sessionId (opencode keeps session context; state only what is new).' },
126
+ model: { type: 'string', description: 'Optional model in provider/model form (e.g. "anthropic/claude-sonnet-4-20250514"). Omit to let opencode use its own configured default.' },
127
+ agent: { type: 'string', description: 'Optional opencode agent (subagent) name.' },
128
+ variant: { type: 'string', description: 'Optional provider-specific reasoning variant (e.g. high, max, minimal).' },
129
+ cwd: { type: 'string', description: 'Working directory for opencode (default: OPENCODE_MCP_CWD or the DSH workspace).' },
130
+ files: { type: 'array', items: { type: 'string' }, description: 'Optional file paths to attach to the message.' },
131
+ title: { type: 'string', description: 'Optional session title.' },
132
+ timeoutSec: { type: 'integer', description: '10-3600, default 600.' },
133
+ autoApprove: { type: 'boolean', description: 'Default true: pass --auto so opencode never blocks on a permission prompt. Set false to require explicit approval.' }
134
+ },
135
+ required: ['prompt']
136
+ }
137
+ },
138
+ {
139
+ name: 'opencode_continue',
140
+ description: 'Continue an existing opencode session with a follow-up prompt, reusing opencode context. Pass sessionId from a prior opencode_run result, or set latest=true for the most recent session.',
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ prompt: { type: 'string', description: 'Follow-up instruction for the ongoing opencode session. State only what is new; opencode preserves its session context.' },
145
+ sessionId: { type: 'string' },
146
+ latest: { type: 'boolean', description: 'Continue the most recent opencode session.' },
147
+ model: { type: 'string' },
148
+ agent: { type: 'string' },
149
+ variant: { type: 'string' },
150
+ cwd: { type: 'string' },
151
+ files: { type: 'array', items: { type: 'string' } },
152
+ timeoutSec: { type: 'integer', description: '10-3600, default 600.' },
153
+ autoApprove: { type: 'boolean' }
154
+ },
155
+ required: ['prompt']
156
+ }
157
+ },
158
+ {
159
+ name: 'opencode_status',
160
+ description: 'Read a live snapshot of what the local opencode 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 opencode_run/opencode_continue without waiting for it to finish.',
161
+ inputSchema: { type: 'object', properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
162
+ },
163
+ {
164
+ name: 'opencode_models',
165
+ description: 'List the models available to the local opencode CLI (from `opencode models`). Use it to pick a model before opencode_run, or to check which models are free. Prefer a -free model for routine work and reserve paid/frontier models for tasks that genuinely need them. If the CLI is missing or not logged in this returns ok:false with an actionable error — it never invents a model list.',
166
+ inputSchema: {
167
+ type: 'object',
168
+ properties: {
169
+ freeOnly: { type: 'boolean', description: 'Return only free models (id ends with -free).' },
170
+ provider: { type: 'string', description: 'Optional provider filter, e.g. "opencode" or "opencode-go".' }
171
+ }
172
+ }
173
+ }
174
+ ]
175
+
176
+ export function tools(env) {
177
+ return TOOLS.map((t) => Object.assign({}, t))
178
+ }
179
+
180
+ export async function call(name, args, io) {
181
+ if (io) lastIo = io
182
+ if (!lastIo) return 'opencode error: MCP io not initialized'
183
+ const a = args || {}
184
+ if (name === 'opencode_status') {
185
+ const snap = engine.statusSnapshot()
186
+ if (a.cwd) {
187
+ snap.projects = snap.projects.filter((p) => p.cwd === String(a.cwd))
188
+ const g = snap.projects[0]
189
+ 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 })
190
+ }
191
+ return renderStatus(snap, provider)[0].text
192
+ }
193
+ if (name === 'opencode_models') {
194
+ const res = await provider.listModels(a)
195
+ if (!res.ok) return 'opencode_models failed: ' + String(res.error || 'unknown')
196
+ const list = res.models || []
197
+ if (!list.length) return 'opencode_models: no models matched'
198
+ const lines = ['opencode models (' + list.length + '):']
199
+ for (const m of list.slice(0, 40)) lines.push(' ' + m.id + (m.free ? ' [free]' : ''))
200
+ if (list.length > 40) lines.push(' … and ' + (list.length - 40) + ' more')
201
+ const free = list.filter((m) => m.free).length
202
+ lines.push('free: ' + free + ' / ' + list.length)
203
+ if (a.freeOnly && !free) lines.push('(no free models under this filter)')
204
+ return lines.join('\n')
205
+ }
206
+ if (!a.prompt || !String(a.prompt).trim()) return 'opencode error: prompt is required'
207
+ const mapped = Object.assign({}, a)
208
+ if (name === 'opencode_continue') {
209
+ if (!mapped.sessionId && mapped.latest) mapped.continueLatest = true
210
+ }
211
+ const res = await runOpencode(mapped)
212
+ return textResult(res)
213
+ }
214
+
215
+ export function check(env) {
216
+ const ts = tools(env)
217
+ const names = ts.map((t) => t.name)
218
+ for (const req of ['opencode_run', 'opencode_continue', 'opencode_status', 'opencode_models']) {
219
+ if (!names.includes(req)) throw new Error('opencode mcp: missing tool ' + req)
220
+ }
221
+ for (const t of ts) {
222
+ if (!t.name || !t.description || !t.inputSchema || t.inputSchema.type !== 'object') throw new Error('opencode mcp: bad tool def ' + JSON.stringify(t && t.name))
223
+ }
224
+ // MCP 面必须覆盖 descriptor 三件套(名字一致,提示词连续性红线)。
225
+ for (const k of ['run', 'continue', 'status']) {
226
+ if (!names.includes(provider.tools[k].name)) throw new Error('opencode mcp: tools surface missing descriptor name ' + provider.tools[k].name)
227
+ }
228
+ // argv 契约漂移锁(实测 opencode 1.19.0 flag 集)。
229
+ const b = provider.buildArgv(['opencode'], { prompt: 'x' }, { planActive: false })
230
+ for (const f of ['run', 'x', '--format', 'json', '--auto']) {
231
+ if (!b.argv.includes(f)) throw new Error('opencode mcp: argv contract drift: ' + f)
232
+ }
233
+ const b2 = provider.buildArgv(['opencode'], { prompt: 'x', autoApprove: false }, { planActive: false })
234
+ if (b2.argv.includes('--auto')) throw new Error('opencode mcp: autoApprove=false must omit --auto')
235
+ return true
236
+ }
package/package.json CHANGED
@@ -1,6 +1,91 @@
1
1
  {
2
2
  "name": "@luchenglong/cli-bridge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.3",
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.",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "engines": {
10
+ "node": ">=18",
11
+ "dsh": ">=0.1.1-rc.2 <0.3.0-0"
12
+ },
13
+ "main": "./home-plugin/cli-bridge-indicator/lib/index.mjs",
14
+ "exports": {
15
+ ".": "./home-plugin/cli-bridge-indicator/lib/index.mjs",
16
+ "./client": "./home-plugin/cli-bridge-indicator/lib/client.js",
17
+ "./bridge-kit": "./core/bridge-kit.mjs",
18
+ "./registry": "./providers/registry.mjs",
19
+ "./providers/agy": "./providers/agy.mjs",
20
+ "./providers/codebuddy": "./providers/codebuddy.mjs",
21
+ "./providers/mimo": "./providers/mimo.mjs",
22
+ "./providers/opencode": "./providers/opencode.mjs",
23
+ "./preset-agy": "./preset/agy-first/cli-bridge.mjs",
24
+ "./preset-codebuddy": "./preset/codebuddy-first/cli-bridge.mjs",
25
+ "./preset-mimo": "./preset/mimo-first/cli-bridge.mjs",
26
+ "./preset-opencode": "./preset/opencode-first/cli-bridge.mjs",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "dsh": {
30
+ "client": {
31
+ "platform": "web"
32
+ },
33
+ "bundle": {
34
+ "patch": "./home-plugin/cli-bridge-indicator/cordis.patch.yml"
35
+ }
36
+ },
37
+ "bin": {
38
+ "cli-bridge-mcp": "mcp/cli-bridge-mcp.mjs",
39
+ "agy-mcp-server": "mcp/agy-mcp-server.mjs",
40
+ "codebuddy-mcp-server": "mcp/codebuddy-mcp-server.mjs",
41
+ "mimo-mcp-server": "mcp/mimo-mcp-server.mjs"
42
+ },
43
+ "scripts": {
44
+ "build": "node scripts/build.mjs",
45
+ "test": "node scripts/build.mjs --check && node --test",
46
+ "verify": "node scripts/verify.mjs",
47
+ "check": "node scripts/build.mjs --check && node scripts/verify.mjs && node --test && node mcp/cli-bridge-mcp.mjs --check",
48
+ "prepack": "node scripts/verify.mjs && node scripts/build.mjs --check",
49
+ "deploy:local": "node scripts/deploy.mjs"
50
+ },
51
+ "files": [
52
+ "core/",
53
+ "providers/",
54
+ "preset/",
55
+ "home-plugin/",
56
+ "dynamic/",
57
+ "mcp/",
58
+ "bin/",
59
+ "scripts/",
60
+ "docs/",
61
+ "assets/",
62
+ "locale/",
63
+ "CHANGELOG.md",
64
+ "MCP-POLICY.md",
65
+ "PROVIDER-CONTRACT.md",
66
+ "README.md",
67
+ "README.en.md",
68
+ "LICENSE"
69
+ ],
70
+ "keywords": [
71
+ "dsh",
72
+ "cordis",
73
+ "cli-bridge",
74
+ "agy",
75
+ "codebuddy",
76
+ "mimo",
77
+ "mcp",
78
+ "mcp-server",
79
+ "agent",
80
+ "deepseek-harness"
81
+ ],
82
+ "author": "chenglong",
83
+ "repository": {
84
+ "type": "git",
85
+ "url": "git+https://github.com/new-256/cli-bridge.git"
86
+ },
87
+ "homepage": "https://github.com/new-256/cli-bridge#readme",
88
+ "bugs": {
89
+ "url": "https://github.com/new-256/cli-bridge/issues"
90
+ }
91
+ }