@luchenglong/cli-bridge 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/PROVIDER-CONTRACT.md +87 -5
  3. package/README.md +12 -11
  4. package/docs/HANDOVER.md +109 -15
  5. package/dynamic/agy.host.js +1 -1
  6. package/dynamic/claude.host.js +1858 -0
  7. package/dynamic/claude.host.template.mjs +108 -0
  8. package/dynamic/codebuddy.host.js +1427 -1379
  9. package/dynamic/codex.host.js +1930 -0
  10. package/dynamic/codex.host.template.mjs +137 -0
  11. package/dynamic/mimo.host.js +721 -515
  12. package/dynamic/mimo.host.template.mjs +2 -0
  13. package/dynamic/opencode.host.js +741 -585
  14. package/dynamic/opencode.host.template.mjs +2 -0
  15. package/home-plugin/cli-bridge-indicator/lib/client.js +74 -90
  16. package/home-plugin/cli-bridge-indicator/lib/index.mjs +48 -0
  17. package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +2 -3
  18. package/mcp/cli-bridge-mcp.mjs +4 -2
  19. package/mcp/providers/claude.mjs +231 -0
  20. package/mcp/providers/codebuddy.mjs +427 -427
  21. package/mcp/providers/codex.mjs +239 -0
  22. package/package.json +6 -2
  23. package/preset/claude-first/agent.cordis.yml +136 -0
  24. package/preset/claude-first/cli-bridge.mjs +131 -0
  25. package/preset/claude-first/plugin-list.mjs +28 -0
  26. package/preset/claude-first/preset.yml +3 -0
  27. package/preset/codex-first/agent.cordis.yml +136 -0
  28. package/preset/codex-first/cli-bridge.mjs +160 -0
  29. package/preset/codex-first/plugin-list.mjs +28 -0
  30. package/preset/codex-first/preset.yml +3 -0
  31. package/providers/claude-exe.mjs +112 -0
  32. package/providers/claude-models.mjs +338 -0
  33. package/providers/claude-policy.mjs +37 -0
  34. package/providers/claude.mjs +561 -0
  35. package/providers/codebuddy-catalog.mjs +367 -599
  36. package/providers/codebuddy-consts.mjs +72 -72
  37. package/providers/codebuddy-models-source.mjs +254 -0
  38. package/providers/codebuddy-models-tool.mjs +106 -106
  39. package/providers/codebuddy-schemas.mjs +46 -46
  40. package/providers/codebuddy.mjs +600 -578
  41. package/providers/codex-cli.mjs +405 -0
  42. package/providers/codex-models.mjs +107 -0
  43. package/providers/codex.mjs +585 -0
  44. package/providers/mimo-pure.mjs +270 -111
  45. package/providers/mimo.mjs +453 -517
  46. package/providers/opencode-exe.mjs +190 -63
  47. package/providers/opencode.mjs +553 -587
  48. package/providers/registry.mjs +25 -1
  49. package/scripts/build.mjs +261 -262
  50. package/scripts/verify.mjs +8 -2
@@ -2,6 +2,7 @@
2
2
  //
3
3
  // 构建(node scripts/build.mjs --only=opencode)把 core/bridge-kit.mjs 与
4
4
  // providers/opencode.mjs 分别内联进下方两个占位标记行(各占一行、行内仅此标记),
5
+ // 第三个占位标记行负责内联 providers/opencode-*.mjs 片文件(可执行探测/模型解析),
5
6
  // 生成 dynamic/opencode.host.js。注意:标记字面量绝不能出现在本注释里 —— 构建做全局
6
7
  // 替换,注释中出现同样会被替换成正文代码而炸掉语法。
7
8
  // 沙箱纪律(旧仓 dynamic/host.js 头注 verbatim 精神):
@@ -13,6 +14,7 @@
13
14
  // 推给家级灯;动态形态不宣告 mode(旧语义:普通模式灯仅在调用时临时显示)。
14
15
  /*__KIT__*/
15
16
  /*__PROVIDER__*/
17
+ /*__EXTRA__*/
16
18
  /*__SCHED__*/
17
19
 
18
20
  return {
@@ -1,11 +1,6 @@
1
- // cli-bridge-indicator — browser half (home-level plugin). 一盏灯聚合三个 CLI:AGY / CB / MIMO。
2
- //
3
- // 合并 agy-indicator / codebuddy-indicator / mimo-indicator 三个家级客户端为单文件:
4
- // 每 1.2s 轮询 GET /cli-bridge/status → { providers:{ agy,codebuddy,mimo } },
5
- // 每个 provider 各画一组 pill(有活动=全局显示;空转「就绪」灯=仅本会话该 preset 时显示)。
6
- // settings.section 一个分区「CLI Bridge」,按 provider 卡片渲染偏好(字段由后端 meta 驱动,
7
- // 本文件不硬编码任何 provider 专有字段 —— provider 侧改设置项无需动这层)。
8
- //
1
+ // cli-bridge-indicator — browser half (home-level plugin),一盏灯聚合本机 CLI。
2
+ // 每 1.2s 轮询 GET /cli-bridge/status → { providers:{...} },每个 provider 画一组 pill(有活动=全局
3
+ // 显示;空转「就绪」灯=仅本会话在该 preset 时显示)。settings 分区字段全由后端 meta 驱动。
9
4
  // running→蓝呼吸 ⟳ ok→绿 ✓ failed→红 ✗ fallback→琥珀 ↩ idle→灰 标签。
10
5
  //
11
6
  // ⚠⚠⚠ 红线 4(历史事故:agy v1.6.1 / codebuddy v1.1.8):下方 __ModuleLoader__.load({ id })
@@ -52,89 +47,50 @@ window.__ModuleLoader__.load({
52
47
  return out;
53
48
  }
54
49
 
55
-
56
50
  const CSS = [
57
- ".clb-row{display:inline-flex;align-items:center;gap:6px}",
58
- ".clb-ind{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 9px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);font-size:12px;line-height:1;color:var(--dsw-alias-label-secondary);white-space:nowrap;user-select:none}",
59
- ".clb-ind:hover{border-color:var(--dsw-alias-border-l2);cursor:pointer}",
60
- ".clb-dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto;background:var(--dsw-alias-label-secondary)}",
61
- ".clb-ind b{font-weight:600}",
62
- ".clb-run .clb-dot{background:var(--dsw-static-blue-500,#3b82f6);animation:clb-pulse 1s ease-in-out infinite}",
63
- ".clb-ok .clb-dot{background:var(--dsw-static-green-500,#22c55e)}",
64
- ".clb-fail .clb-dot{background:var(--dsw-alias-state-error-primary)}",
65
- ".clb-fb .clb-dot{background:var(--dsw-alias-state-warn-primary)}",
66
- ".clb-run{color:var(--dsw-static-blue-500,#3b82f6);border-color:var(--dsw-static-blue-500,#3b82f6)}",
67
- ".clb-ok{color:var(--dsw-static-green-500,#22c55e);border-color:var(--dsw-static-green-500,#22c55e)}",
68
- ".clb-fb{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}",
69
- "@keyframes clb-pulse{0%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.72)}100%{opacity:1;transform:scale(1)}}",
70
- ".clb-pop-overlay{position:fixed;inset:0;background:rgba(0,0,0,.32);z-index:10000;display:flex;align-items:center;justify-content:center}",
71
- ".clb-pop-panel{width:560px;max-width:92vw;max-height:76vh;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.35);font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary);overflow:hidden}",
72
- ".clb-pop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);font-weight:600}",
73
- ".clb-pop-close{border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);border-radius:6px;width:22px;height:22px;line-height:1;font-size:13px;cursor:pointer}",
74
- ".clb-pop-close:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}",
75
- ".clb-pop-body{overflow:auto;padding:12px 14px}",
76
- ".clb-pop-sec{margin-bottom:14px;padding-bottom:12px;border-bottom:1px dashed var(--dsw-alias-border-l1)}",
77
- ".clb-pop-sec:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}",
78
- ".clb-pop-sech{font-weight:600;margin-bottom:6px}",
79
- ".clb-pop-proj{margin:6px 0;padding:8px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px}",
80
- ".clb-pop-proj-head{font-weight:600;margin-bottom:4px}",
81
- ".clb-pop-mono{font-family:Consolas,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-secondary)}",
82
- ".clb-pop-line{padding:1px 0}",
83
- ".clb-pop-cur{background:rgba(59,130,246,.12);border-radius:4px;padding:3px 6px;margin:4px 0}",
84
- ".clb-pop-empty{color:var(--dsw-alias-label-secondary);padding:4px 0}",
51
+ ".clb-row{display:inline-flex;align-items:center;gap:6px}.clb-ind{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 9px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);font-size:12px;line-height:1;color:var(--dsw-alias-label-secondary);white-space:nowrap;user-select:none}",
52
+ ".clb-ind:hover{border-color:var(--dsw-alias-border-l2);cursor:pointer}.clb-dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto;background:var(--dsw-alias-label-secondary)}",
53
+ ".clb-ind b{font-weight:600}.clb-run .clb-dot{background:var(--dsw-static-blue-500,#3b82f6);animation:clb-pulse 1s ease-in-out infinite}",
54
+ ".clb-ok .clb-dot{background:var(--dsw-static-green-500,#22c55e)}.clb-fail .clb-dot{background:var(--dsw-alias-state-error-primary)}",
55
+ ".clb-fb .clb-dot{background:var(--dsw-alias-state-warn-primary)}.clb-run{color:var(--dsw-static-blue-500,#3b82f6);border-color:var(--dsw-static-blue-500,#3b82f6)}",
56
+ ".clb-ok{color:var(--dsw-static-green-500,#22c55e);border-color:var(--dsw-static-green-500,#22c55e)}.clb-fb{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}",
57
+ "@keyframes clb-pulse{0%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.72)}100%{opacity:1;transform:scale(1)}}.clb-pop-overlay{position:fixed;inset:0;background:rgba(0,0,0,.32);z-index:10000;display:flex;align-items:center;justify-content:center}",
58
+ ".clb-pop-panel{width:560px;max-width:92vw;max-height:76vh;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.35);font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary);overflow:hidden}.clb-pop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);font-weight:600}",
59
+ ".clb-pop-close{border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);border-radius:6px;width:22px;height:22px;line-height:1;font-size:13px;cursor:pointer}.clb-pop-close:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}",
60
+ ".clb-pop-body{overflow:auto;padding:12px 14px}.clb-pop-sec{margin-bottom:14px;padding-bottom:12px;border-bottom:1px dashed var(--dsw-alias-border-l1)}",
61
+ ".clb-pop-sec:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}.clb-pop-sech{font-weight:600;margin-bottom:6px}",
62
+ ".clb-pop-proj{margin:6px 0;padding:8px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px}.clb-pop-proj-head{font-weight:600;margin-bottom:4px}",
63
+ ".clb-pop-mono{font-family:Consolas,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-secondary)}.clb-pop-line{padding:1px 0}",
64
+ ".clb-pop-cur{background:rgba(59,130,246,.12);border-radius:4px;padding:3px 6px;margin:4px 0}.clb-pop-empty{color:var(--dsw-alias-label-secondary);padding:4px 0}",
85
65
  // ── 设置面板 ──────────────────────────────────────────────────────────
86
- ".clbs-root{display:flex;flex-direction:column;gap:16px;max-width:720px;font-size:13px;line-height:1.55;color:var(--dsw-alias-label-primary)}",
87
- ".clbs-h{font-size:14px;font-weight:600}",
88
- ".clbs-sub{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:2px}",
89
- ".clbs-card{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:12px 14px}",
90
- ".clbs-card-h{font-weight:600;font-size:13px;margin-bottom:10px}",
91
- ".clbs-tabs{display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid var(--dsw-alias-border-l1);padding-bottom:10px}",
92
- ".clbs-tab{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 12px;border-radius:13px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer;font-family:inherit}",
93
- ".clbs-tab:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}",
94
- ".clbs-tab-on{background:var(--dsw-static-blue-500,#3b82f6);border-color:var(--dsw-static-blue-500,#3b82f6);color:#fff;font-weight:600}",
95
- ".clbs-tab-dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-static-green-500,#22c55e);flex:none}",
96
- ".clbs-tab-dot-off{background:var(--dsw-alias-label-tertiary,var(--dsw-alias-border-l2))}",
97
- ".clbs-tab-rank{font-size:10px;opacity:.8}",
66
+ ".clbs-root{display:flex;flex-direction:column;gap:16px;max-width:720px;font-size:13px;line-height:1.55;color:var(--dsw-alias-label-primary)}.clbs-h{font-size:14px;font-weight:600}",
67
+ ".clbs-sub{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:2px}.clbs-card{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:12px 14px}",
68
+ ".clbs-card-h{font-weight:600;font-size:13px;margin-bottom:10px}.clbs-tabs{display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid var(--dsw-alias-border-l1);padding-bottom:10px}",
69
+ ".clbs-tab{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 12px;border-radius:13px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer;font-family:inherit}.clbs-tab:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}",
70
+ ".clbs-tab-on{background:var(--dsw-static-blue-500,#3b82f6);border-color:var(--dsw-static-blue-500,#3b82f6);color:#fff;font-weight:600}.clbs-tab-dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-static-green-500,#22c55e);flex:none}",
71
+ ".clbs-tab-dot-off{background:var(--dsw-alias-label-tertiary,var(--dsw-alias-border-l2))}.clbs-tab-rank{font-size:10px;opacity:.8}",
98
72
 
99
- ".clbs-card-off{opacity:.62}",
100
- ".clbs-card-hrow{display:flex;align-items:center;gap:8px;margin-bottom:10px}",
101
- ".clbs-card-t{font-weight:600;font-size:13px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
102
- ".clbs-chip{display:inline-flex;align-items:center;height:18px;padding:0 8px;border-radius:9px;font-size:11px;font-weight:600;white-space:nowrap}",
103
- ".clbs-chip-on{background:rgba(34,197,94,.14);color:var(--dsw-static-green-500,#22c55e)}",
104
- ".clbs-chip-off{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}",
105
- ".clbs-chip-rank{background:var(--dsw-static-blue-500,#3b82f6);color:#fff}",
106
- ".clbs-grid{display:grid;grid-template-columns:130px 1fr;gap:8px 12px;align-items:center}",
107
- ".clbs-label{color:var(--dsw-alias-label-secondary);text-align:right;font-size:12px}",
108
- ".clbs-ctl{display:flex;flex-direction:column;gap:4px;min-width:0}",
109
- ".clbs-note{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-size:11px}",
110
- ".clbs-input,.clbs-select{width:100%;box-sizing:border-box;height:28px;padding:0 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-input,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-primary);font-size:12px;font-family:inherit;outline:none}",
111
- ".clbs-input:focus,.clbs-select:focus{border-color:var(--dsw-alias-border-brand,var(--dsw-static-blue-500,#3b82f6))}",
112
- ".clbs-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer;user-select:none}",
113
- ".clbs-actions{display:flex;align-items:center;gap:10px}",
114
- ".clbs-btn{height:28px;padding:0 14px;border-radius:6px;border:1px solid var(--dsw-alias-border-brand,var(--dsw-static-blue-500,#3b82f6));background:var(--dsw-static-blue-500,#3b82f6);color:#fff;font-size:12px;cursor:pointer}",
115
- ".clbs-btn:hover{opacity:.9}",
116
- ".clbs-btn:disabled{opacity:.5;cursor:default}",
117
- ".clbs-btn-ghost{border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary)}",
118
- ".clbs-msg{font-size:12px}",
119
- ".clbs-msg-ok{color:var(--dsw-static-green-500,#22c55e)}",
120
- ".clbs-msg-err{color:var(--dsw-alias-state-error-primary)}",
121
- ".clbs-diag{margin-top:8px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden}",
122
- ".clbs-diag-h{padding:6px 10px;background:var(--dsw-alias-bg-layer-2);font-weight:600;font-size:12px;border-bottom:1px solid var(--dsw-alias-border-l1)}",
123
- ".clbs-diag-row{display:grid;grid-template-columns:150px 1fr auto;gap:8px;padding:7px 10px;border-bottom:1px dashed var(--dsw-alias-border-l1);align-items:center;font-size:12px}",
124
- ".clbs-diag-row:last-child{border-bottom:none}",
125
- ".clbs-diag-mono{font-family:Consolas,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-secondary);word-break:break-all}",
126
- ".clbs-badge{display:inline-flex;align-items:center;gap:4px;height:18px;padding:0 7px;border-radius:9px;font-size:11px;white-space:nowrap}",
127
- ".clbs-badge-ok{color:var(--dsw-static-green-500,#22c55e);border:1px solid var(--dsw-static-green-500,#22c55e)}",
128
- ".clbs-badge-warn{color:var(--dsw-alias-state-warn-primary);border:1px solid var(--dsw-alias-state-warn-primary)}",
129
- ".clbs-badge-err{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary)}",
130
- ".clbs-disp-row{display:grid;grid-template-columns:auto auto 1fr auto;gap:10px;align-items:center;padding:6px 0;border-bottom:1px dashed var(--dsw-alias-border-l1)}",
131
- ".clbs-disp-row:last-child{border-bottom:none}",
132
- ".clbs-disp-name{font-weight:600;font-size:12px}",
133
- ".clbs-disp-off .clbs-disp-name{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-weight:400}",
134
- ".clbs-disp-ord{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;border-radius:9px;background:var(--dsw-static-blue-500,#3b82f6);color:#fff;font-size:11px;font-weight:600}",
135
- ".clbs-disp-btn{width:24px;height:22px;border-radius:5px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1;cursor:pointer;padding:0}",
136
- ".clbs-disp-btn:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}",
137
- ".clbs-disp-btn:disabled{opacity:.3;cursor:default}"
73
+ ".clbs-card-off{opacity:.62}.clbs-card-hrow{display:flex;align-items:center;gap:8px;margin-bottom:10px}",
74
+ ".clbs-card-t{font-weight:600;font-size:13px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.clbs-chip{display:inline-flex;align-items:center;height:18px;padding:0 8px;border-radius:9px;font-size:11px;font-weight:600;white-space:nowrap}",
75
+ ".clbs-chip-on{background:rgba(34,197,94,.14);color:var(--dsw-static-green-500,#22c55e)}.clbs-chip-off{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}",
76
+ ".clbs-chip-rank{background:var(--dsw-static-blue-500,#3b82f6);color:#fff}.clbs-grid{display:grid;grid-template-columns:130px 1fr;gap:8px 12px;align-items:center}",
77
+ ".clbs-label{color:var(--dsw-alias-label-secondary);text-align:right;font-size:12px}.clbs-ctl{display:flex;flex-direction:column;gap:4px;min-width:0}",
78
+ ".clbs-note{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-size:11px}.clbs-input,.clbs-select{width:100%;box-sizing:border-box;height:28px;padding:0 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-input,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-primary);font-size:12px;font-family:inherit;outline:none}",
79
+ ".clbs-input:focus,.clbs-select:focus{border-color:var(--dsw-alias-border-brand,var(--dsw-static-blue-500,#3b82f6))}.clbs-check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer;user-select:none}",
80
+ ".clbs-actions{display:flex;align-items:center;gap:10px}.clbs-btn{height:28px;padding:0 14px;border-radius:6px;border:1px solid var(--dsw-alias-border-brand,var(--dsw-static-blue-500,#3b82f6));background:var(--dsw-static-blue-500,#3b82f6);color:#fff;font-size:12px;cursor:pointer}",
81
+ ".clbs-btn:hover{opacity:.9}.clbs-btn[disabled]{opacity:.6;cursor:default}",
82
+ ".clbs-refresh{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.clbs-refresh-msg{font-size:11px;color:var(--dsw-alias-label-secondary)}",
83
+ ".clbs-btn:disabled{opacity:.5;cursor:default}.clbs-btn-ghost{border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary)}",
84
+ ".clbs-msg{font-size:12px}.clbs-msg-ok{color:var(--dsw-static-green-500,#22c55e)}",
85
+ ".clbs-msg-err{color:var(--dsw-alias-state-error-primary)}.clbs-diag{margin-top:8px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden}",
86
+ ".clbs-diag-h{padding:6px 10px;background:var(--dsw-alias-bg-layer-2);font-weight:600;font-size:12px;border-bottom:1px solid var(--dsw-alias-border-l1)}.clbs-diag-row{display:grid;grid-template-columns:150px 1fr auto;gap:8px;padding:7px 10px;border-bottom:1px dashed var(--dsw-alias-border-l1);align-items:center;font-size:12px}",
87
+ ".clbs-diag-row:last-child{border-bottom:none}.clbs-diag-mono{font-family:Consolas,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-secondary);word-break:break-all}",
88
+ ".clbs-badge{display:inline-flex;align-items:center;gap:4px;height:18px;padding:0 7px;border-radius:9px;font-size:11px;white-space:nowrap}.clbs-badge-ok{color:var(--dsw-static-green-500,#22c55e);border:1px solid var(--dsw-static-green-500,#22c55e)}",
89
+ ".clbs-badge-warn{color:var(--dsw-alias-state-warn-primary);border:1px solid var(--dsw-alias-state-warn-primary)}.clbs-badge-err{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary)}",
90
+ ".clbs-disp-row{display:grid;grid-template-columns:auto auto 1fr auto;gap:10px;align-items:center;padding:6px 0;border-bottom:1px dashed var(--dsw-alias-border-l1)}.clbs-disp-row:last-child{border-bottom:none}",
91
+ ".clbs-disp-name{font-weight:600;font-size:12px}.clbs-disp-off .clbs-disp-name{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-weight:400}",
92
+ ".clbs-disp-ord{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;border-radius:9px;background:var(--dsw-static-blue-500,#3b82f6);color:#fff;font-size:11px;font-weight:600}.clbs-disp-btn{width:24px;height:22px;border-radius:5px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1;cursor:pointer;padding:0}",
93
+ ".clbs-disp-btn:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}.clbs-disp-btn:disabled{opacity:.3;cursor:default}",
138
94
  ].join("");
139
95
  if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="cli-bridge-indicator"]') === null) {
140
96
  const tag = document.createElement("style");
@@ -325,10 +281,37 @@ window.__ModuleLoader__.load({
325
281
  }
326
282
 
327
283
  // ── 设置面板(settings.section):字段由后端 providers[id].meta.fields 驱动 ──
284
+ // 「手动拉取」按钮(契约 §3.2):面板**不自动探测**(避免首屏被 spawnSync 拖慢,
285
+ // 也避免探不到时下拉空着分不清"没模型"与"没拉到")。用户点按钮才 POST 到
286
+ // /cli-bridge/settings/models,拿到 options 后只更新展示态,不落盘。
287
+ function RefreshRow(props) {
288
+ const st = react.useState(""), busy = st[0], setBusy = st[1];
289
+ const ms = react.useState(""), msg = ms[0], setMsg = ms[1];
290
+ const run = function () {
291
+ setBusy("拉取中…"); setMsg("");
292
+ fetch("/cli-bridge/settings/models", {
293
+ method: "POST", headers: { "Content-Type": "application/json" },
294
+ body: JSON.stringify({ provider: props.provider })
295
+ }).then(function (r) { return r.json(); }).then(function (j) {
296
+ setBusy("");
297
+ if (!j || !j.ok) { setMsg("✗ " + ((j && j.error) || "拉取失败")); return; }
298
+ const list = Array.isArray(j.options) ? j.options : [];
299
+ if (!list.length) { setMsg("✗ 端点未返回任何可用项"); return; }
300
+ props.onOptions(list);
301
+ setMsg("✓ 已获取 " + list.length + " 项" + (j.source ? "(来源:" + j.source + ")" : ""));
302
+ }).catch(function (e) { setBusy(""); setMsg("✗ " + String((e && e.message) || e)); });
303
+ };
304
+ return react.createElement("div", { className: "clbs-refresh" },
305
+ react.createElement("button", { type: "button", className: "clbs-btn", disabled: !!busy, onClick: run },
306
+ busy || (props.label || "拉取可用项")),
307
+ msg ? react.createElement("span", { className: "clbs-refresh-msg" }, msg) : null);
308
+ }
328
309
  function Field(props) {
329
310
  const f = props.field, val = props.value, onChange = props.onChange, secretState = props.secret;
311
+ // 手动拉取的展示态:拿到后只用它渲染下拉,用户选中仍走正常 onChange→POST 保存。
312
+ const fs2 = react.useState(null); const fetched = fs2[0]; const setFetched = fs2[1];
330
313
  if (f.type === "select") {
331
- const opts = Array.isArray(f.options) ? f.options : [];
314
+ const opts = fetched || (Array.isArray(f.options) ? f.options : []);
332
315
  const isKnown = opts.some(function (o) { return String(o.value) === String(val); });
333
316
  const CUSTOM = "__clb_custom__";
334
317
  // wantCustom:用户显式选了「自定义…」后保持输入框可见(即使此刻值为空)。
@@ -346,6 +329,7 @@ window.__ModuleLoader__.load({
346
329
  opts.map(function (o) { return react.createElement("option", { key: o.value, value: o.value }, o.label || o.value); }),
347
330
  f.allowCustom ? react.createElement("option", { key: "__custom", value: CUSTOM }, "自定义…(手动输入)") : null),
348
331
  showCustom ? react.createElement("input", { className: "clbs-input", value: val, onChange: function (e) { onChange(e.target.value); }, placeholder: "手动输入值", spellCheck: false }) : null,
332
+ f.refresh ? react.createElement(RefreshRow, { provider: props.providerId, label: f.refresh.label, onOptions: setFetched }) : null,
349
333
  f.note ? react.createElement("div", { className: "clbs-note" }, f.note) : null);
350
334
  }
351
335
  if (f.secret) {
@@ -418,7 +402,7 @@ window.__ModuleLoader__.load({
418
402
  return react.createElement(react.Fragment, { key: f.key },
419
403
  react.createElement("label", { className: "clbs-label" }, f.label || f.key),
420
404
  react.createElement(Field, {
421
- field: f, value: form[f.key] || "", secret: view.secrets && view.secrets[f.key], wantClear: !!(clears[meta.id] && clears[meta.id][f.key]),
405
+ field: f, providerId: meta.id, value: form[f.key] || "", secret: view.secrets && view.secrets[f.key], wantClear: !!(clears[meta.id] && clears[meta.id][f.key]),
422
406
  onChange: function (v) { setField(meta.id, f.key, v); }, onClear: function (b) { setClear(meta.id, f.key, b); }
423
407
  }));
424
408
  })) : react.createElement("div", { className: "clbs-note" }, "(无可配置项)"),
@@ -403,6 +403,54 @@ export function apply(ctx) {
403
403
  }).catch((e) => jsonRes(res, 400, { error: String(e && e.message || e) }))
404
404
  }
405
405
  }, 'cli-bridge: settings route'))
406
+ // 手动拉取可用项(契约 §3.2):面板**不自动探测**,用户点按钮才走到这里。
407
+ // 业务失败一律 HTTP 200 + { ok:false, error },不用 4xx/5xx —— 否则前端容易把
408
+ // 「没拉到」误当网络故障静默吞掉(本项目模型清单反复踩的坑)。
409
+ webCtx.effect(() => ws.register({
410
+ kind: 'exact', path: '/cli-bridge/settings/models',
411
+ handler: (req, res) => {
412
+ const method = String((req && req.method) || 'GET').toUpperCase()
413
+ if (method !== 'POST') { jsonRes(res, 405, { error: 'method not allowed' }); return }
414
+ readBody(req, 16 * 1024).then(async (text) => {
415
+ let provider = ''
416
+ try { provider = String((JSON.parse(text || '{}') || {}).provider || '') } catch (e) {
417
+ jsonRes(res, 200, { ok: false, provider: '', error: 'request body is not valid JSON' }); return
418
+ }
419
+ if (!provider || PROVIDER_IDS.indexOf(provider) < 0) {
420
+ jsonRes(res, 200, { ok: false, provider: provider, error: 'unknown provider ' + JSON.stringify(provider) + '; expected one of ' + PROVIDER_IDS.join(', ') })
421
+ return
422
+ }
423
+ let d = null
424
+ try { d = createProvider(provider, { env: process.env, process }) } catch (e) {
425
+ jsonRes(res, 200, { ok: false, provider: provider, error: 'provider load failed: ' + String(e && e.message || e) }); return
426
+ }
427
+ const fn = d && d.settings && d.settings.refreshModels
428
+ if (typeof fn !== 'function') {
429
+ jsonRes(res, 200, { ok: false, provider: provider, error: 'provider "' + provider + '" does not implement settings.refreshModels()' }); return
430
+ }
431
+ // 生效设置是扁平结构:snap[providerId] = 该 provider 的分区(见 settings.mjs normalizeAll)。
432
+ const snap = effectiveSettings({ env: process.env })
433
+ const sec = (snap && snap[provider]) || {}
434
+ try {
435
+ const r = (await fn(sec, process.env)) || {}
436
+ if (!r.ok || !Array.isArray(r.ids) || !r.ids.length) {
437
+ jsonRes(res, 200, {
438
+ ok: false, provider: provider, source: r.source || null,
439
+ error: r.error || 'provider returned no items'
440
+ })
441
+ return
442
+ }
443
+ const labels = (r.labels && typeof r.labels === 'object') ? r.labels : {}
444
+ jsonRes(res, 200, {
445
+ ok: true, provider: provider, source: r.source || null,
446
+ options: r.ids.map((id) => ({ value: String(id), label: String(labels[id] || id) }))
447
+ })
448
+ } catch (e) {
449
+ jsonRes(res, 200, { ok: false, provider: provider, error: 'refreshModels threw: ' + String(e && e.message || e) })
450
+ }
451
+ }).catch((e) => jsonRes(res, 200, { ok: false, provider: '', error: String(e && e.message || e) }))
452
+ }
453
+ }, 'cli-bridge: settings models route'))
406
454
  // agy 额度(可选增强,脚本缺失返回 { ok:false })
407
455
  webCtx.effect(() => ws.register({
408
456
  kind: 'exact', path: '/agy-indicator/quota',
@@ -19,13 +19,12 @@ import { dirname, join } from 'node:path'
19
19
  import { existsSync } from 'node:fs'
20
20
  import { fileURLToPath } from 'node:url'
21
21
  import { resolveScheduling } from '../../../core/scheduling.mjs'
22
- import { PROVIDER_IDS } from '../../../providers/registry.mjs'
22
+ import { PROVIDER_IDS, PRESET_DIRS } from '../../../providers/registry.mjs'
23
23
 
24
24
  export const PKG_NAME = '@luchenglong/cli-bridge'
25
25
 
26
- // preset 目录名映射(与 scripts/build.mjs 的 PRESET_DIRS 一致;目录名带 -first 后缀是
26
+ // preset 目录名映射:单一来源见 providers/registry.mjs 的 PRESET_DIRS(勿在此重复硬编码)。
27
27
  // 三个旧包时代的遗留布局,provider id 不带 —— 这里不映射就永远找不到 plugin-list)。
28
- const PRESET_DIRS = { agy: 'agy-first', codebuddy: 'codebuddy-first', mimo: 'mimo-first', opencode: 'opencode-first' }
29
28
 
30
29
  // 【成员预设】0.2.1 起 dsh-tool-subagent 的 config 不再接受 persona/toolFilter(apply()
31
30
  // 里显式 throw),角色人格只能作为独立 Agent Preset 存在、由委派行经 agentPreset 选择。
@@ -29,12 +29,14 @@ import * as agyMcp from './providers/agy.mjs'
29
29
  import * as codebuddyMcp from './providers/codebuddy.mjs'
30
30
  import * as mimoMcp from './providers/mimo.mjs'
31
31
  import * as opencodeMcp from './providers/opencode.mjs'
32
+ import * as claudeMcp from './providers/claude.mjs'
33
+ import * as codexMcp from './providers/codex.mjs'
32
34
 
33
35
  const SERVER_NAME = 'cli-bridge-mcp'
34
- const SERVER_VERSION = '0.2.8'
36
+ const SERVER_VERSION = '0.2.10'
35
37
  const PROTOCOL = '2024-11-05'
36
38
 
37
- const PROVIDERS = [agyMcp, codebuddyMcp, mimoMcp, opencodeMcp]
39
+ const PROVIDERS = [agyMcp, codebuddyMcp, mimoMcp, opencodeMcp, claudeMcp, codexMcp]
38
40
 
39
41
  function detectHomeDir() {
40
42
  if (process.env.DSH_HOME) return process.env.DSH_HOME
@@ -0,0 +1,231 @@
1
+ // mcp/providers/claude.mjs — claude MCP 工具面(契约 §4)。
2
+ // framing/生命周期归 keystone 统一服务器 mcp/cli-bridge-mcp.mjs。
3
+ //
4
+ // 与 opencode 面结构同构:直接 spawn claude(原生 exe 直接用;npm 的 .cmd 经 shell:true,
5
+ // Windows 限制;.js/.mjs 目标经 process.execPath 包装,供测试 fixture 使用)。
6
+ // 无 ctx.emit:状态走快照文件 {homeDir}/cli-bridge-mcp-claude.json(bridge-kit
7
+ // buildMcpBridgePayload + 原子写 tmp→rename;写失败静默,绝不影响工具结果)。
8
+ //
9
+ // 工具名/schema 从描述符取(单一来源,防两处漂移),描述与 background 语义按 MCP 面重写:
10
+ // MCP 没有 DSH jobs,background 参数在该形态无意义,故 schema 里裁掉。
11
+ import { spawn as nodeSpawn } from 'node:child_process'
12
+ import { readFileSync } from 'node:fs'
13
+ import { resolve as resolvePath, dirname } from 'node:path'
14
+
15
+ import defineProvider, { resolveExePrefix } from '../../providers/claude.mjs'
16
+ import { splitExeSpec, quoteShellArg } from '../../providers/claude-exe.mjs'
17
+ import { createStatusEngine, createLineStream, buildMcpBridgePayload, isLimited, renderStatus } from '../../core/bridge-kit.mjs'
18
+ import { packageVersion } from '../version.mjs'
19
+
20
+ export const meta = { id: 'claude', label: 'CLAUDE', version: packageVersion() }
21
+
22
+ const SETTINGS_FILE_NAME = 'cli-bridge-settings.json'
23
+
24
+ // 统一设置文件分区读(MCP 子进程无 ctx.get,只读 env/文件)—— 设置面板里的
25
+ // binPath/defaultModel 在 MCP 形态同样生效(与 preset 行为一致)。
26
+ function resolveSettingsFile(env) {
27
+ try {
28
+ if (env.CLI_BRIDGE_SETTINGS_FILE) return env.CLI_BRIDGE_SETTINGS_FILE
29
+ if (env.DSH_HOME) return resolvePath(env.DSH_HOME, SETTINGS_FILE_NAME)
30
+ if (process.platform === 'win32' && env.APPDATA) return resolvePath(env.APPDATA, 'DSH Desktop', 'dsh-home', SETTINGS_FILE_NAME)
31
+ if (env.HOME) return resolvePath(env.HOME, '.dsh', SETTINGS_FILE_NAME)
32
+ } catch (e) { }
33
+ return null
34
+ }
35
+ function readPartition(env) {
36
+ const p = resolveSettingsFile(env)
37
+ if (!p) return { partition: null, path: null, home: null }
38
+ try {
39
+ const all = JSON.parse(readFileSync(p, 'utf8'))
40
+ return { partition: (all && typeof all === 'object' && all.claude) ? all.claude : null, path: p, home: dirname(p) }
41
+ } catch (e) {
42
+ return { partition: null, path: p, home: dirname(p) }
43
+ }
44
+ }
45
+
46
+ const provider = defineProvider({
47
+ env: process.env,
48
+ process,
49
+ nodeChild: { spawn: nodeSpawn },
50
+ readSettings: () => readPartition(process.env)
51
+ })
52
+
53
+ let lastIo = null
54
+ function publishSnapshot(snapshot) {
55
+ const io = lastIo
56
+ if (!io) return
57
+ try {
58
+ const target = io.path.join(io.homeDir, 'cli-bridge-mcp-claude.json')
59
+ const payload = buildMcpBridgePayload(snapshot, process.pid, Date.now())
60
+ const tmp = target + '.' + process.pid + '.tmp'
61
+ try { io.fs.mkdirSync(io.path.dirname(target), { recursive: true }) } catch (e) {}
62
+ io.fs.writeFileSync(tmp, JSON.stringify(payload), 'utf8')
63
+ io.fs.renameSync(tmp, target)
64
+ } catch (e) { /* 灯数据尽力而为,绝不影响工具调用 */ }
65
+ }
66
+ const engine = createStatusEngine({ provider, publish: publishSnapshot })
67
+ provider.engine = engine
68
+
69
+ function exePrefix() { return resolveExePrefix(process.env, '') }
70
+ function resolveCwd(a) {
71
+ if (a && a.cwd) return resolvePath(String(a.cwd))
72
+ return process.env.CLAUDE_MCP_CWD || process.cwd()
73
+ }
74
+
75
+ // ── spawn(实时折叠走 bridge-kit 行流;.cmd 走 shell)─────────────────────────
76
+ function runClaude(args) {
77
+ return new Promise((resolve) => {
78
+ const built = provider.buildArgv(exePrefix(), args, { planActive: false, defaultMode: 'auto' })
79
+ const cwd = resolveCwd(args)
80
+ const cmd = built.argv[0]
81
+ const argv = built.argv.slice(1)
82
+ const isShell = /\.(cmd|bat)$/i.test(cmd)
83
+ const spawnCmd = isShell ? [cmd].concat(argv).map(quoteShellArg).join(' ') : cmd
84
+ let child
85
+ try {
86
+ child = nodeSpawn(spawnCmd, isShell ? [] : argv, { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: isShell })
87
+ } catch (e) {
88
+ resolve({ ok: false, status: 'SPAWN_ERROR', response: '', sessionId: null, durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode: built.mode, backend: null, stderr: String(e && e.message || e) })
89
+ return
90
+ }
91
+ engine.begin(cwd)
92
+ let out = ''
93
+ let err = ''
94
+ let killed = false
95
+ const timer = setTimeout(() => { killed = true; try { child.kill() } catch (e) {} }, (built.timeoutSec + 60) * 1000)
96
+ const stream = createLineStream((ln) => {
97
+ const t = ln.trim()
98
+ if (!t.startsWith('{')) return
99
+ try { engine.foldEvent(JSON.parse(t), cwd) } catch (e) {}
100
+ })
101
+ child.stdout.on('data', (d) => {
102
+ out += d
103
+ if (out.length > 4000000) out = out.slice(-2000000)
104
+ stream.pushChunk(String(d))
105
+ })
106
+ child.stderr.on('data', (d) => { err += d; if (err.length > 1000000) err = err.slice(-500000) })
107
+ child.on('error', (e) => {
108
+ clearTimeout(timer)
109
+ stream.flush()
110
+ const res = { ok: false, status: 'CLAUDE_UNAVAILABLE', response: '', sessionId: null, durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode: built.mode, backend: null, stderr: 'claude spawn failed: ' + String(e && e.message || e) }
111
+ engine.end(res, cwd)
112
+ resolve(res)
113
+ })
114
+ child.on('close', (code) => {
115
+ clearTimeout(timer)
116
+ stream.flush()
117
+ const outcome = { exitCode: killed ? 124 : code }
118
+ let res = provider.buildResult(provider.parseJson(out), outcome, built.mode, err, out, null)
119
+ if (killed && !res.ok) {
120
+ res.status = 'HUNG_TIMEOUT'
121
+ res.stderr = (res.stderr ? res.stderr + ' ' : '') + '[killed by DSH timeout guard after ' + built.timeoutSec + 's; if this was a long-running task raise timeoutSec]'
122
+ }
123
+ engine.end(res, cwd)
124
+ resolve(res)
125
+ })
126
+ })
127
+ }
128
+
129
+ function textResult(res) {
130
+ const head = 'claude ' + (res.ok ? 'OK' : 'FAILED') + ' [status=' + res.status +
131
+ (res.sessionId ? ' session=' + res.sessionId : '') +
132
+ (res.modelUsed ? ' model=' + res.modelUsed : '') +
133
+ (res.durationSeconds != null ? ' ' + res.durationSeconds + 's' : '') + ']'
134
+ let note = ''
135
+ if (!res.ok && isLimited(res)) note = '\n\n[Note: this looks like a rate-limit / network failure. Do NOT retry claude in a loop; finish the task with your own tools, or ask the user.]'
136
+ const hint = provider.failureHint(res)
137
+ const body = res.response || (res.stderr ? '[stderr] ' + res.stderr : (res.rawStdout ? '[raw] ' + res.rawStdout : ''))
138
+ return head + (body ? '\n\n' + body : '') + (hint ? '\n\n' + hint : '') + note
139
+ }
140
+
141
+ // ── 工具面 ────────────────────────────────────────────────────────────────────
142
+ function mcpSchema(name) {
143
+ const src = provider.tools[name].inputSchema
144
+ const clone = JSON.parse(JSON.stringify(src))
145
+ if (clone.properties) delete clone.properties.background
146
+ return clone
147
+ }
148
+
149
+ export function tools(env) {
150
+ const d = provider.tools
151
+ return [
152
+ {
153
+ name: d.run.name,
154
+ description: 'Dispatch a coding/build/debug/investigation task to the local Claude Code CLI and return its final answer. claude runs fully non-interactively (permissions approved, never blocks on a prompt) and streams live steps. Prefer it for implementation, multi-file edits, refactors and debugging; use your own tools for quick read-only lookups and final build/test verification. While it runs, call claude_status to watch what claude is doing live.',
155
+ inputSchema: mcpSchema('run')
156
+ },
157
+ {
158
+ name: d.continue.name,
159
+ description: 'Continue an existing claude session with a follow-up prompt, reusing claude context (passes --resume <sessionId>, or --continue with latest=true).',
160
+ inputSchema: mcpSchema('continue')
161
+ },
162
+ {
163
+ name: d.status.name,
164
+ description: 'Read a live snapshot of what the local claude agent is currently doing. Returns one section per project (working directory): running count, the current step (tool name + arguments being executed, or agent_response thinking/typing), the recent step trail, and the last completed run status + session id. Optional cwd filters to a single project.',
165
+ inputSchema: JSON.parse(JSON.stringify(d.status.inputSchema))
166
+ },
167
+ {
168
+ name: 'claude_models',
169
+ description: 'List the models the current claude account/endpoint really supports, read LIVE from the server (probes it once with a deliberately invalid model id and parses the "The supported API model names are …" answer). Use it to pick a model before claude_run. Returns ok:false with an actionable error when no live roster can be read — it never invents a list and never presents ANTHROPIC_*_MODEL aliases as the usable set.',
170
+ inputSchema: (function () {
171
+ const et = (provider.extraTools || [])[0]
172
+ return JSON.parse(JSON.stringify(et ? et.inputSchema : { type: 'object', properties: {} }))
173
+ })()
174
+ }
175
+ ]
176
+ }
177
+
178
+ export async function call(name, args, io) {
179
+ if (io) lastIo = io
180
+ if (!lastIo) return 'claude error: MCP io not initialized'
181
+ const a = args || {}
182
+ if (name === 'claude_status') {
183
+ const snap = engine.statusSnapshot()
184
+ if (a.cwd) {
185
+ snap.projects = snap.projects.filter((p) => p.cwd === String(a.cwd))
186
+ const g = snap.projects[0]
187
+ if (g) Object.assign(snap, { state: g.state, running: g.running, current: g.current, trail: g.trail, lastStatus: g.lastStatus, lastAt: g.lastAt, lastSessionId: g.lastSessionId, lastBackend: g.lastBackend, fallbackActive: g.fallbackActive, updatedAt: g.updatedAt })
188
+ }
189
+ return renderStatus(snap, provider)[0].text
190
+ }
191
+ if (name === 'claude_models') {
192
+ const et = (provider.extraTools || [])[0]
193
+ const res = await provider.listModels(a)
194
+ if (et && typeof et.render === 'function') return et.render(a, res)[0].text
195
+ return res.ok ? 'claude models: ' + res.models.map((m) => m.id).join(', ') : 'claude_models failed: ' + String(res.error || 'unknown')
196
+ }
197
+ if (!a.prompt || !String(a.prompt).trim()) return 'claude error: prompt is required'
198
+ const mapped = Object.assign({}, a)
199
+ if (name === 'claude_continue') {
200
+ if (!mapped.sessionId && mapped.latest) mapped.continueLatest = true
201
+ }
202
+ const res = await runClaude(mapped)
203
+ return textResult(res)
204
+ }
205
+
206
+ export function check(env) {
207
+ const ts = tools(env)
208
+ const names = ts.map((t) => t.name)
209
+ for (const req of ['claude_run', 'claude_continue', 'claude_status', 'claude_models']) {
210
+ if (names.indexOf(req) < 0) throw new Error('claude mcp: missing tool ' + req)
211
+ }
212
+ for (const t of ts) {
213
+ if (!t.name || !t.description || !t.inputSchema || t.inputSchema.type !== 'object') throw new Error('claude mcp: bad tool def ' + JSON.stringify(t && t.name))
214
+ }
215
+ // MCP 面必须覆盖 descriptor 三件套(名字同源,提示词连续性红线)。
216
+ for (const k of ['run', 'continue', 'status']) {
217
+ if (names.indexOf(provider.tools[k].name) < 0) throw new Error('claude mcp: tools surface missing descriptor name ' + provider.tools[k].name)
218
+ }
219
+ // background 在 MCP 形态无 jobs 语义 → 必须裁掉(防止智能体误以为可后台派发丢结果)。
220
+ if (ts[0].inputSchema.properties && ts[0].inputSchema.properties.background) throw new Error('claude mcp: run schema must not advertise background')
221
+ // argv 契约漂移锁(实测 Claude Code 2.0.76 的 flag 集)。
222
+ const b = provider.buildArgv(['claude'], { prompt: 'x' }, { planActive: false })
223
+ for (const f of ['-p', 'x', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']) {
224
+ if (b.argv.indexOf(f) < 0) throw new Error('claude mcp: argv contract drift: ' + f)
225
+ }
226
+ const b2 = provider.buildArgv(['claude'], { prompt: 'x', mode: 'plan' }, {})
227
+ if (b2.argv.indexOf('--permission-mode') < 0 || b2.argv.indexOf('plan') < 0) throw new Error('claude mcp: plan mode must pass --permission-mode plan')
228
+ const b3 = provider.buildArgv(['claude'], { prompt: 'x', sessionId: 'sid-1' }, {})
229
+ if (b3.argv.indexOf('--resume') < 0 || b3.argv.indexOf('sid-1') < 0) throw new Error('claude mcp: resume must pass --resume <id>')
230
+ return true
231
+ }