@luchenglong/cli-bridge 0.2.2 → 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.
- package/CHANGELOG.md +43 -0
- package/README.md +1 -0
- package/docs/HANDOVER.md +226 -0
- package/docs/RELEASE-SOP.md +5 -2
- package/dynamic/agy.host.js +1 -1
- package/dynamic/codebuddy.host.js +1 -1
- package/dynamic/mimo.host.js +1 -1
- package/dynamic/opencode.host.js +1443 -0
- package/dynamic/opencode.host.template.mjs +125 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +1 -1
- package/mcp/cli-bridge-mcp.mjs +3 -2
- package/mcp/providers/opencode.mjs +236 -0
- package/package.json +4 -2
- package/preset/opencode-first/agent.cordis.yml +141 -0
- package/preset/opencode-first/cli-bridge.mjs +147 -0
- package/preset/opencode-first/plugin-list.mjs +26 -0
- package/preset/opencode-first/preset.yml +3 -0
- package/providers/opencode.mjs +619 -0
- package/providers/registry.mjs +9 -4
- package/scripts/build.mjs +1 -1
- package/scripts/verify.mjs +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// dynamic/opencode.host.template.mjs — opencode 动态沙箱宿主模板(cordis_define code.host 源)。
|
|
2
|
+
//
|
|
3
|
+
// 构建(node scripts/build.mjs --only=opencode)把 core/bridge-kit.mjs 与
|
|
4
|
+
// providers/opencode.mjs 分别内联进下方两个占位标记行(各占一行、行内仅此标记),
|
|
5
|
+
// 生成 dynamic/opencode.host.js。注意:标记字面量绝不能出现在本注释里 —— 构建做全局
|
|
6
|
+
// 替换,注释中出现同样会被替换成正文代码而炸掉语法。
|
|
7
|
+
// 沙箱纪律(旧仓 dynamic/host.js 头注 verbatim 精神):
|
|
8
|
+
// - 禁 import/require、无 TypeScript;无 AbortController/process 裸引用/Buffer/原生
|
|
9
|
+
// 定时器 —— process 只经 typeof 守卫探测;宿主能力全部来自注入的 harness.*/ctx。
|
|
10
|
+
// - 顶层 return { inject, apply } 为动态宿主约定形态(生成物是 .js/CJS 包装,顶层
|
|
11
|
+
// return 合法;本模板文件是 .mjs,不要直接 node --check 模板本身,只 check 生成物)。
|
|
12
|
+
// - 无 ctx.emit:状态经 ctx.get('cliBridgeCollector').mergeSnapshot('opencode', snap)
|
|
13
|
+
// 推给家级灯;动态形态不宣告 mode(旧语义:普通模式灯仅在调用时临时显示)。
|
|
14
|
+
/*__KIT__*/
|
|
15
|
+
/*__PROVIDER__*/
|
|
16
|
+
/*__SCHED__*/
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
inject: ['tools', 'subprocess', 'systemPrompt', 'timer'],
|
|
20
|
+
apply(ctx) {
|
|
21
|
+
// 调度门禁(0.2.0):设置面板关掉 opencode → 本形态不注册工具/政策段;fail-open。
|
|
22
|
+
if (!isBridgeEnabled('opencode', SCHED_ALL_IDS)) return
|
|
23
|
+
const subprocess = ctx.subprocess
|
|
24
|
+
const sandboxPolicy = ctx.get('sandboxPolicy')
|
|
25
|
+
// 沙箱可能运行在无 process 的环境:typeof 守卫读 env(旧仓 dynamic 的等价面)。
|
|
26
|
+
const env = (typeof process !== 'undefined' && process.env) ? process.env : {}
|
|
27
|
+
|
|
28
|
+
const provider = defineProvider({ env })
|
|
29
|
+
|
|
30
|
+
function publish(snapshot) {
|
|
31
|
+
try {
|
|
32
|
+
const collector = ctx.get('cliBridgeCollector')
|
|
33
|
+
if (collector && typeof collector.mergeSnapshot === 'function') { collector.mergeSnapshot('opencode', snapshot); return }
|
|
34
|
+
} catch (e) { /* fall through to legacy alias */ }
|
|
35
|
+
try {
|
|
36
|
+
const legacy = ctx.get('opencodeCollector')
|
|
37
|
+
if (legacy && typeof legacy.mergeSnapshot === 'function') legacy.mergeSnapshot(snapshot)
|
|
38
|
+
} catch (e) { /* 灯数据尽力而为 */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// exe 优先级(与描述符同一套):env OPENCODE_BIN/OPENCODE_CLI > 桌面版 V2 探测
|
|
42
|
+
// > PATH 查找 opencode-cli(subprocess.resolveExecutable)。
|
|
43
|
+
// 二进制名 opencode-cli(V2 桌面版),不做 opencode.exe 兜底(V1/V2 不能并存)。
|
|
44
|
+
const exeCandidates = resolveExePrefix(env, '')
|
|
45
|
+
async function resolveExe(backend, signal) {
|
|
46
|
+
for (const c of exeCandidates) {
|
|
47
|
+
if (/\.(mjs|cjs|js)$/i.test(c) && typeof process !== 'undefined' && process.execPath) return [process.execPath, c]
|
|
48
|
+
return c
|
|
49
|
+
}
|
|
50
|
+
return await subprocess.resolveExecutable('opencode-cli', undefined, signal)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// cwd 兜底:OPENCODE_MCP_CWD > sandboxPolicy.workspaceRoot > process.cwd() > '.'。
|
|
54
|
+
function getCwdFallback() {
|
|
55
|
+
if (env.OPENCODE_MCP_CWD) return String(env.OPENCODE_MCP_CWD)
|
|
56
|
+
if (sandboxPolicy && typeof sandboxPolicy.workspaceRoot === 'string' && sandboxPolicy.workspaceRoot) return sandboxPolicy.workspaceRoot
|
|
57
|
+
if (typeof process !== 'undefined' && typeof process.cwd === 'function') { try { return process.cwd() } catch (e) {} }
|
|
58
|
+
return '.'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
provider.attach({ ctx, subprocess, resolveExe, getCwdFallback, publish })
|
|
62
|
+
|
|
63
|
+
const OUTPUT_SCHEMA = { type: 'object', additionalProperties: true }
|
|
64
|
+
|
|
65
|
+
harness.registerTool(ctx, harness.defineTool({
|
|
66
|
+
name: provider.tools.run.name,
|
|
67
|
+
description: provider.tools.run.description,
|
|
68
|
+
parameters: provider.tools.run.inputSchema,
|
|
69
|
+
output: { schema: OUTPUT_SCHEMA, render: function (args, value) { return renderResult(value, provider) } },
|
|
70
|
+
execute: function (args, exec) { return provider.runToolLogic(args, exec) }
|
|
71
|
+
}))
|
|
72
|
+
harness.registerTool(ctx, harness.defineTool({
|
|
73
|
+
name: provider.tools.continue.name,
|
|
74
|
+
description: provider.tools.continue.description,
|
|
75
|
+
parameters: provider.tools.continue.inputSchema,
|
|
76
|
+
output: { schema: OUTPUT_SCHEMA, render: function (args, value) { return renderResult(value, provider) } },
|
|
77
|
+
execute: function (args, exec) { return provider.continueToolLogic(args, exec) }
|
|
78
|
+
}))
|
|
79
|
+
harness.registerTool(ctx, harness.defineTool({
|
|
80
|
+
name: provider.tools.status.name,
|
|
81
|
+
description: provider.tools.status.description,
|
|
82
|
+
parameters: provider.tools.status.inputSchema,
|
|
83
|
+
output: { schema: OUTPUT_SCHEMA, render: function (args, value) { return renderStatus(value, provider) } },
|
|
84
|
+
execute: function (args) { return provider.statusToolLogic(args) }
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
// 浏览器指示灯轮询的 RPC 面(旧 dynamic/handle('<id>_status') verbatim 语义)。
|
|
88
|
+
harness.handle('opencode_status', function () { return provider.statusToolLogic({}) })
|
|
89
|
+
|
|
90
|
+
const policyText = [
|
|
91
|
+
'opencode-first execution policy (local opencode CLI bridge).',
|
|
92
|
+
'',
|
|
93
|
+
'Local agent CLI "opencode" handles coding tasks under full DSH control (permissions auto-approved, never blocks on a prompt). By default, opencode runs in the background and returns a jobId immediately; collect results via job_output and watch live progress via opencode_status anytime.',
|
|
94
|
+
'',
|
|
95
|
+
'Decision table for tool selection:',
|
|
96
|
+
'- Single read/glob/grep or short (<30s) command -> Native tools.',
|
|
97
|
+
'- Implementing, editing, refactoring, debugging, builds, multi-file investigation -> opencode_run.',
|
|
98
|
+
'- Follow-ups on the same topic -> opencode_continue (with latest=true or sessionId; opencode preserves session context).',
|
|
99
|
+
'- Final build/test verification of opencode output -> Native tools.',
|
|
100
|
+
'',
|
|
101
|
+
'Context protocol (one topic = one opencode session):',
|
|
102
|
+
'- First dispatch on a topic: provide the task with a compact CONTEXT preamble (point to paths, do not paste file contents):',
|
|
103
|
+
' CONTEXT: 2-6 lines — goal; repo root; key paths; decisions/constraints; current state.',
|
|
104
|
+
' TASK: the complete instruction.',
|
|
105
|
+
'- Follow-ups: call opencode_continue and state only what is new.',
|
|
106
|
+
'',
|
|
107
|
+
'Execution & Fallback protocol:',
|
|
108
|
+
'- Dispatch defaults to background=true (set background=false only to block). Collect via job_output.',
|
|
109
|
+
'- On rate-limit or network failure, a confirmation dialog opens in BOTH foreground and background while in flight.',
|
|
110
|
+
'- If result has fallback=true (status FALLBACK_TO_DSH), the user chose DSH-local: finish with native tools and NEVER call opencode again for this task.',
|
|
111
|
+
'- Never loop opencode calls; never ask opencode to call back into DSH.',
|
|
112
|
+
'',
|
|
113
|
+
'Model & credentials:',
|
|
114
|
+
'- Prefer a free model for routine work: pass a model id ending in -free (e.g. "opencode/nemotron-3-ultra-free") unless this task genuinely needs a frontier model.',
|
|
115
|
+
'- Omit the model unless you have a reason: opencode then uses its own configured default (or the one chosen in CLI Bridge settings). Pass model in provider/model form to override.',
|
|
116
|
+
'- Credentials are owned by opencode, not by this bridge: auth/API-key failures mean the user should run `opencode auth login`. Do not retry in a loop.'
|
|
117
|
+
].join('\n')
|
|
118
|
+
|
|
119
|
+
ctx.systemPrompt.section({
|
|
120
|
+
name: 'opencode:policy',
|
|
121
|
+
order: (typeof ctx.systemPrompt.getSectionOrder === 'function' ? ctx.systemPrompt.getSectionOrder('TOOL_SUBAGENT') : 2800) + 10,
|
|
122
|
+
text: policyText
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -25,7 +25,7 @@ export const PKG_NAME = '@luchenglong/cli-bridge'
|
|
|
25
25
|
|
|
26
26
|
// preset 目录名映射(与 scripts/build.mjs 的 PRESET_DIRS 一致;目录名带 -first 后缀是
|
|
27
27
|
// 三个旧包时代的遗留布局,provider id 不带 —— 这里不映射就永远找不到 plugin-list)。
|
|
28
|
-
const PRESET_DIRS = { agy: 'agy-first', codebuddy: 'codebuddy-first', mimo: 'mimo-first' }
|
|
28
|
+
const PRESET_DIRS = { agy: 'agy-first', codebuddy: 'codebuddy-first', mimo: 'mimo-first', opencode: 'opencode-first' }
|
|
29
29
|
|
|
30
30
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
31
31
|
const ROOT = join(HERE, '..', '..', '..')
|
package/mcp/cli-bridge-mcp.mjs
CHANGED
|
@@ -28,12 +28,13 @@ import { resolveScheduling } from '../core/scheduling.mjs'
|
|
|
28
28
|
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
|
+
import * as opencodeMcp from './providers/opencode.mjs'
|
|
31
32
|
|
|
32
33
|
const SERVER_NAME = 'cli-bridge-mcp'
|
|
33
|
-
const SERVER_VERSION = '0.2.
|
|
34
|
+
const SERVER_VERSION = '0.2.3'
|
|
34
35
|
const PROTOCOL = '2024-11-05'
|
|
35
36
|
|
|
36
|
-
const PROVIDERS = [agyMcp, codebuddyMcp, mimoMcp]
|
|
37
|
+
const PROVIDERS = [agyMcp, codebuddyMcp, mimoMcp, opencodeMcp]
|
|
37
38
|
|
|
38
39
|
function detectHomeDir() {
|
|
39
40
|
if (process.env.DSH_HOME) return process.env.DSH_HOME
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// mcp/providers/opencode.mjs — opencode MCP 工具面(契约 §4)。
|
|
2
|
+
// framing/生命周期归 keystone 统一服务器 mcp/cli-bridge-mcp.mjs。
|
|
3
|
+
//
|
|
4
|
+
// 与 agy 面的结构同构(旧 MCP 语义保持):直接 spawn opencode(.exe)(win32 用
|
|
5
|
+
// opencode.exe;OPENCODE_BIN 覆盖;.js/.mjs 目标经 process.execPath 包装,供测试
|
|
6
|
+
// fixture 使用)。无 ctx.emit:状态走快照文件 {homeDir}/cli-bridge-mcp-opencode.json
|
|
7
|
+
// (bridge-kit buildMcpBridgePayload + 原子写 tmp→rename;写失败静默)。
|
|
8
|
+
//
|
|
9
|
+
// opencode 无 quota/额度门禁(凭据与额度由 opencode 自身与 provider 决定),
|
|
10
|
+
// 因此本面只有三件套。
|
|
11
|
+
import { spawn as nodeSpawn } from 'node:child_process'
|
|
12
|
+
import { resolve as resolvePath } from 'node:path'
|
|
13
|
+
|
|
14
|
+
import defineProvider, { resolveExePrefix } from '../../providers/opencode.mjs'
|
|
15
|
+
import { createStatusEngine, createLineStream, buildMcpBridgePayload, isLimited, renderStatus } from '../../core/bridge-kit.mjs'
|
|
16
|
+
|
|
17
|
+
export const meta = { id: 'opencode', label: 'OPENCODE', version: '0.2.2' }
|
|
18
|
+
|
|
19
|
+
const provider = defineProvider({
|
|
20
|
+
env: process.env,
|
|
21
|
+
process,
|
|
22
|
+
nodeChild: { spawn: nodeSpawn }
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
let lastIo = null
|
|
26
|
+
function publishSnapshot(snapshot) {
|
|
27
|
+
const io = lastIo
|
|
28
|
+
if (!io) return
|
|
29
|
+
try {
|
|
30
|
+
const target = io.path.join(io.homeDir, 'cli-bridge-mcp-opencode.json')
|
|
31
|
+
const payload = buildMcpBridgePayload(snapshot, process.pid, Date.now())
|
|
32
|
+
const tmp = target + '.' + process.pid + '.tmp'
|
|
33
|
+
try { io.fs.mkdirSync(io.path.dirname(target), { recursive: true }) } catch (e) {}
|
|
34
|
+
io.fs.writeFileSync(tmp, JSON.stringify(payload), 'utf8')
|
|
35
|
+
io.fs.renameSync(tmp, target)
|
|
36
|
+
} catch (e) { /* 灯数据尽力而为,绝不影响工具调用 */ }
|
|
37
|
+
}
|
|
38
|
+
// provider.engine 在 attach 后才存在;MCP 形态不 attach,用本模块的 engine。
|
|
39
|
+
const engine = createStatusEngine({ provider, publish: publishSnapshot })
|
|
40
|
+
provider.engine = engine
|
|
41
|
+
|
|
42
|
+
// ── exe / cwd 解析 ────────────────────────────────────────────────────────────
|
|
43
|
+
// 与描述符同一套优先级(env > 桌面版探测 > PATH 名字);二进制名 opencode-cli.exe
|
|
44
|
+
// (V2 桌面版),不做 opencode.exe 兜底以免误命中不能并存的另一个版本。
|
|
45
|
+
function exePrefix() {
|
|
46
|
+
return resolveExePrefix(process.env, '')
|
|
47
|
+
}
|
|
48
|
+
function resolveCwd(a) {
|
|
49
|
+
if (a && a.cwd) return resolvePath(String(a.cwd))
|
|
50
|
+
return process.env.OPENCODE_MCP_CWD || process.cwd()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── spawn(实时折叠走 bridge-kit 行流)────────────────────────────────────────
|
|
54
|
+
function runOpencode(args) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const built = provider.buildArgv(exePrefix(), args, { planActive: false, defaultMode: 'auto' })
|
|
57
|
+
const cwd = resolveCwd(args)
|
|
58
|
+
let child
|
|
59
|
+
try {
|
|
60
|
+
child = nodeSpawn(built.argv[0], built.argv.slice(1), { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
|
|
61
|
+
} catch (e) {
|
|
62
|
+
resolve({ ok: false, status: 'SPAWN_ERROR', response: '', sessionId: null, durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode: built.mode, backend: null, stderr: String(e && e.message || e) })
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
engine.begin(cwd)
|
|
66
|
+
let out = ''
|
|
67
|
+
let err = ''
|
|
68
|
+
let killed = false
|
|
69
|
+
const timer = setTimeout(() => { killed = true; try { child.kill() } catch (e) {} }, (built.timeoutSec + 60) * 1000)
|
|
70
|
+
const stream = createLineStream((ln) => {
|
|
71
|
+
const t = ln.trim()
|
|
72
|
+
if (!t.startsWith('{')) return
|
|
73
|
+
try { engine.foldEvent(JSON.parse(t), cwd) } catch (e) {}
|
|
74
|
+
})
|
|
75
|
+
child.stdout.on('data', (d) => {
|
|
76
|
+
out += d
|
|
77
|
+
if (out.length > 4000000) out = out.slice(-2000000)
|
|
78
|
+
stream.pushChunk(String(d))
|
|
79
|
+
})
|
|
80
|
+
child.stderr.on('data', (d) => { err += d; if (err.length > 1000000) err = err.slice(-500000) })
|
|
81
|
+
child.on('error', (e) => {
|
|
82
|
+
clearTimeout(timer)
|
|
83
|
+
stream.flush()
|
|
84
|
+
const res = { ok: false, status: 'OPENCODE_UNAVAILABLE', response: '', sessionId: null, durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode: built.mode, backend: null, stderr: 'opencode spawn failed: ' + String(e && e.message || e) }
|
|
85
|
+
engine.end(res, cwd)
|
|
86
|
+
resolve(res)
|
|
87
|
+
})
|
|
88
|
+
child.on('close', (code) => {
|
|
89
|
+
clearTimeout(timer)
|
|
90
|
+
stream.flush()
|
|
91
|
+
const outcome = { exitCode: killed ? 124 : code }
|
|
92
|
+
let res = provider.buildResult(provider.parseJson(out), outcome, built.mode, err, out, null)
|
|
93
|
+
if (killed && !res.ok) {
|
|
94
|
+
res.status = 'HUNG_TIMEOUT'
|
|
95
|
+
res.stderr = (res.stderr ? res.stderr + ' ' : '') + '[killed by DSH timeout guard after ' + built.timeoutSec + 's; if this was a long-running script (build/test) raise timeoutSec]'
|
|
96
|
+
}
|
|
97
|
+
engine.end(res, cwd)
|
|
98
|
+
resolve(res)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function textResult(res) {
|
|
104
|
+
if (res.fallback) {
|
|
105
|
+
return 'opencode unavailable (' + res.reason + '). Use native tools / the local model for this task; do not retry opencode.'
|
|
106
|
+
}
|
|
107
|
+
const limited = !res.ok && isLimited(res)
|
|
108
|
+
const head = 'opencode ' + (res.ok ? 'OK' : 'FAILED') + ' [status=' + res.status +
|
|
109
|
+
(res.sessionId ? ' session=' + res.sessionId : '') +
|
|
110
|
+
(res.durationSeconds != null ? ' ' + res.durationSeconds + 's' : '') + ']'
|
|
111
|
+
let note = ''
|
|
112
|
+
if (limited) note = '\n\n[Note: this looks like a rate-limit / network failure. Do NOT retry opencode in a loop; finish the task with your own tools, or ask the user.]'
|
|
113
|
+
const body = res.response || (res.stderr ? '[stderr] ' + res.stderr : (res.rawStdout ? '[raw] ' + res.rawStdout : ''))
|
|
114
|
+
return head + (body ? '\n\n' + body : '') + note
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── 工具面 ────────────────────────────────────────────────────────────────────
|
|
118
|
+
const TOOLS = [
|
|
119
|
+
{
|
|
120
|
+
name: 'opencode_run',
|
|
121
|
+
description: 'Dispatch a coding/build/debug/investigation task to the local opencode agent CLI and return its final answer. opencode runs fully non-interactively (permissions auto-approved, never blocks on a prompt). Prefer it for implementation, multi-file edits, refactors and debugging; use your own tools for quick read-only lookups and final build/test verification. While it runs, call opencode_status to watch what opencode is doing live.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
prompt: { type: 'string', description: 'The task instruction for opencode. On first dispatch for a topic, include a compact CONTEXT preamble (goal, repo root, key paths, decisions/constraints; point at paths, do not paste file contents — opencode reads the repo itself). For follow-ups on the same topic, use opencode_continue with latest=true or sessionId (opencode keeps session context; state only what is new).' },
|
|
126
|
+
model: { type: 'string', description: 'Optional model in provider/model form (e.g. "anthropic/claude-sonnet-4-20250514"). Omit to let opencode use its own configured default.' },
|
|
127
|
+
agent: { type: 'string', description: 'Optional opencode agent (subagent) name.' },
|
|
128
|
+
variant: { type: 'string', description: 'Optional provider-specific reasoning variant (e.g. high, max, minimal).' },
|
|
129
|
+
cwd: { type: 'string', description: 'Working directory for opencode (default: OPENCODE_MCP_CWD or the DSH workspace).' },
|
|
130
|
+
files: { type: 'array', items: { type: 'string' }, description: 'Optional file paths to attach to the message.' },
|
|
131
|
+
title: { type: 'string', description: 'Optional session title.' },
|
|
132
|
+
timeoutSec: { type: 'integer', description: '10-3600, default 600.' },
|
|
133
|
+
autoApprove: { type: 'boolean', description: 'Default true: pass --auto so opencode never blocks on a permission prompt. Set false to require explicit approval.' }
|
|
134
|
+
},
|
|
135
|
+
required: ['prompt']
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'opencode_continue',
|
|
140
|
+
description: 'Continue an existing opencode session with a follow-up prompt, reusing opencode context. Pass sessionId from a prior opencode_run result, or set latest=true for the most recent session.',
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
prompt: { type: 'string', description: 'Follow-up instruction for the ongoing opencode session. State only what is new; opencode preserves its session context.' },
|
|
145
|
+
sessionId: { type: 'string' },
|
|
146
|
+
latest: { type: 'boolean', description: 'Continue the most recent opencode session.' },
|
|
147
|
+
model: { type: 'string' },
|
|
148
|
+
agent: { type: 'string' },
|
|
149
|
+
variant: { type: 'string' },
|
|
150
|
+
cwd: { type: 'string' },
|
|
151
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
152
|
+
timeoutSec: { type: 'integer', description: '10-3600, default 600.' },
|
|
153
|
+
autoApprove: { type: 'boolean' }
|
|
154
|
+
},
|
|
155
|
+
required: ['prompt']
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'opencode_status',
|
|
160
|
+
description: 'Read a live snapshot of what the local opencode agent is currently doing. Returns one section per project (working directory): running count, the current step (tool name + arguments being executed, or agent_response thinking/typing), the recent step trail (tools executed, done/error), and the last completed run status + session id. Optional cwd filters to a single project. Call this to check on an in-flight opencode_run/opencode_continue without waiting for it to finish.',
|
|
161
|
+
inputSchema: { type: 'object', properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'opencode_models',
|
|
165
|
+
description: 'List the models available to the local opencode CLI (from `opencode models`). Use it to pick a model before opencode_run, or to check which models are free. Prefer a -free model for routine work and reserve paid/frontier models for tasks that genuinely need them. If the CLI is missing or not logged in this returns ok:false with an actionable error — it never invents a model list.',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
freeOnly: { type: 'boolean', description: 'Return only free models (id ends with -free).' },
|
|
170
|
+
provider: { type: 'string', description: 'Optional provider filter, e.g. "opencode" or "opencode-go".' }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
export function tools(env) {
|
|
177
|
+
return TOOLS.map((t) => Object.assign({}, t))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function call(name, args, io) {
|
|
181
|
+
if (io) lastIo = io
|
|
182
|
+
if (!lastIo) return 'opencode error: MCP io not initialized'
|
|
183
|
+
const a = args || {}
|
|
184
|
+
if (name === 'opencode_status') {
|
|
185
|
+
const snap = engine.statusSnapshot()
|
|
186
|
+
if (a.cwd) {
|
|
187
|
+
snap.projects = snap.projects.filter((p) => p.cwd === String(a.cwd))
|
|
188
|
+
const g = snap.projects[0]
|
|
189
|
+
if (g) Object.assign(snap, { state: g.state, running: g.running, current: g.current, trail: g.trail, lastStatus: g.lastStatus, lastAt: g.lastAt, lastSessionId: g.lastSessionId, lastBackend: g.lastBackend, fallbackActive: g.fallbackActive, updatedAt: g.updatedAt })
|
|
190
|
+
}
|
|
191
|
+
return renderStatus(snap, provider)[0].text
|
|
192
|
+
}
|
|
193
|
+
if (name === 'opencode_models') {
|
|
194
|
+
const res = await provider.listModels(a)
|
|
195
|
+
if (!res.ok) return 'opencode_models failed: ' + String(res.error || 'unknown')
|
|
196
|
+
const list = res.models || []
|
|
197
|
+
if (!list.length) return 'opencode_models: no models matched'
|
|
198
|
+
const lines = ['opencode models (' + list.length + '):']
|
|
199
|
+
for (const m of list.slice(0, 40)) lines.push(' ' + m.id + (m.free ? ' [free]' : ''))
|
|
200
|
+
if (list.length > 40) lines.push(' … and ' + (list.length - 40) + ' more')
|
|
201
|
+
const free = list.filter((m) => m.free).length
|
|
202
|
+
lines.push('free: ' + free + ' / ' + list.length)
|
|
203
|
+
if (a.freeOnly && !free) lines.push('(no free models under this filter)')
|
|
204
|
+
return lines.join('\n')
|
|
205
|
+
}
|
|
206
|
+
if (!a.prompt || !String(a.prompt).trim()) return 'opencode error: prompt is required'
|
|
207
|
+
const mapped = Object.assign({}, a)
|
|
208
|
+
if (name === 'opencode_continue') {
|
|
209
|
+
if (!mapped.sessionId && mapped.latest) mapped.continueLatest = true
|
|
210
|
+
}
|
|
211
|
+
const res = await runOpencode(mapped)
|
|
212
|
+
return textResult(res)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function check(env) {
|
|
216
|
+
const ts = tools(env)
|
|
217
|
+
const names = ts.map((t) => t.name)
|
|
218
|
+
for (const req of ['opencode_run', 'opencode_continue', 'opencode_status', 'opencode_models']) {
|
|
219
|
+
if (!names.includes(req)) throw new Error('opencode mcp: missing tool ' + req)
|
|
220
|
+
}
|
|
221
|
+
for (const t of ts) {
|
|
222
|
+
if (!t.name || !t.description || !t.inputSchema || t.inputSchema.type !== 'object') throw new Error('opencode mcp: bad tool def ' + JSON.stringify(t && t.name))
|
|
223
|
+
}
|
|
224
|
+
// MCP 面必须覆盖 descriptor 三件套(名字一致,提示词连续性红线)。
|
|
225
|
+
for (const k of ['run', 'continue', 'status']) {
|
|
226
|
+
if (!names.includes(provider.tools[k].name)) throw new Error('opencode mcp: tools surface missing descriptor name ' + provider.tools[k].name)
|
|
227
|
+
}
|
|
228
|
+
// argv 契约漂移锁(实测 opencode 1.19.0 flag 集)。
|
|
229
|
+
const b = provider.buildArgv(['opencode'], { prompt: 'x' }, { planActive: false })
|
|
230
|
+
for (const f of ['run', 'x', '--format', 'json', '--auto']) {
|
|
231
|
+
if (!b.argv.includes(f)) throw new Error('opencode mcp: argv contract drift: ' + f)
|
|
232
|
+
}
|
|
233
|
+
const b2 = provider.buildArgv(['opencode'], { prompt: 'x', autoApprove: false }, { planActive: false })
|
|
234
|
+
if (b2.argv.includes('--auto')) throw new Error('opencode mcp: autoApprove=false must omit --auto')
|
|
235
|
+
return true
|
|
236
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luchenglong/cli-bridge",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "CLI Bridge for DeepSeek Harness (DSH): one package bridging
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "CLI Bridge for DeepSeek Harness (DSH): one package bridging four local coding-CLI agents — agy (Google Antigravity CLI), Tencent CodeBuddy Code, Xiaomi MiMo (MiMoCode / MiMo Desktop), and OpenCode. Each provider keeps its own agent preset (cordis-agy / codebuddy-first / cordis-mimo / opencode-first), tools (agy_run / codebuddy_run / mimo_run / opencode_run + continue + status), rate-limit/network fallback dialog, and zero-dependency MCP server, unified under a single home-level status light, one settings file, and one bundle patch. Merges agy-first-bridge, codebuddy-first-bridge and mimo-first-bridge.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
"./providers/agy": "./providers/agy.mjs",
|
|
20
20
|
"./providers/codebuddy": "./providers/codebuddy.mjs",
|
|
21
21
|
"./providers/mimo": "./providers/mimo.mjs",
|
|
22
|
+
"./providers/opencode": "./providers/opencode.mjs",
|
|
22
23
|
"./preset-agy": "./preset/agy-first/cli-bridge.mjs",
|
|
23
24
|
"./preset-codebuddy": "./preset/codebuddy-first/cli-bridge.mjs",
|
|
24
25
|
"./preset-mimo": "./preset/mimo-first/cli-bridge.mjs",
|
|
26
|
+
"./preset-opencode": "./preset/opencode-first/cli-bridge.mjs",
|
|
25
27
|
"./package.json": "./package.json"
|
|
26
28
|
},
|
|
27
29
|
"dsh": {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# 生成物 — 由 node scripts/build.mjs 从 preset/opencode-first/plugin-list.mjs 生成,勿手改。
|
|
2
|
+
# legacy 目录形态($DSH_HOME/.agent-presets/opencode-first/);新 DSH 走声明制
|
|
3
|
+
# (home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs 直接 import 同一源)。
|
|
4
|
+
- id: persona
|
|
5
|
+
name: '@deepseek-ai/dsh-persona'
|
|
6
|
+
config:
|
|
7
|
+
suffix: 'Your working directory is {{cwd}}.'
|
|
8
|
+
prefix: 'You are a coding agent powered by the {{model}} model.'
|
|
9
|
+
- id: agent-instructions
|
|
10
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
11
|
+
config:
|
|
12
|
+
maxBytes: 65536
|
|
13
|
+
- id: tool-bash
|
|
14
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
15
|
+
disabled: !!js 'process.platform === ''win32'''
|
|
16
|
+
- id: tool-pwsh
|
|
17
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
18
|
+
disabled: !!js 'process.platform !== ''win32'''
|
|
19
|
+
- id: tool-fs
|
|
20
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
21
|
+
- id: tool-fs-search
|
|
22
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
23
|
+
config:
|
|
24
|
+
sampleOverCapGlobResults: false
|
|
25
|
+
- id: tool-jobs
|
|
26
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
27
|
+
- id: skill-filesystem
|
|
28
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
29
|
+
- id: tool-skill
|
|
30
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
31
|
+
- id: command-goal
|
|
32
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
33
|
+
- id: tool-goal
|
|
34
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
35
|
+
- id: planning
|
|
36
|
+
name: 'cordis:group'
|
|
37
|
+
group: true
|
|
38
|
+
isolate:
|
|
39
|
+
planMode: true
|
|
40
|
+
config:
|
|
41
|
+
- id: plan-mode
|
|
42
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
43
|
+
config:
|
|
44
|
+
section: |
|
|
45
|
+
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
46
|
+
|
|
47
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
48
|
+
|
|
49
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
50
|
+
|
|
51
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
52
|
+
|
|
53
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
54
|
+
|
|
55
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
56
|
+
- id: compaction
|
|
57
|
+
name: 'cordis:group'
|
|
58
|
+
group: true
|
|
59
|
+
isolate:
|
|
60
|
+
compaction: true
|
|
61
|
+
toolResultPruner: true
|
|
62
|
+
config:
|
|
63
|
+
- id: compaction-basic
|
|
64
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
65
|
+
- id: command-compact
|
|
66
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
67
|
+
- id: tool-result-pruner
|
|
68
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
69
|
+
config:
|
|
70
|
+
thresholdChars: 8192
|
|
71
|
+
headChars: 4096
|
|
72
|
+
tailChars: 1024
|
|
73
|
+
- id: delegation
|
|
74
|
+
name: 'cordis:group'
|
|
75
|
+
group: true
|
|
76
|
+
isolate:
|
|
77
|
+
workflowEngine: true
|
|
78
|
+
config:
|
|
79
|
+
- id: tool-subagent-control
|
|
80
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
81
|
+
- id: tool-subagent-list-agents
|
|
82
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
83
|
+
- id: tool-subagent
|
|
84
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
85
|
+
config:
|
|
86
|
+
provider: spawn
|
|
87
|
+
toolName: subagent
|
|
88
|
+
modelSelectionSettings: true
|
|
89
|
+
backgroundMode: continuable
|
|
90
|
+
- id: tool-subagent-fork
|
|
91
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
92
|
+
config:
|
|
93
|
+
provider: fork
|
|
94
|
+
toolName: subagent_fork
|
|
95
|
+
backgroundMode: continuable
|
|
96
|
+
- id: tool-subagent-codex
|
|
97
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
98
|
+
disabled: true
|
|
99
|
+
config:
|
|
100
|
+
provider: codex
|
|
101
|
+
toolName: subagent_codex
|
|
102
|
+
backgroundMode: one-shot
|
|
103
|
+
maxDepth: provider-managed
|
|
104
|
+
- id: tool-subagent-claude-code
|
|
105
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
106
|
+
disabled: true
|
|
107
|
+
config:
|
|
108
|
+
provider: claude-code
|
|
109
|
+
toolName: subagent_claude_code
|
|
110
|
+
backgroundMode: one-shot
|
|
111
|
+
maxDepth: provider-managed
|
|
112
|
+
- id: workflow-ptc
|
|
113
|
+
name: '@deepseek-ai/dsh-workflow-ptc'
|
|
114
|
+
config:
|
|
115
|
+
provider: spawn
|
|
116
|
+
- id: tool-workflow
|
|
117
|
+
name: '@deepseek-ai/dsh-tool-workflow'
|
|
118
|
+
- id: tool-ralph
|
|
119
|
+
name: '@deepseek-ai/dsh-tool-ralph'
|
|
120
|
+
disabled: true
|
|
121
|
+
config:
|
|
122
|
+
subagentProvider: spawn
|
|
123
|
+
maxRounds: 64
|
|
124
|
+
- id: tool-ask-user
|
|
125
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
126
|
+
- id: tool-todo
|
|
127
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
128
|
+
config:
|
|
129
|
+
allowParallelInProgress: true
|
|
130
|
+
- id: tool-web
|
|
131
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
132
|
+
config:
|
|
133
|
+
fetch: true
|
|
134
|
+
searchTimeoutMs: 60000
|
|
135
|
+
- id: present
|
|
136
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
137
|
+
- id: tool-plugin-manager
|
|
138
|
+
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
139
|
+
disabled: true
|
|
140
|
+
- id: cli-bridge-opencode
|
|
141
|
+
name: '@luchenglong/cli-bridge/preset-opencode'
|