@luchenglong/cli-bridge 0.0.0-stage → 0.2.2

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 (58) hide show
  1. package/CHANGELOG.md +148 -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 +150 -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/INSTALL.md +118 -0
  14. package/docs/MCP-POLICY.zh.md +49 -0
  15. package/docs/MIGRATION.md +43 -0
  16. package/docs/RELEASE-SOP.md +77 -0
  17. package/docs/history/agy/CHANGELOG.md +326 -0
  18. package/docs/history/codebuddy/CHANGELOG.md +668 -0
  19. package/docs/history/mimo/CHANGELOG.md +346 -0
  20. package/dynamic/agy.host.js +1237 -0
  21. package/dynamic/agy.host.template.mjs +140 -0
  22. package/dynamic/codebuddy.host.js +1787 -0
  23. package/dynamic/codebuddy.host.template.mjs +162 -0
  24. package/dynamic/mimo.host.js +1732 -0
  25. package/dynamic/mimo.host.template.mjs +169 -0
  26. package/home-plugin/cli-bridge-indicator/cordis.patch.yml +31 -0
  27. package/home-plugin/cli-bridge-indicator/lib/client.js +606 -0
  28. package/home-plugin/cli-bridge-indicator/lib/index.mjs +420 -0
  29. package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +137 -0
  30. package/home-plugin/cli-bridge-indicator/lib/settings.mjs +300 -0
  31. package/mcp/agy-mcp-server.mjs +6 -0
  32. package/mcp/cli-bridge-mcp.mjs +184 -0
  33. package/mcp/codebuddy-mcp-server.mjs +6 -0
  34. package/mcp/mimo-mcp-server.mjs +6 -0
  35. package/mcp/providers/agy.mjs +254 -0
  36. package/mcp/providers/codebuddy.mjs +397 -0
  37. package/mcp/providers/mimo.mjs +290 -0
  38. package/package.json +87 -4
  39. package/preset/agy-first/agent.cordis.yml +141 -0
  40. package/preset/agy-first/cli-bridge.mjs +169 -0
  41. package/preset/agy-first/plugin-list.mjs +26 -0
  42. package/preset/agy-first/preset.yml +3 -0
  43. package/preset/codebuddy-first/agent.cordis.yml +141 -0
  44. package/preset/codebuddy-first/cli-bridge.mjs +188 -0
  45. package/preset/codebuddy-first/plugin-list.mjs +20 -0
  46. package/preset/codebuddy-first/preset.yml +3 -0
  47. package/preset/mimo-first/agent.cordis.yml +143 -0
  48. package/preset/mimo-first/cli-bridge.mjs +236 -0
  49. package/preset/mimo-first/plugin-list.mjs +39 -0
  50. package/preset/mimo-first/preset.yml +3 -0
  51. package/preset/standard-base.mjs +102 -0
  52. package/providers/agy.mjs +398 -0
  53. package/providers/codebuddy.mjs +926 -0
  54. package/providers/mimo.mjs +864 -0
  55. package/providers/registry.mjs +34 -0
  56. package/scripts/build.mjs +235 -0
  57. package/scripts/deploy.mjs +61 -0
  58. package/scripts/verify.mjs +148 -0
@@ -0,0 +1,300 @@
1
+ // home-plugin/cli-bridge-indicator/lib/settings.mjs — 统一设置读写(keystone)。
2
+ //
3
+ // 单一文件 {DSH_HOME}/cli-bridge-settings.json,按 provider id 分区:
4
+ // { "agy": {...}, "codebuddy": {...}, "mimo": {...}, "_scheduling": {...}, "updatedAt": 123 }
5
+ // 分区契约(keys/secretKeys/defaults/normalize/meta/diagnose)见 PROVIDER-CONTRACT.md §3,
6
+ // 纯逻辑全部来自 providers/registry.mjs 的描述符;本文件只做落盘、掩码、清洗。
7
+ //
8
+ // `_scheduling` 区(0.2.0 调度):顶层非 provider 区(下划线前缀 = 永不与 provider id
9
+ // 撞名;旧代码逐分区读取会整区忽略它 → 纯增量向后兼容)。{ enabled:[ids], priority:[ids] }
10
+ // = 启用集合 + 调用优先级(数组顺序即优先级);清洗逻辑单源在 core/scheduling.mjs。
11
+ //
12
+ // token 语义(与 codebuddy v1.3.x 完全一致):GET 永不回传明文(只回 {set,hint} 掩码);
13
+ // POST 空串/缺省 = 保持现值;`<key>Clear: true` = 显式清除。
14
+
15
+ import { PROVIDER_IDS, createProvider } from '../../../providers/registry.mjs'
16
+ import { SCHEDULING_SECTION, normalizeScheduling } from '../../../core/scheduling.mjs'
17
+
18
+ export const SETTINGS_FILE_NAME = 'cli-bridge-settings.json'
19
+
20
+ function builtinModule(name) {
21
+ try {
22
+ const proc = globalThis.process
23
+ return (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule(name) : null
24
+ } catch (e) { return null }
25
+ }
26
+
27
+ export function settingsPath(io) {
28
+ const o = io || {}
29
+ const proc = globalThis.process
30
+ const env = o.env || (proc && proc.env) || {}
31
+ try {
32
+ if (env.CLI_BRIDGE_SETTINGS_FILE) return env.CLI_BRIDGE_SETTINGS_FILE
33
+ const nodePath = builtinModule('node:path')
34
+ if (nodePath && env.DSH_HOME) return nodePath.join(env.DSH_HOME, SETTINGS_FILE_NAME)
35
+ } catch (e) { }
36
+ return null
37
+ }
38
+
39
+ function descriptors(io) {
40
+ const o = io || {}
41
+ const map = Object.create(null)
42
+ for (const id of PROVIDER_IDS) {
43
+ try { map[id] = createProvider(id, { env: o.env }) } catch (e) { map[id] = null }
44
+ }
45
+ return map
46
+ }
47
+
48
+ // ── 旧包独立设置文件的一次性迁移读(keystone 职责:文件兼容属于落盘层)──────────
49
+ // codebuddy-first-bridge v1.3.x → {DSH_HOME}/codebuddy-bridge-settings.json(扁平同字段名;
50
+ // 旧 env 覆盖 CODEBUDDY_SETTINGS_FILE)
51
+ // mimo-first-bridge v1.x → {APPDATA}/mimo-first-bridge/settings.json(键 backend/
52
+ // desktopModel;旧 env 覆盖 MIMO_BRIDGE_SETTINGS;非 Windows 为 HOME/.config)
53
+ // 规则:统一文件缺失/无该分区值时回退读旧文件一次;统一文件一旦有值旧文件即失效;
54
+ // 永不写旧文件(用户在新面板保存后自然完成迁移)。
55
+ function envOf(io) {
56
+ const o = io || {}
57
+ if (o.env) return o.env
58
+ const proc = globalThis.process
59
+ return (proc && proc.env) || {}
60
+ }
61
+
62
+ function configBase(env) {
63
+ const nodePath = builtinModule('node:path')
64
+ if (process.platform === 'win32' && env.APPDATA) return nodePath ? nodePath.join(env.APPDATA, 'mimo-first-bridge', 'settings.json') : env.APPDATA + '/mimo-first-bridge/settings.json'
65
+ const base = env.HOME ? (nodePath ? nodePath.join(env.HOME, '.config') : env.HOME + '/.config') : null
66
+ return base ? (nodePath ? nodePath.join(base, 'mimo-first-bridge', 'settings.json') : base + '/mimo-first-bridge/settings.json') : null
67
+ }
68
+
69
+ const LEGACY_SOURCES = {
70
+ codebuddy: (env) => env.CODEBUDDY_SETTINGS_FILE || (() => {
71
+ const nodePath = builtinModule('node:path')
72
+ return env.DSH_HOME ? (nodePath ? nodePath.join(env.DSH_HOME, 'codebuddy-bridge-settings.json') : env.DSH_HOME + '/codebuddy-bridge-settings.json') : null
73
+ })(),
74
+ mimo: (env) => env.MIMO_BRIDGE_SETTINGS || configBase(env)
75
+ }
76
+
77
+ function legacyPartitionSlice(id, io) {
78
+ const o = io || {}
79
+ try {
80
+ const env = envOf(o)
81
+ const file = LEGACY_SOURCES[id] ? LEGACY_SOURCES[id](env) : null
82
+ if (!file) return null
83
+ const nodeFs = o.legacyFs || builtinModule('node:fs')
84
+ if (!nodeFs || !nodeFs.existsSync || !nodeFs.existsSync(file)) return null
85
+ const parsed = JSON.parse(nodeFs.readFileSync(file, 'utf8'))
86
+ if (!parsed || typeof parsed !== 'object') return null
87
+ let mapped = parsed
88
+ if (id === 'mimo') {
89
+ mapped = {}
90
+ if (parsed.backend) mapped.preferredBackend = parsed.backend
91
+ if (parsed.desktopModel) mapped.desktopModel = parsed.desktopModel
92
+ }
93
+ const out = {}
94
+ for (const k of Object.keys(mapped)) if (mapped[k] !== undefined && mapped[k] !== null && mapped[k] !== '') out[k] = mapped[k]
95
+ return Object.keys(out).length ? out : null
96
+ } catch (e) { return null }
97
+ }
98
+
99
+ export function defaultSettings(io) {
100
+ const map = descriptors(io)
101
+ const out = Object.create(null)
102
+ for (const id of PROVIDER_IDS) {
103
+ const d = map[id]
104
+ out[id] = d && d.settings && typeof d.settings.defaults === 'function' ? d.settings.defaults() : {}
105
+ }
106
+ return out
107
+ }
108
+
109
+ /** 清洗整份设置对象:只认已知 provider 分区 + 各自 keys;未知字段丢弃。 */
110
+ export function normalizeAll(raw, io) {
111
+ const map = descriptors(io)
112
+ const snap = Object.create(null)
113
+ for (const id of PROVIDER_IDS) snap[id] = Object.assign({}, defaultSettings(io)[id])
114
+ const touched = Object.create(null)
115
+ for (const id of PROVIDER_IDS) touched[id] = Object.create(null)
116
+ if (raw && typeof raw === 'object') {
117
+ for (const id of PROVIDER_IDS) {
118
+ const src = raw[id]
119
+ if (src && typeof src === 'object') for (const k of Object.keys(src)) touched[id][k] = true
120
+ }
121
+ }
122
+ if (!raw || typeof raw !== 'object') return applyLegacy(snap, map, io, touched)
123
+ for (const id of PROVIDER_IDS) {
124
+ const d = map[id]
125
+ if (!d || !d.settings) continue
126
+ const src = raw[id]
127
+ if (!src || typeof src !== 'object') continue
128
+ try {
129
+ const cleaned = d.settings.normalize(src, (io && io.env) || undefined)
130
+ if (cleaned && typeof cleaned === 'object') snap[id] = Object.assign(snap[id], cleaned)
131
+ } catch (e) { }
132
+ }
133
+ if (typeof raw.updatedAt === 'number' && raw.updatedAt > 0) snap.updatedAt = raw.updatedAt
134
+ const sched = normalizeScheduling(raw[SCHEDULING_SECTION], PROVIDER_IDS)
135
+ snap[SCHEDULING_SECTION] = { enabled: sched.enabled, priority: sched.priority }
136
+ return applyLegacy(snap, map, io, touched)
137
+ }
138
+
139
+ /** 生效调度(含派生 order):任何来源(文件缺 _ 区/坏值)都回落全开 + 注册表顺序。 */
140
+ export function schedulingOf(snap, io) {
141
+ return normalizeScheduling(snap && snap[SCHEDULING_SECTION], PROVIDER_IDS)
142
+ }
143
+
144
+ // 旧包设置文件的字段级回退:只补「统一文件里从未写过这个键」的字段(touched 记录原始
145
+ // 键存在性,显式保存过的默认值也算有值 → 旧文件对该字段永久失效)。
146
+ function applyLegacy(snap, map, io, touched) {
147
+ for (const id of PROVIDER_IDS) {
148
+ const d = map[id]
149
+ if (!d || !d.settings) continue
150
+ const legacy = legacyPartitionSlice(id, io)
151
+ if (!legacy) continue
152
+ const keys = Array.isArray(d.settings.keys) ? d.settings.keys : []
153
+ const merged = Object.assign({}, snap[id])
154
+ let changed = false
155
+ for (const k of keys) {
156
+ if (legacy[k] === undefined) continue
157
+ if (!touched[id] || !touched[id][k]) { merged[k] = legacy[k]; changed = true }
158
+ }
159
+ if (!changed) continue
160
+ try {
161
+ const cleaned = d.settings.normalize(merged, (io && io.env) || undefined)
162
+ if (cleaned && typeof cleaned === 'object') snap[id] = Object.assign(snap[id], cleaned)
163
+ } catch (e) { }
164
+ }
165
+ return snap
166
+ }
167
+
168
+ export function readSettingsFile(io) {
169
+ const o = io || {}
170
+ try {
171
+ const nodeFs = o.readFileSync || (builtinModule('node:fs') || {})
172
+ const p = ('path' in o) ? o.path : settingsPath(o)
173
+ if (typeof nodeFs.readFileSync !== 'function' || !p) return null
174
+ return normalizeAll(JSON.parse(nodeFs.readFileSync(p, 'utf8')), o)
175
+ } catch (e) { return null }
176
+ }
177
+
178
+ /** 当前生效设置:统一文件缺失时 = 默认值 + 旧包文件字段级回退(仍不写盘)。 */
179
+ export function effectiveSettings(io) {
180
+ return readSettingsFile(io) || normalizeAll({}, io)
181
+ }
182
+
183
+ export function writeSettingsFile(value, io) {
184
+ const o = io || {}
185
+ const snap = normalizeAll(value, o)
186
+ snap.updatedAt = Date.now()
187
+ const p = ('path' in o) ? o.path : settingsPath(o)
188
+ if (!p) return { ok: false, error: 'dsh-home not resolved (set DSH_HOME or CLI_BRIDGE_SETTINGS_FILE)' }
189
+ const nodeFs = o.writeFileSync ? { writeFileSync: o.writeFileSync, renameSync: o.renameSync, mkdirSync: o.mkdirSync } : builtinModule('node:fs')
190
+ if (!nodeFs || typeof nodeFs.writeFileSync !== 'function') return { ok: false, error: 'fs unavailable' }
191
+ try {
192
+ const nodePath = builtinModule('node:path')
193
+ const dir = nodePath ? nodePath.dirname(p) : String(p).replace(/[\\/][^\\/]+$/, '')
194
+ if (nodePath && typeof nodeFs.mkdirSync === 'function') { try { nodeFs.mkdirSync(dir, { recursive: true }) } catch (e) { } }
195
+ const tmp = p + '.tmp-' + process.pid
196
+ nodeFs.writeFileSync(tmp, JSON.stringify(snap, null, 2), 'utf8')
197
+ if (typeof nodeFs.renameSync === 'function') nodeFs.renameSync(tmp, p)
198
+ else nodeFs.writeFileSync(p, JSON.stringify(snap, null, 2), 'utf8')
199
+ return { ok: true, path: p }
200
+ } catch (e) { return { ok: false, error: String(e && e.message || e) } }
201
+ }
202
+
203
+ /** token 掩码:只回是否已设置 + 尾 4 位,绝不把明文送回浏览器。 */
204
+ export function maskToken(tok) {
205
+ const s = String(tok || '')
206
+ if (!s) return { set: false, hint: '' }
207
+ return { set: true, hint: s.length <= 8 ? '••••' : '••••' + s.slice(-4) }
208
+ }
209
+
210
+ /** GET /cli-bridge/settings 响应体:分区视图 + 元数据 + 诊断;secret 字段掩码。 */
211
+ export function settingsView(io) {
212
+ const o = io || {}
213
+ const map = descriptors(o)
214
+ const fromFile = readSettingsFile(o)
215
+ const snap = effectiveSettings(o)
216
+ const providers = Object.create(null)
217
+ for (const id of PROVIDER_IDS) {
218
+ const d = map[id]
219
+ if (!d || !d.settings) { providers[id] = { present: false }; continue }
220
+ const sec = Array.isArray(d.settings.secretKeys) ? d.settings.secretKeys : []
221
+ const view = { present: true, fileName: SETTINGS_FILE_NAME, persisted: fromFile !== null, values: Object.assign({}, snap[id]), secrets: Object.create(null) }
222
+ for (const k of Object.keys(view.values)) {
223
+ if (sec.indexOf(k) >= 0) { view.secrets[k] = maskToken(view.values[k]); delete view.values[k] }
224
+ }
225
+ try { if (d.settings.meta) view.meta = d.settings.meta(o.env) } catch (e) { }
226
+ try { if (d.settings.diagnose) view.diagnosis = d.settings.diagnose(snap[id], o.env) } catch (e) { }
227
+ providers[id] = view
228
+ }
229
+ const sched = normalizeScheduling(snap[SCHEDULING_SECTION], PROVIDER_IDS)
230
+ const labels = Object.create(null)
231
+ for (const id of PROVIDER_IDS) labels[id] = (map[id] && map[id].label) || id
232
+ return {
233
+ fileName: SETTINGS_FILE_NAME, updatedAt: snap.updatedAt || 0, providers,
234
+ scheduling: {
235
+ enabled: sched.enabled, priority: sched.priority, order: sched.order,
236
+ all: PROVIDER_IDS.slice(), labels
237
+ }
238
+ }
239
+ }
240
+
241
+ /**
242
+ * POST body(文本)→ { ok, value?|error? }:
243
+ * body = { agy:{...}, codebuddy:{...}, mimo:{...}, <id>Clear:{key:true}? } 形式的分区对象。
244
+ * 缺省字段 = 保持现值;secret 空串 = 保持现值,`<key>Clear:true` = 清除。
245
+ */
246
+ export function parseSettingsBody(text, io) {
247
+ if (typeof text !== 'string' || !text.trim()) return { ok: false, error: 'empty body' }
248
+ let raw
249
+ try { raw = JSON.parse(text) } catch (e) { return { ok: false, error: 'invalid JSON' } }
250
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, error: 'body must be an object' }
251
+ const o = io || {}
252
+ const map = descriptors(o)
253
+ const cur = effectiveSettings(o)
254
+ const next = Object.create(null)
255
+ for (const id of PROVIDER_IDS) next[id] = Object.assign({}, cur[id] || {})
256
+ // 保持语义:body 不带 _scheduling 时沿用现值(旧客户端/其它调用方 provider-only 提交
257
+ // 绝不能把用户的调度配置悄悄重置)。
258
+ if (cur[SCHEDULING_SECTION]) next[SCHEDULING_SECTION] = Object.assign({}, cur[SCHEDULING_SECTION])
259
+ for (const id of Object.keys(raw)) {
260
+ if (PROVIDER_IDS.indexOf(id) < 0) continue
261
+ const d = map[id]
262
+ if (!d || !d.settings) continue
263
+ const part = raw[id]
264
+ if (!part || typeof part !== 'object') return { ok: false, error: 'partition ' + id + ' must be an object' }
265
+ const keys = Array.isArray(d.settings.keys) ? d.settings.keys : []
266
+ const sec = Array.isArray(d.settings.secretKeys) ? d.settings.secretKeys : []
267
+ for (const k of keys) {
268
+ const v = part[k]
269
+ if (v !== undefined && typeof v !== 'string') return { ok: false, error: 'field ' + id + '.' + k + ' must be a string' }
270
+ if (typeof v !== 'string' || !v.trim()) {
271
+ if (sec.indexOf(k) >= 0 && part[k + 'Clear'] !== true) continue // 保持现值
272
+ if (sec.indexOf(k) >= 0) next[id][k] = '' // 显式清除
273
+ continue
274
+ }
275
+ next[id][k] = v.trim()
276
+ }
277
+ }
278
+ // 调度区:整区替换语义(面板每次提交完整 enabled+priority);缺省 = 保持现值。
279
+ // 类型严格:非对象/数组元素含未知 id 直接 400,避免半写坏配置。
280
+ if (Object.prototype.hasOwnProperty.call(raw, SCHEDULING_SECTION)) {
281
+ const sec = raw[SCHEDULING_SECTION]
282
+ if (!sec || typeof sec !== 'object' || Array.isArray(sec)) return { ok: false, error: 'field _scheduling must be an object' }
283
+ for (const key of ['enabled', 'priority']) {
284
+ if (sec[key] === undefined) continue
285
+ if (!Array.isArray(sec[key])) return { ok: false, error: 'field _scheduling.' + key + ' must be an array' }
286
+ for (const x of sec[key]) {
287
+ if (typeof x !== 'string' || PROVIDER_IDS.indexOf(x) < 0) return { ok: false, error: 'unknown provider id in _scheduling.' + key + ': ' + JSON.stringify(x) }
288
+ }
289
+ }
290
+ if (sec.enabled !== undefined || sec.priority !== undefined) {
291
+ const base = normalizeScheduling(cur[SCHEDULING_SECTION], PROVIDER_IDS)
292
+ const merged = normalizeScheduling({
293
+ enabled: Array.isArray(sec.enabled) ? sec.enabled : base.enabled,
294
+ priority: Array.isArray(sec.priority) ? sec.priority : base.priority
295
+ }, PROVIDER_IDS)
296
+ next[SCHEDULING_SECTION] = { enabled: merged.enabled, priority: merged.priority }
297
+ }
298
+ }
299
+ return { ok: true, value: normalizeAll(next, o) }
300
+ }
@@ -0,0 +1,6 @@
1
+ // mcp/agy-mcp-server.mjs — 兼容入口:等价 node mcp/cli-bridge-mcp.mjs --provider=agy。
2
+ // 旧包(agy-first-bridge)的用户迁移时只需把 MCP 配置的 args 换成本文件路径,
3
+ // 其余(env 变量名等)保持 agy 侧语义不变。原参数全部保留,前面插入 --provider。
4
+ const argv = process.argv.slice(0)
5
+ process.argv = [argv[0], argv[1], '--provider=agy'].concat(argv.slice(2))
6
+ await import('./cli-bridge-mcp.mjs')
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ // mcp/cli-bridge-mcp.mjs — 统一零依赖 MCP(stdio JSON-RPC 2.0)服务器。
3
+ //
4
+ // 三个旧包各带一个 MCP 服务器(agy-mcp-server / codebuddy-mcp-server /
5
+ // mimo-mcp-server,~500 行 framing 几乎相同)。合并版:本文件只有一份 framing/
6
+ // 生命周期逻辑,工具面来自 mcp/providers/<id>.mjs(契约 §4)。
7
+ //
8
+ // 用法:
9
+ // node mcp/cli-bridge-mcp.mjs → 聚合三个 provider 的全部工具
10
+ // node mcp/cli-bridge-mcp.mjs --provider=agy → 只暴露 agy 面(等价旧 agy-mcp-server;
11
+ // mcp/<id>-mcp-server.mjs 即此形态的薄封装)
12
+ // node mcp/cli-bridge-mcp.mjs --check → 自检:provider 模块可加载、工具表
13
+ // 合法(name/inputSchema),打印 JSON 后退出
14
+ //
15
+ // 与 DSH 的 MCP 配置:command=node,args=[<绝对路径>/mcp/cli-bridge-mcp.mjs],
16
+ // env 可选 CLI_BRIDGE_PROVIDER / DSH_HOME / CLI_BRIDGE_SETTINGS_FILE 等(各 provider
17
+ // 自己约定额外的 *_BIN / *_MCP_CWD 变量,见 README)。
18
+ //
19
+ // 状态上报:每次 call 结束把该 provider 的 engine 快照原子写
20
+ // {dsh-home}/cli-bridge-mcp-<id>.json,家级灯 GET /cli-bridge/status 读盘合并 ——
21
+ // MCP 子进程没有 ctx.emit,这是唯一的跨进程通道(红线:写失败静默,绝不影响工具结果)。
22
+
23
+ import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync } from 'node:fs'
24
+ import { dirname, join, resolve } from 'node:path'
25
+ import { fileURLToPath } from 'node:url'
26
+ import { buildMcpBridgePayload } from '../core/bridge-kit.mjs'
27
+ import { resolveScheduling } from '../core/scheduling.mjs'
28
+ import * as agyMcp from './providers/agy.mjs'
29
+ import * as codebuddyMcp from './providers/codebuddy.mjs'
30
+ import * as mimoMcp from './providers/mimo.mjs'
31
+
32
+ const SERVER_NAME = 'cli-bridge-mcp'
33
+ const SERVER_VERSION = '0.2.2'
34
+ const PROTOCOL = '2024-11-05'
35
+
36
+ const PROVIDERS = [agyMcp, codebuddyMcp, mimoMcp]
37
+
38
+ function detectHomeDir() {
39
+ if (process.env.DSH_HOME) return process.env.DSH_HOME
40
+ if (process.platform === 'win32' && process.env.APPDATA) return join(process.env.APPDATA, 'DSH Desktop', 'dsh-home')
41
+ if (process.env.HOME) return join(process.env.HOME, '.dsh')
42
+ try { return dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))) } catch (e) { return process.cwd() }
43
+ }
44
+
45
+ const io = {
46
+ fs: { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync },
47
+ path: { join, dirname, resolve, sep: process.platform === 'win32' ? '\\' : '/', basename: (p) => String(p).split(/[\\/]/).pop() },
48
+ homeDir: detectHomeDir(),
49
+ env: process.env,
50
+ log: (msg) => { try { process.stderr.write('[' + SERVER_NAME + '] ' + msg + '\n') } catch (e) { } }
51
+ }
52
+
53
+ // 快照写盘器:provider 模块的 engine publish 到这里。
54
+ export function makeSnapshotWriter(id) {
55
+ const target = join(io.homeDir, 'cli-bridge-mcp-' + id + '.json')
56
+ return function publish(snap) {
57
+ try {
58
+ const payload = buildMcpBridgePayload(snap, process.pid, Date.now())
59
+ const tmp = target + '.' + process.pid + '.tmp'
60
+ try { mkdirSync(dirname(target), { recursive: true }) } catch (e) { }
61
+ writeFileSync(tmp, JSON.stringify(payload), 'utf8')
62
+ renameSync(tmp, target)
63
+ } catch (e) { /* 灯数据是尽力而为,绝不影响工具调用 */ }
64
+ }
65
+ }
66
+
67
+ function selectedProviders() {
68
+ // 钉住面(薄封装兼容语义)优先级最高:--provider= > env CLI_BRIDGE_PROVIDER。
69
+ const arg = process.argv.find((a) => a.startsWith('--provider='))
70
+ const pinned = arg ? arg.slice('--provider='.length) : (process.env.CLI_BRIDGE_PROVIDER || '')
71
+ if (pinned) {
72
+ const hit = PROVIDERS.filter((p) => p.meta.id === pinned)
73
+ if (!hit.length) { io.log('unknown --provider=' + pinned); process.exit(2) }
74
+ return hit
75
+ }
76
+ // 无钉住 → 读统一设置文件的 _scheduling:enabled 过滤 + order 决定工具面顺序
77
+ // (MCP 子进程启动时读一次;文件缺失/坏值 = 全开,source:'default')。
78
+ const ids = PROVIDERS.map((p) => p.meta.id)
79
+ const sched = resolveScheduling(ids, { env: process.env })
80
+ const byId = Object.create(null)
81
+ for (const p of PROVIDERS) byId[p.meta.id] = p
82
+ const picked = sched.order.map((id) => byId[id]).filter(Boolean)
83
+ return picked.length ? picked : PROVIDERS
84
+ }
85
+
86
+ function toolSurface() {
87
+ const out = []
88
+ for (const p of selectedProviders()) {
89
+ try { for (const t of p.tools(process.env)) out.push(Object.assign({}, t, { __provider: p.meta.id })) }
90
+ catch (e) { io.log('provider ' + p.meta.id + ' tools() failed: ' + String(e && e.message || e)) }
91
+ }
92
+ return out
93
+ }
94
+
95
+ const TOOLS = toolSurface()
96
+
97
+ function writeMsg(obj) {
98
+ try { process.stdout.write(JSON.stringify(obj) + '\n') } catch (e) { }
99
+ }
100
+
101
+ let pendingCalls = 0
102
+ let stdinClosed = false
103
+ function maybeExit() { if (stdinClosed && pendingCalls === 0) process.exit(0) }
104
+
105
+ async function handleLine(line) {
106
+ let msg
107
+ try { msg = JSON.parse(line) } catch (e) { return }
108
+ if (!msg || typeof msg !== 'object') return
109
+ const { id, method, params } = msg
110
+ const isReq = id !== undefined && id !== null
111
+ const reply = (result) => writeMsg({ jsonrpc: '2.0', id, result })
112
+ const replyErr = (code, message) => writeMsg({ jsonrpc: '2.0', id, error: { code, message } })
113
+
114
+ if (method === 'initialize') {
115
+ writeMsg({ jsonrpc: '2.0', id, result: { protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: SERVER_NAME, version: SERVER_VERSION } } })
116
+ return
117
+ }
118
+ if (method === 'notifications/initialized' || String(method || '').startsWith('notifications/')) return
119
+ if (method === 'ping') { reply({}); return }
120
+ if (method === 'tools/list') {
121
+ reply({ tools: TOOLS.map(({ __provider, ...t }) => t) })
122
+ return
123
+ }
124
+ if (method === 'tools/call') {
125
+ const name = params && params.name
126
+ const hit = TOOLS.filter((t) => t.name === name)[0]
127
+ if (!hit) { replyErr(-32602, 'Unknown tool: ' + name); return }
128
+ const provider = PROVIDERS.filter((p) => p.meta.id === hit.__provider)[0]
129
+ pendingCalls++
130
+ try {
131
+ const text = await provider.call(name, (params && params.arguments) || {}, io)
132
+ reply({ content: [{ type: 'text', text: String(text == null ? '' : text) }] })
133
+ } catch (e) {
134
+ reply({ content: [{ type: 'text', text: '[' + provider.meta.id + '] tool error: ' + String(e && e.message || e) }], isError: true })
135
+ } finally {
136
+ pendingCalls--
137
+ maybeExit()
138
+ }
139
+ return
140
+ }
141
+ if (method === 'resources/list') { reply({ resources: [] }); return }
142
+ if (method === 'prompts/list') { reply({ prompts: [] }); return }
143
+ if (isReq) replyErr(-32601, 'Method not found: ' + method)
144
+ }
145
+
146
+ if (process.argv.includes('--check')) {
147
+ const schedReport = resolveScheduling(PROVIDERS.map((p) => p.meta.id), { env: process.env })
148
+ const report = { ok: true, server: SERVER_NAME, version: SERVER_VERSION, scheduling: { source: schedReport.source, enabled: schedReport.enabled, order: schedReport.order }, providers: [], errors: [] }
149
+ for (const p of selectedProviders()) {
150
+ const entry = { id: p.meta.id, label: p.meta.label, version: p.meta.version, tools: [] }
151
+ try {
152
+ const tools = p.tools(process.env)
153
+ if (!Array.isArray(tools) || !tools.length) throw new Error('tools() must return non-empty array')
154
+ for (const t of tools) {
155
+ if (!t.name || !t.inputSchema || t.inputSchema.type !== 'object') throw new Error('bad tool def: ' + JSON.stringify(t && t.name))
156
+ entry.tools.push(t.name)
157
+ }
158
+ if (typeof p.check === 'function') p.check(process.env)
159
+ if (typeof p.call !== 'function') throw new Error('call() missing')
160
+ } catch (e) {
161
+ entry.error = String(e && e.message || e)
162
+ report.errors.push(p.meta.id + ': ' + entry.error)
163
+ }
164
+ report.providers.push(entry)
165
+ }
166
+ if (report.errors.length) report.ok = false
167
+ console.log(JSON.stringify(report, null, 2))
168
+ process.exit(report.ok ? 0 : 1)
169
+ }
170
+
171
+ let buf = ''
172
+ process.stdin.setEncoding('utf8')
173
+ process.stdin.on('data', (d) => {
174
+ buf += d
175
+ let idx
176
+ while ((idx = buf.indexOf('\n')) >= 0) {
177
+ const line = buf.slice(0, idx)
178
+ buf = buf.slice(idx + 1)
179
+ if (line.trim()) handleLine(line)
180
+ }
181
+ })
182
+ process.stdin.on('end', () => { stdinClosed = true; maybeExit() })
183
+ process.on('SIGINT', () => process.exit(0))
184
+ process.on('SIGTERM', () => process.exit(0))
@@ -0,0 +1,6 @@
1
+ // mcp/codebuddy-mcp-server.mjs — 兼容入口:等价 node mcp/cli-bridge-mcp.mjs --provider=codebuddy。
2
+ // 旧包(codebuddy-first-bridge)的用户迁移时只需把 MCP 配置的 args 换成本文件路径,
3
+ // 其余(env 变量名等)保持 codebuddy 侧语义不变。原参数全部保留,前面插入 --provider。
4
+ const argv = process.argv.slice(0)
5
+ process.argv = [argv[0], argv[1], '--provider=codebuddy'].concat(argv.slice(2))
6
+ await import('./cli-bridge-mcp.mjs')
@@ -0,0 +1,6 @@
1
+ // mcp/mimo-mcp-server.mjs — 兼容入口:等价 node mcp/cli-bridge-mcp.mjs --provider=mimo。
2
+ // 旧包(mimo-first-bridge)的用户迁移时只需把 MCP 配置的 args 换成本文件路径,
3
+ // 其余(env 变量名等)保持 mimo 侧语义不变。原参数全部保留,前面插入 --provider。
4
+ const argv = process.argv.slice(0)
5
+ process.argv = [argv[0], argv[1], '--provider=mimo'].concat(argv.slice(2))
6
+ await import('./cli-bridge-mcp.mjs')