@luchenglong/cli-bridge 0.0.0-stage → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/MCP-POLICY.md +53 -0
  4. package/PROVIDER-CONTRACT.md +235 -0
  5. package/README.en.md +98 -0
  6. package/README.md +151 -2
  7. package/assets/indicator-states.svg +62 -0
  8. package/bin/agy-quota.mjs +223 -0
  9. package/core/bridge-kit.mjs +549 -0
  10. package/core/scheduling.mjs +149 -0
  11. package/docs/ARCHITECTURE.md +117 -0
  12. package/docs/FALLBACK-AND-INDICATOR.md +93 -0
  13. package/docs/HANDOVER.md +226 -0
  14. package/docs/INSTALL.md +118 -0
  15. package/docs/MCP-POLICY.zh.md +49 -0
  16. package/docs/MIGRATION.md +43 -0
  17. package/docs/RELEASE-SOP.md +80 -0
  18. package/docs/history/agy/CHANGELOG.md +326 -0
  19. package/docs/history/codebuddy/CHANGELOG.md +668 -0
  20. package/docs/history/mimo/CHANGELOG.md +346 -0
  21. package/dynamic/agy.host.js +1237 -0
  22. package/dynamic/agy.host.template.mjs +140 -0
  23. package/dynamic/codebuddy.host.js +1787 -0
  24. package/dynamic/codebuddy.host.template.mjs +162 -0
  25. package/dynamic/mimo.host.js +1732 -0
  26. package/dynamic/mimo.host.template.mjs +169 -0
  27. package/dynamic/opencode.host.js +1443 -0
  28. package/dynamic/opencode.host.template.mjs +125 -0
  29. package/home-plugin/cli-bridge-indicator/cordis.patch.yml +31 -0
  30. package/home-plugin/cli-bridge-indicator/lib/client.js +606 -0
  31. package/home-plugin/cli-bridge-indicator/lib/index.mjs +420 -0
  32. package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +137 -0
  33. package/home-plugin/cli-bridge-indicator/lib/settings.mjs +300 -0
  34. package/mcp/agy-mcp-server.mjs +6 -0
  35. package/mcp/cli-bridge-mcp.mjs +185 -0
  36. package/mcp/codebuddy-mcp-server.mjs +6 -0
  37. package/mcp/mimo-mcp-server.mjs +6 -0
  38. package/mcp/providers/agy.mjs +254 -0
  39. package/mcp/providers/codebuddy.mjs +397 -0
  40. package/mcp/providers/mimo.mjs +290 -0
  41. package/mcp/providers/opencode.mjs +236 -0
  42. package/package.json +89 -4
  43. package/preset/agy-first/agent.cordis.yml +141 -0
  44. package/preset/agy-first/cli-bridge.mjs +169 -0
  45. package/preset/agy-first/plugin-list.mjs +26 -0
  46. package/preset/agy-first/preset.yml +3 -0
  47. package/preset/codebuddy-first/agent.cordis.yml +141 -0
  48. package/preset/codebuddy-first/cli-bridge.mjs +188 -0
  49. package/preset/codebuddy-first/plugin-list.mjs +20 -0
  50. package/preset/codebuddy-first/preset.yml +3 -0
  51. package/preset/mimo-first/agent.cordis.yml +143 -0
  52. package/preset/mimo-first/cli-bridge.mjs +236 -0
  53. package/preset/mimo-first/plugin-list.mjs +39 -0
  54. package/preset/mimo-first/preset.yml +3 -0
  55. package/preset/opencode-first/agent.cordis.yml +141 -0
  56. package/preset/opencode-first/cli-bridge.mjs +147 -0
  57. package/preset/opencode-first/plugin-list.mjs +26 -0
  58. package/preset/opencode-first/preset.yml +3 -0
  59. package/preset/standard-base.mjs +102 -0
  60. package/providers/agy.mjs +398 -0
  61. package/providers/codebuddy.mjs +926 -0
  62. package/providers/mimo.mjs +864 -0
  63. package/providers/opencode.mjs +619 -0
  64. package/providers/registry.mjs +39 -0
  65. package/scripts/build.mjs +235 -0
  66. package/scripts/deploy.mjs +61 -0
  67. package/scripts/verify.mjs +148 -0
@@ -0,0 +1,254 @@
1
+ // mcp/providers/agy.mjs — agy MCP 工具面(契约 §4;移植自旧 agy-mcp-server.mjs,
2
+ // framing/生命周期归 keystone 统一服务器 mcp/cli-bridge-mcp.mjs)。
3
+ //
4
+ // 旧语义保持:
5
+ // - 直接 spawn agy(.exe)(win32 用 agy.exe;AGY_BIN 覆盖;.js/.mjs 目标经
6
+ // process.execPath 包装,供测试 fixture 使用)。
7
+ // - cwd 兜底 = env AGY_MCP_CWD || process.cwd()。旧仓硬编码
8
+ // 'C:\\Users\\lcl\\Desktop\\DSH',机器特定,新实现按定案改 process.cwd() 兜底。
9
+ // (注:AGY_MCP_ALLOWED_ROOTS 白名单是旧 codebuddy MCP 的特性,旧 agy MCP 没有,
10
+ // 不自造语义 —— Lead 定案 1。)
11
+ // - 5h 额度硬阻断(30 分钟缓存)照旧:Gemini 5h <10% → 不调用 agy。
12
+ // - 无 ctx.emit:状态走快照文件 {homeDir}/cli-bridge-mcp-agy.json
13
+ // (bridge-kit buildMcpBridgePayload + 原子写 tmp→rename;写失败静默)。
14
+ import { spawn as nodeSpawn } from 'node:child_process'
15
+ import { existsSync } from 'node:fs'
16
+ import { resolve as resolvePath } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ import defineProvider from '../../providers/agy.mjs'
20
+ import { createStatusEngine, createLineStream, buildMcpBridgePayload, isLimited, renderStatus } from '../../core/bridge-kit.mjs'
21
+
22
+ export const meta = { id: 'agy', label: 'AGY', version: '0.2.2' }
23
+
24
+ const QUOTA_SCRIPT_DEFAULT = fileURLToPath(new URL('../../bin/agy-quota.mjs', import.meta.url))
25
+
26
+ const provider = defineProvider({
27
+ env: process.env,
28
+ process,
29
+ nodeChild: { spawn: nodeSpawn },
30
+ quotaScript: existsSync(QUOTA_SCRIPT_DEFAULT) ? QUOTA_SCRIPT_DEFAULT : null
31
+ })
32
+
33
+ let lastIo = null
34
+ function publishSnapshot(snapshot) {
35
+ const io = lastIo
36
+ if (!io) return
37
+ try {
38
+ const target = io.path.join(io.homeDir, 'cli-bridge-mcp-agy.json')
39
+ const payload = buildMcpBridgePayload(snapshot, process.pid, Date.now())
40
+ const tmp = target + '.' + process.pid + '.tmp'
41
+ try { io.fs.mkdirSync(io.path.dirname(target), { recursive: true }) } catch (e) {}
42
+ io.fs.writeFileSync(tmp, JSON.stringify(payload), 'utf8')
43
+ io.fs.renameSync(tmp, target)
44
+ } catch (e) { /* 灯数据尽力而为,绝不影响工具调用 */ }
45
+ }
46
+ // provider.engine 在 attach 后才存在;MCP 形态不 attach,用本模块的 engine。
47
+ const engine = createStatusEngine({ provider, publish: publishSnapshot })
48
+ provider.engine = engine
49
+
50
+ // ── exe / cwd 解析(旧 MCP verbatim + AGY_BIN)────────────────────────────────
51
+ function exePrefix() {
52
+ const bin = process.env.AGY_BIN || process.env.AGY_CLI || ''
53
+ if (bin) {
54
+ if (/\.(mjs|cjs|js)$/i.test(bin)) return [process.execPath, bin]
55
+ return [bin]
56
+ }
57
+ return [process.platform === 'win32' ? 'agy.exe' : 'agy']
58
+ }
59
+ function resolveCwd(a) {
60
+ if (a && a.cwd) return resolvePath(String(a.cwd))
61
+ return process.env.AGY_MCP_CWD || process.cwd()
62
+ }
63
+
64
+ // ── spawn(旧 runAgy verbatim 语义;实时折叠走 bridge-kit 行流)────────────────
65
+ function runAgy(args) {
66
+ return new Promise((resolve) => {
67
+ const built = provider.buildArgv(exePrefix(), args, { planActive: false, defaultMode: 'auto' })
68
+ const cwd = resolveCwd(args)
69
+ let child
70
+ try {
71
+ child = nodeSpawn(built.argv[0], built.argv.slice(1), { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
72
+ } catch (e) {
73
+ resolve({ ok: false, status: 'SPAWN_ERROR', response: '', conversationId: null, sessionId: null, durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode: built.mode, backend: null, stderr: String(e && e.message || e) })
74
+ return
75
+ }
76
+ engine.begin(cwd)
77
+ let out = ''
78
+ let err = ''
79
+ let killed = false
80
+ const timer = setTimeout(() => { killed = true; try { child.kill() } catch (e) {} }, (built.timeoutSec + 60) * 1000)
81
+ const stream = createLineStream((ln) => {
82
+ const t = ln.trim()
83
+ if (!t.startsWith('{')) return
84
+ try { engine.foldEvent(JSON.parse(t), cwd) } catch (e) {}
85
+ })
86
+ child.stdout.on('data', (d) => {
87
+ out += d
88
+ if (out.length > 4000000) out = out.slice(-2000000)
89
+ stream.pushChunk(String(d))
90
+ })
91
+ child.stderr.on('data', (d) => { err += d; if (err.length > 1000000) err = err.slice(-500000) })
92
+ child.on('error', (e) => {
93
+ clearTimeout(timer)
94
+ stream.flush()
95
+ const res = { ok: false, status: 'AGY_UNAVAILABLE', response: '', conversationId: null, sessionId: null, durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode: built.mode, backend: null, stderr: 'agy spawn failed: ' + String(e && e.message || e) }
96
+ engine.end(res, cwd)
97
+ resolve(res)
98
+ })
99
+ child.on('close', (code) => {
100
+ clearTimeout(timer)
101
+ stream.flush()
102
+ const outcome = { exitCode: killed ? 124 : code }
103
+ let res = provider.buildResult(provider.parseJson(out), outcome, built.mode, err, out, null)
104
+ if (killed && !res.ok) {
105
+ res.status = 'HUNG_TIMEOUT'
106
+ res.stderr = (res.stderr ? res.stderr + ' ' : '') + '[killed by DSH timeout guard after ' + built.timeoutSec + 's; if this was a long-running script (build/test) raise timeoutSec]'
107
+ }
108
+ engine.end(res, cwd)
109
+ resolve(res)
110
+ })
111
+ })
112
+ }
113
+
114
+ // 旧 textResult verbatim(conversationId/sessionId 双写已在 buildResult)。
115
+ function textResult(res) {
116
+ if (res.fallback) {
117
+ return 'agy unavailable (' + res.reason + '). Use native tools / the local model for this task; do not retry agy.'
118
+ }
119
+ const limited = !res.ok && isLimited(res)
120
+ const head = 'agy ' + (res.ok ? 'OK' : 'FAILED') + ' [status=' + res.status + ' mode=' + res.mode +
121
+ (res.conversationId ? ' conv=' + res.conversationId : '') +
122
+ (res.durationSeconds != null ? ' ' + res.durationSeconds + 's' : '') + ']'
123
+ let note = ''
124
+ if (limited) note = '\n\n[Note: this looks like a rate-limit / network failure. Do NOT retry agy in a loop; finish the task with your own tools, or ask the user.]'
125
+ const body = res.response || (res.stderr ? '[stderr] ' + res.stderr : (res.rawStdout ? '[raw] ' + res.rawStdout : ''))
126
+ return head + (body ? '\n\n' + body : '') + note
127
+ }
128
+
129
+ // ── 工具面(旧 MCP TOOLS verbatim:run/continue 无 background,+agy_quota)────
130
+ const TOOLS = [
131
+ {
132
+ name: 'agy_run',
133
+ description: 'Dispatch a coding/build/debug/investigation task to the local agy agent CLI and return its final answer. agy runs fully non-interactively (permissions auto-approved, never prompts). Prefer it for implementation, multi-file edits, refactors and debugging; use your own tools for quick read-only lookups and final build/test verification. mode=plan runs agy read-only; default accept-edits applies edits directly. While it runs, call agy_status to watch what agy is doing live.',
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ prompt: { type: 'string', description: 'The task instruction for agy. On first dispatch for a topic, include a compact CONTEXT preamble (goal, repo root, key paths, decisions/constraints; point at paths, do not paste file contents — agy reads the repo itself). For follow-ups on the same topic, use agy_continue with latest=true or conversationId (agy keeps conversation context; state only what is new).' },
138
+ mode: { type: 'string', enum: ['plan', 'accept-edits'], description: 'plan = no writes; accept-edits = allow edits (default).' },
139
+ model: { type: 'string', description: 'Optional agy model id — pick from the Gemini pool or utility models per the model-selection policy (use agy_quota to see the recommended:true list). Do NOT pass a Claude/GPT (3p) model.' },
140
+ effort: { type: 'string', enum: ['low', 'medium', 'high'] },
141
+ cwd: { type: 'string', description: 'Working directory for agy (default: AGY_MCP_CWD or the DSH workspace).' },
142
+ addDirs: { type: 'array', items: { type: 'string' } },
143
+ timeoutSec: { type: 'integer', description: '10-3600, default 600.' }
144
+ },
145
+ required: ['prompt']
146
+ }
147
+ },
148
+ {
149
+ name: 'agy_continue',
150
+ description: 'Continue an existing agy conversation with a follow-up prompt, reusing agy context. Pass conversationId from a prior agy_run result, or latest=true for the most recent conversation.',
151
+ inputSchema: {
152
+ type: 'object',
153
+ properties: {
154
+ prompt: { type: 'string', description: 'Follow-up instruction for the ongoing agy conversation. State only what is new; agy preserves its conversation context.' },
155
+ conversationId: { type: 'string' },
156
+ latest: { type: 'boolean', description: 'Continue the most recent agy conversation.' },
157
+ mode: { type: 'string', enum: ['plan', 'accept-edits'] },
158
+ model: { type: 'string' },
159
+ effort: { type: 'string', enum: ['low', 'medium', 'high'] },
160
+ cwd: { type: 'string' },
161
+ timeoutSec: { type: 'integer', description: '10-3600, default 600.' }
162
+ },
163
+ required: ['prompt']
164
+ }
165
+ },
166
+ {
167
+ name: 'agy_status',
168
+ description: 'Read a live snapshot of what the local agy agent is currently doing. Returns one section per project (working directory): running count, the current step (tool name + arguments being executed, or agent_response thinking/typing), the recent step trail (tools executed, done/error), and the last completed run status + conversation id. Optional cwd filters to a single project. Call this to check on an in-flight agy_run/agy_continue without waiting for it to finish.',
169
+ inputSchema: { type: 'object', properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
170
+ },
171
+ {
172
+ name: 'agy_quota',
173
+ description: 'Query the Google AI plan (Antigravity OAuth consumer) quota pool behind the local agy CLI. Reads the Windows credential manager entry (gemini:antigravity) that agy wrote at login, refreshes the access token, then calls Google Cloud Code quota APIs: fetchAvailableModels (per-model remaining pool percentage) and retrieveUserQuotaSummary (grouped plan buckets: weekly + 5h windows). Each model carries family (gemini/claude/gpt/other) and recommended (false for Claude/GPT 3p models — do not pass those to agy_run; they are effectively unusable). Call before agy_run only if you want the numbers: the Gemini 5h window is the sole gate (agy_run/agy_continue already refuse silently below 10% of it), while the weekly window is NOT a per-task gate — an exhausted weekly bucket only means agy is a poor pick for the rest of the week, never a reason to abort or shrink the current task.',
174
+ inputSchema: { type: 'object', properties: { summary: { type: 'boolean', description: 'Return only a compact summary (weekly buckets + top models).' } } }
175
+ }
176
+ ]
177
+
178
+ export function tools(env) {
179
+ // MCP 形态是真 Node 进程(process 恒可用)→ quota 工具恒注册;畸形无 process
180
+ // 环境退化为三件套(Lead 定案 4 的注册条件精神)。
181
+ if (typeof process === 'undefined') return TOOLS.filter((t) => t.name !== 'agy_quota').map((t) => Object.assign({}, t))
182
+ return TOOLS.map((t) => Object.assign({}, t))
183
+ }
184
+
185
+ export async function call(name, args, io) {
186
+ if (io) lastIo = io
187
+ if (!lastIo) return 'agy error: MCP io not initialized'
188
+ const a = args || {}
189
+ if (name === 'agy_quota') {
190
+ const res = await provider.cachedQuotaCheck()
191
+ const lines = []
192
+ if (!res || !res.ok) {
193
+ lines.push('agy_quota failed: ' + String((res && res.error) || 'unknown'))
194
+ } else if (a.summary) {
195
+ const weekly = (res.groups || []).flatMap((g) => (g.buckets || []).filter((b) => b.window === 'weekly').map((b) => ({ g: g.displayName, id: b.bucketId, pct: Math.round((b.remainingFraction || 0) * 100), reset: b.resetTime })))
196
+ lines.push('agy quota [tier=' + (res.tier || '?') + '] weekly: ' + weekly.map((w) => (w.id || w.g) + ' ' + w.pct + '%').join(', '))
197
+ const pool = (res.models || []).filter((m) => m.recommended !== false)
198
+ const list = pool.length ? pool : (res.models || [])
199
+ const top = list.slice(0, 4).map((m) => String(m.name).split('-').slice(0, 2).join('-') + ' ' + m.percentage + '%').join(', ')
200
+ lines.push('top models: ' + (top || '(none)'))
201
+ } else {
202
+ lines.push('agy quota [tier=' + (res.tier || '?') + ']')
203
+ for (const g of (res.groups || [])) {
204
+ for (const b of (g.buckets || [])) {
205
+ const pct = typeof b.remainingFraction === 'number' ? Math.round(b.remainingFraction * 100) : '?'
206
+ lines.push('· ' + (b.bucketId || g.displayName || '') + ' [' + (b.window || '') + '] ' + pct + '%' + (b.resetTime ? ' reset=' + b.resetTime : ''))
207
+ }
208
+ }
209
+ lines.push('models (pool %):')
210
+ for (const m of (res.models || []).slice(0, 10)) { lines.push(' ' + (m.displayName || m.name) + ' : ' + m.percentage + '%' + (m.recommended === false ? ' [3p: Claude/GPT 不推荐]' : '') + (m.resetTime ? ' (reset ' + m.resetTime + ')' : '')) }
211
+ }
212
+ return lines.join('\n')
213
+ }
214
+ if (name === 'agy_status') {
215
+ const snap = engine.statusSnapshot()
216
+ if (a.cwd) {
217
+ snap.projects = snap.projects.filter((p) => p.cwd === String(a.cwd))
218
+ const g = snap.projects[0]
219
+ 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 })
220
+ }
221
+ return renderStatus(snap, provider)[0].text
222
+ }
223
+ if (!a.prompt || !String(a.prompt).trim()) return 'agy error: prompt is required'
224
+ const mapped = Object.assign({}, a)
225
+ if (name === 'agy_continue') {
226
+ if (!mapped.conversationId && mapped.latest) mapped.continueLatest = true
227
+ if (mapped.conversationId) mapped.sessionId = mapped.conversationId
228
+ }
229
+ // v1.5.11 5h 硬阻断 verbatim:额度不可得/失败 → 静默放行(宽容降级)。
230
+ const block = await provider.quotaGate5h(mapped, null)
231
+ if (block) return 'agy 未调用(5h 池子额度 <10%):' + block.stderr
232
+ const res = await runAgy(mapped)
233
+ return textResult(res)
234
+ }
235
+
236
+ export function check(env) {
237
+ const ts = tools(env)
238
+ const names = ts.map((t) => t.name)
239
+ for (const req of ['agy_run', 'agy_continue', 'agy_status', 'agy_quota']) {
240
+ if (!names.includes(req)) throw new Error('agy mcp: missing tool ' + req)
241
+ }
242
+ for (const t of ts) {
243
+ if (!t.name || !t.description || !t.inputSchema || t.inputSchema.type !== 'object') throw new Error('agy mcp: bad tool def ' + JSON.stringify(t && t.name))
244
+ }
245
+ // MCP 面必须覆盖 descriptor 三件套(名字一致,提示词连续性红线)。
246
+ for (const k of ['run', 'continue', 'status']) {
247
+ if (!names.includes(provider.tools[k].name)) throw new Error('agy mcp: tools surface missing descriptor name ' + provider.tools[k].name)
248
+ }
249
+ const b = provider.buildArgv(['agy'], { prompt: 'x' }, { planActive: false })
250
+ for (const f of ['--output-format', 'stream-json', '--dangerously-skip-permissions', '--print-timeout', '600s', '--mode', 'accept-edits']) {
251
+ if (!b.argv.includes(f)) throw new Error('agy mcp: argv contract drift: ' + f)
252
+ }
253
+ return true
254
+ }