@luchenglong/cli-bridge 0.2.5 → 0.2.6

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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,40 @@ GitHub Release(`scripts/verify.mjs` 在 CI 与 prepack 双重锁一致)。
9
9
  > - CodeBuddy 线:[docs/history/codebuddy/CHANGELOG.md](docs/history/codebuddy/CHANGELOG.md)(终版 1.6.2)
10
10
  > - MiMo 线:[docs/history/mimo/CHANGELOG.md](docs/history/mimo/CHANGELOG.md)(终版 1.2.9)
11
11
 
12
+ ## [0.2.6] - 2026-10-18
13
+
14
+ ### Fixed
15
+ - **`codebuddy_models` / 设置面板的模型清单「不是实时、也是错的」**(用户报告)。根因:清单
16
+ 读的是随包冻结的 `product.ioa.json` 静态表,实测已落后于本机 CLI ——
17
+ **静态 16 项 vs CLI 19 项**,其中 `minimax-m3` 实为 `minimax-m3-pay`、`kimi-k3-1` 实为
18
+ `kimi-k3-2`(两个 id 已失效),另缺 `step-5-preview` / `space-bunny` / `glm-5.3-flashx` / `kimi-k3-2`。
19
+ 改为**以 CLI 为实时权威源**(对齐 `workbuddy-auth` 的「`agents[cli].models` 权威白名单」思路):
20
+ - **A. `account-live`**(首选):起 CLI 用无效模型触发服务端 400,「Currently supported models
21
+ for your account」即该账号的授权清单 —— 与 workbuddy-auth 从 `/v3/config` 取白名单同义。
22
+ - **B. `cli-help`**(兜底,如实标注):解析 `--help` 的 `Currently supported: (...)`,
23
+ 零网络、随 CLI 版本自动更新。回落时 `source` 明确写 `cli-help` 并附 note,
24
+ **绝不把静态表冒充成账号可用清单**。
25
+ - 静态表降级为**仅倍率元数据**,不再参与「是否可用」判定。
26
+ - **面板 `defaultModel` 无视 `preferredBackend`**:旧实现硬编码 `DEFAULT_BACKEND`(codebuddy 面),
27
+ 无论用户选哪个后端都只列 codebuddy 面的模型。现跟随设置里的默认后端,且账号探测走与
28
+ `buildArgv` 完全同一套端点/凭据注入(`faceEnv`)—— 否则会出现「标着 codebuddy-en、
29
+ 实际问的是 codebuddy 面」的错配。
30
+ - **`--help` 名册折行导致清单抖动**:yargs 在长名册上任意列折行,实测把 `minimax-m3-pay`
31
+ 从中间断成 `minimax-` + `m3-pay`,同一命令两次运行给出 18/19 两种结果。解析改为
32
+ 「去掉括号内所有空白再按逗号切」,三次连跑稳定 19 项。
33
+ - **探测失败文案误导**:该面未登录时曾报「codebuddy CLI not found」,实际是「CLI 跑了但没吐
34
+ 账号名册」。现记录每个候选的真实结局(spawn 失败 / exit code + 输出首段),错误可指导处置。
35
+
36
+ ### Changed
37
+ - **`scripts/build.mjs` 支持内联多个片文件**:`providers/<id>-*.mjs` 全部按字典序内联进
38
+ dynamic 沙箱的同一作用域(此前只认写死的 `<id>-catalog.mjs`)。每撞一次 600 行红线就改一次
39
+ 构建器的做法不可持续。
40
+ - **`codebuddy` provider 再拆一层**:`codebuddy.mjs` 864 → 593 行,拆出 `codebuddy-catalog.mjs`
41
+ (564)与 `codebuddy-models-tool.mjs`(101),三者均在红线内。
42
+ - **`.mjs` 入口支持**:`CODEBUDDY_BIN` 指向 `.js/.mjs/.cjs` 时经 node 包装(也让测试夹具能
43
+ 完全脱离真实 CLI、可在任意机器复现)。
44
+ - 修的 `DEP0190` 告警:Windows `.cmd` 经 shell 时把命令拼成单字符串,不再传 args 数组。
45
+
12
46
  ## [0.2.5] - 2026-10-18
13
47
 
14
48
  ### Added
@@ -659,45 +659,7 @@ const SETTINGS_KEYS = ['preferredBackend', 'defaultModel', 'codebuddyEnToken', '
659
659
  // • dynamic 沙箱形态:scripts/build.mjs 把该文件内联进同一作用域(模板顶部的
660
660
  // 第三个占位标记),import 行被 stripModuleSyntax 剥掉,同名标识符天然可见。
661
661
  // ⚠️ 本注释里绝不写出那个标记的字面量:build 做全局文本替换,注释中出现同样
662
- // 会被替换成正文代码,把注释劈开 → 生成物语法崩。
663
- // ── 三工具 schema(旧 preset/codebuddy-first verbatim;契约 §8)────────────────
664
- const RUN_PARAMS = {
665
- type: 'object',
666
- additionalProperties: false,
667
- required: ['prompt'],
668
- properties: {
669
- prompt: { type: 'string', description: 'The full task/instruction for codebuddy. Be complete and self-contained.' },
670
- backend: { type: 'string', enum: BACKEND_ENUM, description: 'Which CLI face to dispatch to. codebuddy (default; domestic CodeBuddy npm CLI, product endpoint www.codebuddy.ai) for coding work; codebuddy-intl = CodeBuddy INTERNATIONAL face of the same npm CLI (product.ioa.json catalogue: claude-sonnet-5 / claude-opus-5 / gemini-3.1-pro / gpt-6-astra / hy3-ioa ...; aliases codebuddy-ioa / codebuddy-international) when the account is an international CodeBuddy account; codebuddy-en = WorkBuddy international edition (the WorkBuddyAI desktop bundled CLI at C:\\Program Files\\WorkBuddyAI, product endpoint www.workbuddy.ai; needs codebuddyEnToken unless DSH already holds a workbuddy key) — aliases workbuddy-en / workbuddy-ai also accepted; workbuddy = WorkBuddy domestic desktop CLI (product endpoint copilot.tencent.com, zero config), the office-scenario face: documents/slides/spreadsheets, knowledge-base lookups, image/video generation, WeChat/WeCom replies. New calls without a backend follow the user-preferred default backend (plugin settings). Continuing a session routes back to its owning backend automatically.' },
671
- mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'auto follows DSH plan state; plan = no writes; accept-edits = allow edits. Default auto.' },
672
- model: { type: 'string', description: MODEL_DESCRIPTION },
673
- effort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], description: 'Optional reasoning effort.' },
674
- maxTurns: { type: 'integer', description: 'Optional max agentic turns (1-500, default unlimited).' },
675
- cwd: { type: 'string', description: 'Working directory for codebuddy. Defaults to the DSH workspace root.' },
676
- addDirs: { type: 'array', items: { type: 'string' }, description: 'Extra directories to add to codebuddy workspace.' },
677
- timeoutSec: { type: 'integer', description: 'Run timeout seconds (10-3600, default 300); a DSH-side hang guard force-terminates at timeout+60s.' },
678
- background: { type: 'boolean', description: 'Run as a background job and return a jobId immediately.' }
679
- }
680
- }
681
- const CONTINUE_PARAMS = {
682
- type: 'object',
683
- additionalProperties: false,
684
- required: ['prompt'],
685
- properties: {
686
- prompt: { type: 'string', description: 'Follow-up instruction for the ongoing codebuddy conversation.' },
687
- sessionId: { type: 'string', description: 'codebuddy session id to resume (from a prior codebuddy_run result).' },
688
- latest: { type: 'boolean', description: 'Continue the most recent codebuddy conversation instead of a specific id.' },
689
- backend: { type: 'string', enum: BACKEND_ENUM, description: 'Which CLI face to resume on. codebuddy-en = WorkBuddy international (WorkBuddyAI desktop CLI; aliases workbuddy-en / workbuddy-ai accepted), workbuddy = WorkBuddy domestic desktop CLI. When omitted, the backend that owns the sessionId is used automatically; brand-new conversations follow the user-preferred default backend (plugin settings).' },
690
- mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'Execution mode; default auto.' },
691
- model: { type: 'string', description: 'Optional model id (two layers: product face carries it, account licenses it; the credit multiplier of each id comes from the CLI descriptor — see codebuddy_run).' },
692
- effort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], description: 'Optional reasoning effort.' },
693
- maxTurns: { type: 'integer', description: 'Optional max agentic turns.' },
694
- cwd: { type: 'string', description: "Working directory for codebuddy; when resuming, defaults to the resumed session's project directory." },
695
- timeoutSec: { type: 'integer', description: 'Run timeout seconds (10-3600, default 300); a DSH-side hang guard force-terminates at timeout+60s.' },
696
- background: { type: 'boolean', description: 'Run as a background job and return a jobId immediately.' }
697
- }
698
- }
699
- const STATUS_PARAMS = { type: 'object', additionalProperties: false, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
700
-
662
+ // 会被替换成正文代码,把注释劈开 → 生成物语法崩。
701
663
  function defineProvider (deps = {}) {
702
664
  const env = deps.env || {}
703
665
  let host = null // attach 后保存 hostOpts(preset/dynamic/MCP 形态宿主能力)
@@ -1084,12 +1046,23 @@ function defineProvider (deps = {}) {
1084
1046
  }
1085
1047
  },
1086
1048
 
1087
- // codebuddy_models:列出本机 codebuddy 可用模型(静态表 ∪ 描述文件 ∪ 账号清单)。
1049
+ // codebuddy_models:实时列出本机 codebuddy 可用模型(账号探测优先,回落 CLI 名册)。
1088
1050
  // 只在宿主有 spawn/fs 能力时注册(preset 注入 deps.process / MCP 注入 deps.nodeChild;
1089
1051
  // dynamic 沙箱形态省略)。**失败必须 ok:false + 可操作错误**,绝不静默返回空列表或
1090
1052
  // 伪造模型(工作手册红线 §7;对照 workbuddy-auth 的 catalog-store 静默降级 —— 不照抄)。
1091
1053
  extraTools: (deps.process || deps.nodeChild || deps.nodeFs) ? [codebuddyModelsTool({
1092
- env, deps, BACKEND_ENUM, normalizeBackend, resolveBackend, modelCatalogDetailed, getAccountCatalog
1054
+ env, deps, BACKEND_ENUM, normalizeBackend, resolveBackend, modelCatalogDetailed, getAccountCatalog,
1055
+ // faceEnv:让账号探测打到**请求的那个产品面**(与 buildArgv 完全同一套端点/凭据
1056
+ // 注入规则)。缺了它,探测会走 CLI 自身默认登录面 —— 标着 codebuddy-en 实际却
1057
+ // 问的是 codebuddy 面,得出的清单看着对其实是另一个面的(本轮修的就是这类偏差)。
1058
+ faceEnv: (backend) => {
1059
+ try {
1060
+ const settings = (deps.settings && typeof deps.settings === 'object') ? deps.settings : {}
1061
+ const authDomain = readBackendAuthDomain(backend, { env })
1062
+ const tok = resolveEnToken(backend === 'codebuddy-en' ? settings.codebuddyEnToken : null, { env })
1063
+ return endpointEnv(backend, settings.endpointOverride, authDomain, tok.token) || {}
1064
+ } catch (e) { return {} }
1065
+ }
1093
1066
  })] : [],
1094
1067
 
1095
1068
  // 设置分区(契约 §3):keystone 统一文件 {codebuddy:{...}};旧扁平文件的字段
@@ -1100,10 +1073,27 @@ function defineProvider (deps = {}) {
1100
1073
  defaults: () => normalizeSettings(null),
1101
1074
  normalize: (raw) => normalizeSettings(raw),
1102
1075
  meta() {
1103
- // defaultModel 下拉 = 默认后端(codebuddy 面)的带倍率目录(升级 CLI 后
1104
- // 自动同步);其余三面目录不同,字段允许自定义输入即可覆盖。促销角标为
1105
- // 0.1.0 有意省略(见头注 TODO)。
1106
- const options = modelCatalogDetailed(DEFAULT_BACKEND, {}).map((x) => ({ value: x.id, label: x.label }))
1076
+ // defaultModel 下拉 = **实时**取自 CLI(parseHelpModels 解析 `--help` 的
1077
+ // "Currently supported: (...)")。此前读磁盘 product.ioa.json 静态表,实测
1078
+ // 落后于 CLI(16 项 vs CLI 19 项,且 2 个 id 已改名),是「列表不对」的根因。
1079
+ // 面板必须同步返回,故用零网络的 cli-help roster;账号级权威清单由
1080
+ // codebuddy_models 工具(异步探测)提供。
1081
+ // 面跟随 preferredBackend(旧实现硬编码 codebuddy 面,无视用户设置)。
1082
+ const face = (currentPreferredBackend() && normalizeBackend(currentPreferredBackend())) || DEFAULT_BACKEND
1083
+ const live = cachedCliModels(face, () => fetchHelpModelsSync(env))
1084
+ let options = []
1085
+ let note = '账号实际可用性以服务端实时裁决;日常任务优先免费(x0.00)。'
1086
+ if (live && live.ok && live.ids.length) {
1087
+ const detail = modelCatalogDetailed(face, { env, accountModels: live.ids })
1088
+ const byId = new Map(detail.map((x) => [x.id, x]))
1089
+ options = live.ids.map((id) => {
1090
+ const d = byId.get(id)
1091
+ return { value: id, label: (d && d.label) || id }
1092
+ })
1093
+ note = '实时来自本机 codebuddy CLI 的声明名册(' + live.ids.length + ' 项,随 CLI 版本更新);当前面:' + face + '。账号级权威清单见 codebuddy_models。'
1094
+ } else {
1095
+ note = '未能从 CLI 读取模型名册(' + String((live && live.error) || 'unknown') + ')—— 下拉为空,可手动输入模型 id。'
1096
+ }
1107
1097
  return {
1108
1098
  titleSuffix: 'CodeBuddy',
1109
1099
  fields: [
@@ -1115,7 +1105,7 @@ function defineProvider (deps = {}) {
1115
1105
  {
1116
1106
  key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true, placeholder: '(CLI 默认)',
1117
1107
  options,
1118
- note: '账号实际可用性以服务端实时裁决(400 报文回吐权威清单,桥接自动缓存)。优先选能完成任务里最便宜的,日常任务优先免费(x0.00)。'
1108
+ note
1119
1109
  },
1120
1110
  { key: 'codebuddyEnToken', label: 'WorkBuddy 国际版 Token', type: 'secret', note: '仅 backend="codebuddy-en" 需要。国际版登录 token 被 protector key 封装不落盘,只能手工提供;留空则自动尝试 DSH 凭据库的 WORKBUDDY_TOKEN。' },
1121
1111
  { key: 'endpointOverride', label: '端点覆盖', type: 'text', placeholder: 'https://…/v2', note: '一般留空:端点按登录域自动对齐(www.codebuddy.cn 等)。仅在需要强制指定 API 端点时填写,且必须自带 /v2。' }
@@ -1199,6 +1189,7 @@ function defineProvider (deps = {}) {
1199
1189
  return descriptor
1200
1190
  }
1201
1191
 
1192
+ // ── inlined from providers/codebuddy-catalog.mjs ──
1202
1193
  // providers/codebuddy-catalog.mjs — codebuddy 模型目录/积分/描述文件解析(纯逻辑)。
1203
1194
  //
1204
1195
  // 从 providers/codebuddy.mjs 拆出:主文件原 864 行远超 600 行红线(规则 §26)。
@@ -1431,6 +1422,206 @@ const BACKEND_ENUM = ['codebuddy', 'codebuddy-intl', 'codebuddy-ioa', 'codebuddy
1431
1422
  return v && v.length ? v.slice() : null
1432
1423
  }
1433
1424
 
1425
+ // ── 实时模型清单(CLI 是一手权威来源,对齐 workbuddy-auth 的「权威白名单」思路)──
1426
+ //
1427
+ // 为什么不再以静态表/磁盘描述文件为主:实测本机 codebuddy CLI --
1428
+ // • 账号实时清单(用无效模型触发 400 报文)= 19 项
1429
+ // • CLI `--help` 声明的 roster = 19 项,与账号清单完全一致
1430
+ // • 我们原先读的 product.ioa.json 静态表 = 16 项,且 2 个 id 已过时
1431
+ // (minimax-m3 → minimax-m3-pay、kimi-k3-1 → kimi-k3-2),另缺 4 个新模型。
1432
+ // 静态表是「随包冻结」的,必然落后于 CLI,故只保留作倍率元数据与最后兜底。
1433
+ //
1434
+ // 两级实时来源(都不静默冒充权威数据,各带 source 标签):
1435
+ // A. account-live —— 起 CLI 用无效模型探测「该账号当前支持」的清单(需该面已登录,
1436
+ // 一次网络往返;对照 workbuddy-auth 的 agents[cli].models 白名单语义)。
1437
+ // B. cli-help —— 解析 `--help` 的 "Currently supported: (...)"(零网络、随 CLI
1438
+ // 版本自动更新;产品面级 roster)。
1439
+ // 静态表仅作 credit 元数据与 descriptor-static 兜底标签,绝不冒充实时。
1440
+
1441
+ /** 解析 CLI `--help` 里声明的模型 roster。空/畸形输入返回 []。
1442
+ * ⚠️ 名册很长时 yargs 会在任意列**折行**(实测把 `minimax-m3-pay` 从中间断成
1443
+ * `minimax-` + `m3-pay`),若按行/按逗号直接切会丢项或产生残片(同一命令两次运行
1444
+ * 给出 18/19 两种结果)。故先去掉括号内所有空白再按逗号切:逗号是唯一分隔符,
1445
+ * 折行插入的空白一律视为噪声。 */
1446
+ function parseHelpModels(helpText) {
1447
+ const m = /Currently supported:\s*\(([^)]+)\)/.exec(String(helpText || ''))
1448
+ if (!m) return []
1449
+ const out = []
1450
+ for (const raw of m[1].replace(/\s+/g, '').split(',')) {
1451
+ const id = raw.trim()
1452
+ if (id && /^[A-Za-z0-9_.\-]+$/.test(id) && out.indexOf(id) < 0) out.push(id)
1453
+ }
1454
+ return out
1455
+ }
1456
+
1457
+ /** 解析 codebuddy CLI 的可执行候选(env > npm 全局 shim > PATH 名)。 */
1458
+ function cbCatalogCliCandidates(env) {
1459
+ const e = env || cbCatalogEnv || {}
1460
+ const out = []
1461
+ const push = (p) => { if (p && out.indexOf(p) < 0) out.push(p) }
1462
+ if (e.CODEBUDDY_BIN) push(String(e.CODEBUDDY_BIN))
1463
+ const joinP = (a, b) => cbCatalogPathJoin(a, b)
1464
+ if (e.APPDATA) push(joinP(joinP(e.APPDATA, 'npm'), 'codebuddy.cmd'))
1465
+ if (e.APPDATA) push(joinP(joinP(e.APPDATA, 'npm'), 'codebuddy'))
1466
+ push('codebuddy')
1467
+ return out
1468
+ }
1469
+
1470
+ const CLI_MODEL_CACHE = new Map() // backend → { at, mode, ids }
1471
+ const CLI_MODEL_TTL_MS = 5 * 60 * 1000
1472
+
1473
+ // Windows .cmd/.bat 需要经 shell(Node 安全限制),但 `shell:true` + args 数组会触发
1474
+ // DEP0190(参数不转义)。这里统一做法:把命令与参数拼成【一个】字符串交给 shell,
1475
+ // 参数各自按 Windows 规则加引号 —— 既避开 DEP0190,又保证含空格参数不被拆开。
1476
+ function quoteWinArg(a) {
1477
+ const s = String(a)
1478
+ if (s === '') return '""'
1479
+ if (!/[\s"&|<>^]/.test(s)) return s
1480
+ return '"' + s.replace(/"/g, '\\"') + '"'
1481
+ }
1482
+ function cliCommandLine(exe, args) {
1483
+ return [exe].concat(args || []).map(quoteWinArg).join(' ')
1484
+ }
1485
+
1486
+ /** 把候选拆成 { cmd, prefixArgs }:.mjs/.js/.cjs 目标经 node 包装(测试夹具与自定义
1487
+ * 入口都靠这条),其余原样。 */
1488
+ function splitExeSpec(exe) {
1489
+ if (/\.(mjs|cjs|js)$/i.test(String(exe))) {
1490
+ const proc = globalThis.process
1491
+ return { cmd: (proc && proc.execPath) ? proc.execPath : 'node', prefixArgs: [String(exe)] }
1492
+ }
1493
+ return { cmd: String(exe), prefixArgs: [] }
1494
+ }
1495
+
1496
+ function cliSpawnSync(exe, args, opts) {
1497
+ const proc = globalThis.process
1498
+ const child = (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule('node:child_process') : null
1499
+ if (!child || typeof child.spawnSync !== 'function') return null
1500
+ const base = Object.assign({
1501
+ encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe']
1502
+ }, opts || {})
1503
+ const spec = splitExeSpec(exe)
1504
+ const argv = spec.prefixArgs.concat(args || [])
1505
+ const isCmd = /\.(cmd|bat)$/i.test(spec.cmd)
1506
+ if (isCmd) base.shell = true
1507
+ try {
1508
+ // shell 形态不带 args 数组(否则 DEP0190);非 shell 形态正常传数组。
1509
+ return isCmd
1510
+ ? child.spawnSync(cliCommandLine(spec.cmd, argv), base)
1511
+ : child.spawnSync(spec.cmd, argv, base)
1512
+ } catch (e) { return null }
1513
+ }
1514
+
1515
+ /**
1516
+ * 同步取「CLI 声明的 roster」(零网络,随 CLI 版本走)。
1517
+ * @returns {{ok:boolean, ids:string[], mode:string, error?:string}}
1518
+ */
1519
+ function fetchHelpModelsSync(env, opts) {
1520
+ const o = opts || {}
1521
+ const cands = cbCatalogCliCandidates(env)
1522
+ let lastErr = null
1523
+ for (const exe of cands) {
1524
+ const r = cliSpawnSync(exe, ['--help'], { timeout: Math.max(1000, Math.min(o.timeoutMs || 12000, 30000)) })
1525
+ if (!r || r.error) { lastErr = 'spawn failed: ' + String((r && r.error && r.error.message) || 'unavailable'); continue }
1526
+ const text = String(r.stdout || '') + String(r.stderr || '')
1527
+ const ids = parseHelpModels(text)
1528
+ if (ids.length) return { ok: true, ids, mode: 'cli-help', exe }
1529
+ lastErr = 'no roster in --help output'
1530
+ }
1531
+ return { ok: false, ids: [], mode: 'cli-help', error: lastErr || 'codebuddy CLI not found (set CODEBUDDY_BIN)' }
1532
+ }
1533
+
1534
+ /**
1535
+ * 异步探测「该账号当前支持」的清单:用无效模型触发 CLI 的 400 报文。
1536
+ * 对齐 workbuddy-auth 的权威白名单语义(服务器告诉我们这个账号能用什么)。
1537
+ * @returns {Promise<{ok:boolean, ids:string[], mode:string, error?:string}>}
1538
+ */
1539
+ function probeAccountModels(env, opts) {
1540
+ const o = opts || {}
1541
+ const cands = cbCatalogCliCandidates(env)
1542
+ const proc = globalThis.process
1543
+ const child = (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule('node:child_process') : null
1544
+ if (!child || typeof child.spawn !== 'function') {
1545
+ return Promise.resolve({ ok: false, ids: [], mode: 'account-live', error: 'no spawn capability in this host form' })
1546
+ }
1547
+ const extraEnv = o.envExtra || {}
1548
+ return new Promise((resolve) => {
1549
+ let idx = 0
1550
+ // 记录每个候选的真实结局:区分「压根没找到 CLI」与「CLI 跑了但没吐账号名录」
1551
+ // (后者通常是该面未登录 / 端点拒了)—— 报错文案必须能指导下一步,不能一律
1552
+ // 说 "CLI not found"(本轮就踩到:codebuddy-en 无 token 时被误报成找不到 CLI)。
1553
+ const reasons = []
1554
+ const attempt = () => {
1555
+ if (idx >= cands.length) {
1556
+ resolve({
1557
+ ok: false, ids: [], mode: 'account-live',
1558
+ error: reasons.length ? reasons.join('; ') : 'no CLI candidate to probe',
1559
+ notFound: reasons.every((r) => /not found|ENOENT|spawn/i.test(r))
1560
+ })
1561
+ return
1562
+ }
1563
+ const exe = cands[idx++]
1564
+ let c
1565
+ try {
1566
+ // .cmd/.bat 需经 shell(Windows 安全限制);.mjs/.js 经 node 包装。
1567
+ // 与 cliSpawnSync 同一规则:shell 形态把命令拼成单字符串(避免 DEP0190)。
1568
+ const spec = splitExeSpec(exe)
1569
+ const isCmd = /\.(cmd|bat)$/i.test(spec.cmd)
1570
+ const probeArgs = spec.prefixArgs.concat(['-p', 'hi', '--model', '__clb_probe_invalid__', '--output-format', 'text'])
1571
+ const opts = {
1572
+ stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
1573
+ env: Object.assign({}, proc.env, extraEnv)
1574
+ }
1575
+ c = isCmd
1576
+ ? child.spawn(cliCommandLine(spec.cmd, probeArgs), Object.assign({ shell: true }, opts))
1577
+ : child.spawn(spec.cmd, probeArgs, opts)
1578
+ } catch (e) { reasons.push(exe + ': spawn threw ' + String(e && e.message || e)); attempt(); return }
1579
+ let out = ''
1580
+ const timer = setTimeout(() => { try { c.kill() } catch (e) { } }, Math.max(5000, Math.min(o.timeoutMs || 30000, 90000)))
1581
+ const onData = (d) => { out += d; if (out.length > 400000) out = out.slice(-200000) }
1582
+ c.stdout.on('data', onData)
1583
+ c.stderr.on('data', onData)
1584
+ c.on('error', (e) => {
1585
+ clearTimeout(timer)
1586
+ reasons.push(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
1587
+ attempt()
1588
+ })
1589
+ c.on('close', (code) => {
1590
+ clearTimeout(timer)
1591
+ const ids = parseAccountModels(out)
1592
+ if (ids && ids.length) { resolve({ ok: true, ids, mode: 'account-live', exe }); return }
1593
+ const head = String(out).replace(/\s+/g, ' ').trim().slice(0, 160)
1594
+ reasons.push(exe + ': exit ' + code + ', no account roster' + (head ? ' — ' + head : ''))
1595
+ attempt()
1596
+ })
1597
+ }
1598
+ attempt()
1599
+ })
1600
+ }
1601
+
1602
+ /** 缓存读写(面板每次渲染都会取,避免反复起进程)。 */
1603
+ function cachedCliModels(backend, load, nowMs) {
1604
+ const at = Number(nowMs) || Date.now()
1605
+ const hit = CLI_MODEL_CACHE.get(backend)
1606
+ if (hit && at - hit.at < CLI_MODEL_TTL_MS) return hit
1607
+ const v = typeof load === 'function' ? load() : load
1608
+ if (v && v.ok && v.ids && v.ids.length) CLI_MODEL_CACHE.set(backend, { at, mode: v.mode, ids: v.ids.slice() })
1609
+ return v
1610
+ }
1611
+
1612
+ /** 读统一设置文件里 codebuddy 分区的 preferredBackend(meta 需要它才知道列哪个面)。 */
1613
+ function currentPreferredBackend() {
1614
+ try {
1615
+ const e = cbCatalogEnv || {}
1616
+ const p = cbCatalogRead(e.CLI_BRIDGE_SETTINGS_FILE || cbCatalogPathJoin(
1617
+ e.DSH_HOME || (e.APPDATA ? cbCatalogPathJoin(e.APPDATA, 'DSH Desktop', 'dsh-home') : ''), 'cli-bridge-settings.json'))
1618
+ if (!p) return null
1619
+ const all = JSON.parse(p)
1620
+ const v = all && all.codebuddy && all.codebuddy.preferredBackend
1621
+ return typeof v === 'string' && v ? v : null
1622
+ } catch (err) { return null }
1623
+ }
1624
+
1434
1625
  // CLI 报错里解析「该账号当前支持的模型」(旧 core parseAccountModels verbatim)。
1435
1626
  function parseAccountModels(text) {
1436
1627
  const s = String(text || '')
@@ -1566,6 +1757,52 @@ const CREDENTIAL_KEYS = ['WORKBUDDY_TOKEN', 'WORKBUDDY_AI_TOKEN', 'CODEBUDDY_EN_
1566
1757
  return { token: null, source: null }
1567
1758
  }
1568
1759
 
1760
+ // ── 三工具 schema(旧 preset/codebuddy-first verbatim;契约 §8 红线)────────────
1761
+ // 纯数据,无依赖 —— 放这里是为了让主描述符留在 600 行红线内(规则 §26)。
1762
+ const RUN_PARAMS = {
1763
+ type: 'object',
1764
+ additionalProperties: false,
1765
+ required: ['prompt'],
1766
+ properties: {
1767
+ prompt: { type: 'string', description: 'The full task/instruction for codebuddy. Be complete and self-contained.' },
1768
+ backend: { type: 'string', enum: BACKEND_ENUM, description: 'Which CLI face to dispatch to. codebuddy (default; domestic CodeBuddy npm CLI, product endpoint www.codebuddy.ai) for coding work; codebuddy-intl = CodeBuddy INTERNATIONAL face of the same npm CLI (product.ioa.json catalogue: claude-sonnet-5 / claude-opus-5 / gemini-3.1-pro / gpt-6-astra / hy3-ioa ...; aliases codebuddy-ioa / codebuddy-international) when the account is an international CodeBuddy account; codebuddy-en = WorkBuddy international edition (the WorkBuddyAI desktop bundled CLI at C:\\Program Files\\WorkBuddyAI, product endpoint www.workbuddy.ai; needs codebuddyEnToken unless DSH already holds a workbuddy key) — aliases workbuddy-en / workbuddy-ai also accepted; workbuddy = WorkBuddy domestic desktop CLI (product endpoint copilot.tencent.com, zero config), the office-scenario face: documents/slides/spreadsheets, knowledge-base lookups, image/video generation, WeChat/WeCom replies. New calls without a backend follow the user-preferred default backend (plugin settings). Continuing a session routes back to its owning backend automatically.' },
1769
+ mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'auto follows DSH plan state; plan = no writes; accept-edits = allow edits. Default auto.' },
1770
+ model: { type: 'string', description: MODEL_DESCRIPTION },
1771
+ effort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], description: 'Optional reasoning effort.' },
1772
+ maxTurns: { type: 'integer', description: 'Optional max agentic turns (1-500, default unlimited).' },
1773
+ cwd: { type: 'string', description: 'Working directory for codebuddy. Defaults to the DSH workspace root.' },
1774
+ addDirs: { type: 'array', items: { type: 'string' }, description: 'Extra directories to add to codebuddy workspace.' },
1775
+ timeoutSec: { type: 'integer', description: 'Run timeout seconds (10-3600, default 300); a DSH-side hang guard force-terminates at timeout+60s.' },
1776
+ background: { type: 'boolean', description: 'Run as a background job and return a jobId immediately.' }
1777
+ }
1778
+ }
1779
+ const CONTINUE_PARAMS = {
1780
+ type: 'object',
1781
+ additionalProperties: false,
1782
+ required: ['prompt'],
1783
+ properties: {
1784
+ prompt: { type: 'string', description: 'Follow-up instruction for the ongoing codebuddy conversation.' },
1785
+ sessionId: { type: 'string', description: 'codebuddy session id to resume (from a prior codebuddy_run result).' },
1786
+ latest: { type: 'boolean', description: 'Continue the most recent codebuddy conversation instead of a specific id.' },
1787
+ backend: { type: 'string', enum: BACKEND_ENUM, description: 'Which CLI face to resume on. codebuddy-en = WorkBuddy international (WorkBuddyAI desktop CLI; aliases workbuddy-en / workbuddy-ai accepted), workbuddy = WorkBuddy domestic desktop CLI. When omitted, the backend that owns the sessionId is used automatically; brand-new conversations follow the user-preferred default backend (plugin settings).' },
1788
+ mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'Execution mode; default auto.' },
1789
+ model: { type: 'string', description: 'Optional model id (two layers: product face carries it, account licenses it; the credit multiplier of each id comes from the CLI descriptor — see codebuddy_run).' },
1790
+ effort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], description: 'Optional reasoning effort.' },
1791
+ maxTurns: { type: 'integer', description: 'Optional max agentic turns.' },
1792
+ cwd: { type: 'string', description: "Working directory for codebuddy; when resuming, defaults to the resumed session's project directory." },
1793
+ timeoutSec: { type: 'integer', description: 'Run timeout seconds (10-3600, default 300); a DSH-side hang guard force-terminates at timeout+60s.' },
1794
+ background: { type: 'boolean', description: 'Run as a background job and return a jobId immediately.' }
1795
+ }
1796
+ }
1797
+ const STATUS_PARAMS = { type: 'object', additionalProperties: false, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
1798
+
1799
+ // ── inlined from providers/codebuddy-models-tool.mjs ──
1800
+ // providers/codebuddy-models-tool.mjs — codebuddy_models 工具定义(纯逻辑)。
1801
+ //
1802
+ // 从 codebuddy-catalog.mjs 再拆出:两个文件都要留在 600 行红线内(规则 §26)。
1803
+ // scripts/build.mjs 会把 providers/codebuddy-*.mjs 全部内联进 dynamic 沙箱的同一个
1804
+ // 作用域,所以这里仍可引用 catalog 的函数(沙箱内天然可见);ESM 形态走本文件的 import。
1805
+ // ⚠️ 注释里绝不写出口令标记字面量(build 全局替换会劈开注释 → 生成物语法崩)。
1569
1806
  // ── codebuddy_models 工具定义(搬自主文件;纯逻辑,依赖经 ctx 注入)────────────
1570
1807
  // ctx = { env, deps, resolveBackend, modelCatalogDetailed, getAccountCatalog }
1571
1808
  function codebuddyModelsTool(ctx) {
@@ -1574,7 +1811,7 @@ function codebuddyModelsTool(ctx) {
1574
1811
  const env = c.env || {}
1575
1812
  return {
1576
1813
  name: 'codebuddy_models',
1577
- description: 'List the models available to the local codebuddy CLI for a given backend. Merges three layers: the built-in product descriptor id table, any descriptor file on disk, and the account catalogue harvested from a previous CLI run (authorized ids first). Each item carries its credit multiplier (x0.00 = free). Use it to pick a model before codebuddy_run, or to see which ids are free. Returns ok:false with an actionable error when no catalogue can be read — it never invents a model list.',
1814
+ description: 'List the models the local codebuddy CLI can use for a given backend, read LIVE from the CLI. Primary source: an account probe (runs the CLI with a deliberately invalid model id, which makes the server print "Currently supported models for your account") — this is the authoritative licensed list for THIS account. If that face is not logged in, it falls back to the roster the installed CLI declares in --help and says so in the source field. Credit multipliers (x0.00 = free) come from the product descriptor as metadata. Returns ok:false with an actionable error when no live roster can be read — it never presents a frozen static table as if it were live.',
1578
1815
  inputSchema: {
1579
1816
  type: 'object', additionalProperties: false, required: [],
1580
1817
  properties: {
@@ -1596,41 +1833,73 @@ function codebuddyModelsTool(ctx) {
1596
1833
  lines.push(' ' + m.id + (m.creditLabel ? ' ' + m.creditLabel : '') + (flags.length ? ' [' + flags.join(',') + ']' : ''))
1597
1834
  }
1598
1835
  if (list.length > 40) lines.push(' … and ' + (list.length - 40) + ' more')
1599
- lines.push('source: ' + (v.source || 'unknown') + (v.accountCatalogue ? ' · account catalogue available' : ' · no account catalogue yet (run once to harvest)'))
1836
+ const srcLabel = v.source === 'account-live'
1837
+ ? 'account-live (authoritative for this account)'
1838
+ : 'cli-help (installed CLI roster, not the licensed subset)'
1839
+ lines.push('source: ' + srcLabel)
1840
+ if (v.note) lines.push('note: ' + v.note)
1600
1841
  return [{ type: 'text', text: lines.join('\n') }]
1601
1842
  },
1602
1843
  async logic(args) {
1603
1844
  const a = args || {}
1604
- const preferred = (deps.settings && deps.settings.preferredBackend) || null
1845
+ const preferred = currentPreferredBackend() || (deps.settings && deps.settings.preferredBackend) || null
1605
1846
  const backend = c.normalizeBackend(a.backend) || c.resolveBackend({}, null, preferred)
1606
- let items = []
1607
- try {
1608
- items = c.modelCatalogDetailed(backend, { env }) || []
1609
- } catch (e) {
1610
- return { ok: false, backend, error: 'catalogue read failed: ' + String(e && e.message || e) }
1847
+
1848
+ // ① 账号实时(最权威,对照 workbuddy-auth 的 agents[cli] 白名单语义)。
1849
+ // 该面未登录/无 token 时探测会失败 —— 失败就**明确降级到 CLI 名册并标注**,
1850
+ // 绝不把静态表冒充成账号可用清单。
1851
+ const extraEnv = (c.faceEnv && c.faceEnv(backend)) || {}
1852
+ const probe = await probeAccountModels(env, { envExtra: extraEnv, timeoutMs: a.timeoutMs })
1853
+ let ids = null
1854
+ let source = null
1855
+ let note = ''
1856
+ if (probe.ok && probe.ids.length) {
1857
+ ids = probe.ids
1858
+ source = 'account-live'
1859
+ } else {
1860
+ // ② CLI 声明的 roster(零网络,随 CLI 版本更新)。
1861
+ const help = fetchHelpModelsSync(env, { timeoutMs: a.timeoutMs })
1862
+ if (help.ok && help.ids.length) {
1863
+ ids = help.ids
1864
+ source = 'cli-help'
1865
+ note = 'account probe unavailable (' + String(probe.error || 'not logged in for this face') +
1866
+ '); showing the roster the installed CLI declares. It reflects the CLI version, not the licensed subset of this account.'
1867
+ }
1611
1868
  }
1612
- if (!Array.isArray(items) || !items.length) {
1869
+ if (!ids || !ids.length) {
1613
1870
  return {
1614
1871
  ok: false, backend,
1615
- error: 'no model catalogue could be read for backend "' + backend + '". Check that the codebuddy CLI is installed (CODEBUDDY_BIN) and that its product descriptor exists, or run the CLI once so the account catalogue can be harvested.'
1872
+ error: 'no live model roster could be read for backend "' + backend + '". ' +
1873
+ 'Account probe: ' + String(probe.error || 'failed') + '. ' +
1874
+ 'Install/fix the codebuddy CLI (CODEBUDDY_BIN) and make sure this face is logged in, then retry.'
1616
1875
  }
1617
1876
  }
1618
- const acctIds = c.getAccountCatalog(backend)
1619
- const accountCatalogue = !!(acctIds && acctIds.length)
1620
- let list = items.map((x) => ({
1621
- id: x.id, authorized: !!x.authorized, credits: typeof x.credits === 'number' ? x.credits : null,
1622
- creditLabel: x.creditLabel || x.label || '', label: x.label || x.id
1623
- }))
1877
+
1878
+ // ③ 倍率/授权元数据(来自描述文件;仅作标注,不参与「是否可用」判定)。
1879
+ let detail = []
1880
+ try { detail = c.modelCatalogDetailed(backend, { env, accountModels: ids }) || [] } catch (e) { detail = [] }
1881
+ const byId = new Map(detail.map((x) => [x.id, x]))
1882
+ let list = ids.map((id) => {
1883
+ const d = byId.get(id)
1884
+ return {
1885
+ id,
1886
+ authorized: d ? !!d.authorized : (source === 'account-live'),
1887
+ credits: d && typeof d.credits === 'number' ? d.credits : null,
1888
+ creditLabel: (d && (d.creditLabel || d.label)) || '',
1889
+ label: (d && d.label) || id
1890
+ }
1891
+ })
1624
1892
  if (a.authorizedOnly) list = list.filter((m) => m.authorized)
1625
1893
  if (a.freeOnly) list = list.filter((m) => m.credits === 0)
1626
1894
  return {
1627
1895
  ok: true, backend, models: list, count: list.length,
1628
- source: accountCatalogue ? 'account+descriptor' : 'descriptor',
1629
- accountCatalogue
1896
+ source, note: note || undefined,
1897
+ probedAt: Date.now()
1630
1898
  }
1631
1899
  }
1632
1900
  }
1633
- }
1901
+ }
1902
+
1634
1903
 
1635
1904
  // core/scheduling.mjs — CLI 调度(启用集合 + 调用优先级)单源纯逻辑。
1636
1905
  //
@@ -31,7 +31,7 @@ import * as mimoMcp from './providers/mimo.mjs'
31
31
  import * as opencodeMcp from './providers/opencode.mjs'
32
32
 
33
33
  const SERVER_NAME = 'cli-bridge-mcp'
34
- const SERVER_VERSION = '0.2.5'
34
+ const SERVER_VERSION = '0.2.6'
35
35
  const PROTOCOL = '2024-11-05'
36
36
 
37
37
  const PROVIDERS = [agyMcp, codebuddyMcp, mimoMcp, opencodeMcp]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luchenglong/cli-bridge",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
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
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -91,3 +91,4 @@
91
91
  }
92
92
 
93
93
 
94
+