@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.
- package/CHANGELOG.md +91 -0
- package/PROVIDER-CONTRACT.md +87 -5
- package/README.md +12 -11
- package/docs/HANDOVER.md +109 -15
- package/dynamic/agy.host.js +1 -1
- package/dynamic/claude.host.js +1858 -0
- package/dynamic/claude.host.template.mjs +108 -0
- package/dynamic/codebuddy.host.js +1427 -1379
- package/dynamic/codex.host.js +1930 -0
- package/dynamic/codex.host.template.mjs +137 -0
- package/dynamic/mimo.host.js +721 -515
- package/dynamic/mimo.host.template.mjs +2 -0
- package/dynamic/opencode.host.js +741 -585
- package/dynamic/opencode.host.template.mjs +2 -0
- package/home-plugin/cli-bridge-indicator/lib/client.js +74 -90
- package/home-plugin/cli-bridge-indicator/lib/index.mjs +48 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +2 -3
- package/mcp/cli-bridge-mcp.mjs +4 -2
- package/mcp/providers/claude.mjs +231 -0
- package/mcp/providers/codebuddy.mjs +427 -427
- package/mcp/providers/codex.mjs +239 -0
- package/package.json +6 -2
- package/preset/claude-first/agent.cordis.yml +136 -0
- package/preset/claude-first/cli-bridge.mjs +131 -0
- package/preset/claude-first/plugin-list.mjs +28 -0
- package/preset/claude-first/preset.yml +3 -0
- package/preset/codex-first/agent.cordis.yml +136 -0
- package/preset/codex-first/cli-bridge.mjs +160 -0
- package/preset/codex-first/plugin-list.mjs +28 -0
- package/preset/codex-first/preset.yml +3 -0
- package/providers/claude-exe.mjs +112 -0
- package/providers/claude-models.mjs +338 -0
- package/providers/claude-policy.mjs +37 -0
- package/providers/claude.mjs +561 -0
- package/providers/codebuddy-catalog.mjs +367 -599
- package/providers/codebuddy-consts.mjs +72 -72
- package/providers/codebuddy-models-source.mjs +254 -0
- package/providers/codebuddy-models-tool.mjs +106 -106
- package/providers/codebuddy-schemas.mjs +46 -46
- package/providers/codebuddy.mjs +600 -578
- package/providers/codex-cli.mjs +405 -0
- package/providers/codex-models.mjs +107 -0
- package/providers/codex.mjs +585 -0
- package/providers/mimo-pure.mjs +270 -111
- package/providers/mimo.mjs +453 -517
- package/providers/opencode-exe.mjs +190 -63
- package/providers/opencode.mjs +553 -587
- package/providers/registry.mjs +25 -1
- package/scripts/build.mjs +261 -262
- package/scripts/verify.mjs +8 -2
package/providers/registry.mjs
CHANGED
|
@@ -6,16 +6,40 @@
|
|
|
6
6
|
// 单一 id 轴(契约 §3.1):本数组是全局唯一 provider 清单来源,调度面板、四形态门禁、
|
|
7
7
|
// preset 声明、MCP 工具面、status 视图全部由此派生,禁止第二处硬编码 id 列表。
|
|
8
8
|
import defineAgy from './agy.mjs'
|
|
9
|
+
import defineClaude from './claude.mjs'
|
|
9
10
|
import defineCodebuddy from './codebuddy.mjs'
|
|
11
|
+
import defineCodex from './codex.mjs'
|
|
10
12
|
import defineMimo from './mimo.mjs'
|
|
11
13
|
import defineOpencode from './opencode.mjs'
|
|
12
14
|
|
|
13
15
|
/** 稳定顺序(家级灯渲染顺序 = 此顺序;新增项一律追加在末尾,不重排既有顺序)。 */
|
|
14
|
-
export const PROVIDER_IDS = ['agy', 'codebuddy', 'mimo', 'opencode']
|
|
16
|
+
export const PROVIDER_IDS = ['agy', 'codebuddy', 'mimo', 'opencode', 'claude', 'codex']
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* provider id → `preset/<dir>/` 目录名(**单一来源**)。
|
|
20
|
+
*
|
|
21
|
+
* 目录名 ≠ presetId:agy 的 presetId 是 'cordis-agy' 但目录是 'agy-first',
|
|
22
|
+
* mimo 同理(旧包历史遗留);新 provider 两者相同('<id>-first')。
|
|
23
|
+
*
|
|
24
|
+
* 这份映射原先在 build.mjs / preset-declarations.mjs / verify.mjs 里各硬编码一份,
|
|
25
|
+
* 新增 provider 漏改任一处就会出现「产物静默不生成」或「plugin-list missing」这类
|
|
26
|
+
* 误导性报错(claude/codex 接入时实际踩到)。集中在此后,加第 N 家 provider 时
|
|
27
|
+
* 只需改本文件 + 交付描述符,与契约 §3.1 的单一 id 轴一致。
|
|
28
|
+
*/
|
|
29
|
+
export const PRESET_DIRS = {
|
|
30
|
+
agy: 'agy-first',
|
|
31
|
+
codebuddy: 'codebuddy-first',
|
|
32
|
+
mimo: 'mimo-first',
|
|
33
|
+
opencode: 'opencode-first',
|
|
34
|
+
claude: 'claude-first',
|
|
35
|
+
codex: 'codex-first'
|
|
36
|
+
}
|
|
15
37
|
|
|
16
38
|
const defs = {
|
|
17
39
|
agy: defineAgy,
|
|
40
|
+
claude: defineClaude,
|
|
18
41
|
codebuddy: defineCodebuddy,
|
|
42
|
+
codex: defineCodex,
|
|
19
43
|
mimo: defineMimo,
|
|
20
44
|
opencode: defineOpencode
|
|
21
45
|
}
|
package/scripts/build.mjs
CHANGED
|
@@ -1,262 +1,261 @@
|
|
|
1
|
-
// scripts/build.mjs — 派生产物生成器(keystone)。
|
|
2
|
-
//
|
|
3
|
-
// 生成物(勿手改;`--check` 模式漂移即失败,npm test 里的 build-sync 测试调用它):
|
|
4
|
-
// 1. dynamic/<id>.host.js ← dynamic/<id>.host.template.mjs
|
|
5
|
-
// 把 core/bridge-kit.mjs 内联进 /*__KIT__*/、providers/<id>.mjs 内联进
|
|
6
|
-
// /*__PROVIDER__*/、core/scheduling.mjs(+SCHED_ALL_IDS 常量)内联进
|
|
7
|
-
// /*__SCHED__*/(动态沙箱禁止 import,只能文本注入;单标记替换防递归错乱)。
|
|
8
|
-
// 注入时剥离各段的 `import ... from '...mjs'` 行并把 `export ` 关键字去掉。
|
|
9
|
-
// 2. preset/<id>/agent.cordis.yml ← preset/<id>/plugin-list.mjs(唯一权威源)
|
|
10
|
-
// 纯 JS 插件清单落地为 legacy 目录形态 YAML({__jsExpr} → !!js 块)。
|
|
11
|
-
// 两者同源生成,结构性消除「yml 与声明制漂移」这一历史风险。
|
|
12
|
-
// 3. home-plugin/cli-bridge-indicator/lib/preset-definitions/<id>.mjs
|
|
13
|
-
// ← 同上(声明制形态:插件数组直接给 agentPresets.register 消费)。
|
|
14
|
-
// ⚠️ 这一份由 preset-declarations.mjs 直接 import plugin-list,本脚本不再复制;
|
|
15
|
-
// 保留目录仅作兼容占位(旧版生成物清理)。
|
|
16
|
-
//
|
|
17
|
-
// 用法:node scripts/build.mjs [--check]
|
|
18
|
-
|
|
19
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync } from 'node:fs'
|
|
20
|
-
import { dirname, join, resolve } from 'node:path'
|
|
21
|
-
import { fileURLToPath } from 'node:url'
|
|
22
|
-
import { pathToFileURL } from 'node:url'
|
|
23
|
-
import { PROVIDER_IDS } from '../providers/registry.mjs'
|
|
24
|
-
|
|
25
|
-
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
26
|
-
// 单一 id 轴:新 provider 只改 registry(PROVIDER-CONTRACT.md §3.1),build 派生
|
|
27
|
-
// 物(dynamic 宿主 / SCHED_ALL_IDS 内联 / legacy YAML / plugin-list)自动跟上。
|
|
28
|
-
const ids = PROVIDER_IDS
|
|
29
|
-
const PKG = '@luchenglong/cli-bridge'
|
|
30
|
-
|
|
31
|
-
const MODE = process.argv.includes('--check') ? 'check' : 'write'
|
|
32
|
-
// --only=<id> 让单个 provider 的作者只生成/校验自己的派生物,避免并发写他人产物。
|
|
33
|
-
const onlyArg = process.argv.find((a) => a.startsWith('--only='))
|
|
34
|
-
const ONLY = onlyArg ? onlyArg.slice('--only='.length) : null
|
|
35
|
-
const drift = []
|
|
36
|
-
|
|
37
|
-
function read(rel) { return readFileSync(join(ROOT, rel), 'utf8') }
|
|
38
|
-
|
|
39
|
-
function emit(rel, content) {
|
|
40
|
-
const abs = join(ROOT, rel)
|
|
41
|
-
if (MODE === 'check') {
|
|
42
|
-
const cur = existsSync(abs) ? readFileSync(abs, 'utf8') : null
|
|
43
|
-
if (cur !== content) drift.push(rel)
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
mkdirSync(dirname(abs), { recursive: true })
|
|
47
|
-
writeFileSync(abs, content, 'utf8')
|
|
48
|
-
console.log('generated ' + rel)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ── 1. dynamic host(文本内联,沙箱禁 import)──────────────────────────────────
|
|
52
|
-
function stripModuleSyntax(src, label) {
|
|
53
|
-
// 多行 import(`import {\n a, b\n} from '...'`)在单行过滤下会残留 `import ` 前缀并
|
|
54
|
-
// 触发下面的守卫误报 —— 先把跨行 import 折叠成一行再过滤(与单行同规则)。
|
|
55
|
-
const collapsed = src.replace(
|
|
56
|
-
/^\s*import\s*\{[^}]*\}\s*from\s*['"][^'"]+['"];?\s*$/gms,
|
|
57
|
-
(m) => '// ' + m.replace(/\s+/g, ' ').trim()
|
|
58
|
-
)
|
|
59
|
-
const out = collapsed
|
|
60
|
-
.split('\n')
|
|
61
|
-
.filter((l) => !/^\s*import\s[^(]*from\s*['"][^'"]+['"];?\s*$/.test(l) && !/^\s*import\s*['"]/.test(l))
|
|
62
|
-
.join('\n')
|
|
63
|
-
// 缩进形态也要剥:`export function f()` 可能带前导空格(从 descriptor 闭包里
|
|
64
|
-
// 搬出来的函数保留原缩进),只匹配行首会漏掉 → 残留 export 在脚本体里直接语法错。
|
|
65
|
-
.replace(/^(\s*)export\s+(const|let|var|function|async function|class)\s/gm, '$1$2 ')
|
|
66
|
-
.replace(/^(\s*)export\s+default\s+(async\s+)?function\s+([A-Za-z0-9_$]+)\s*/gm, '$1$2function $3 ')
|
|
67
|
-
.replace(/^(\s*)export\s+default\s+(async\s+)?function\s*\(/gm, '$1$2function __default__(')
|
|
68
|
-
.replace(/^(\s*)export\s*\{[^}]*\};?\s*$/gm, '')
|
|
69
|
-
// 内联目标是「脚本体」:残留的 export default/import 说明源用了本转换器不支持
|
|
70
|
-
// 的形态。静默产出坏代码会让 dynamic host 在沙箱里炸得莫名其妙,必须大声失败。
|
|
71
|
-
if (/^\s*(export\s+default|import\s|import\()/m.test(out)) {
|
|
72
|
-
throw new Error('build: ' + (label || 'source') + ' 用了沙箱内联不支持的模块形态(provider 需具名 `export default function defineProvider(...)`;bridge-kit 不得 import)')
|
|
73
|
-
}
|
|
74
|
-
return out
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function buildDynamicHost(id) {
|
|
78
|
-
const tplRel = 'dynamic/' + id + '.host.template.mjs'
|
|
79
|
-
if (!existsSync(join(ROOT, tplRel))) return null
|
|
80
|
-
const tpl = read(tplRel)
|
|
81
|
-
const kit = stripModuleSyntax(read('core/bridge-kit.mjs'), 'core/bridge-kit.mjs')
|
|
82
|
-
const prov = stripModuleSyntax(read('providers/' + id + '.mjs'), 'providers/' + id + '.mjs')
|
|
83
|
-
// 调度单源(0.2.0):core/scheduling.mjs 无 import,可安全内联进沙箱作用域。
|
|
84
|
-
// SCHED_ALL_IDS 由构建注入(全量 provider 清单)—— 沙箱禁 import,拿不到 registry。
|
|
85
|
-
// 模板没有 /*__SCHED__*/ 占位(旧模板)→ 不注入,保持向后兼容。
|
|
86
|
-
const sched = stripModuleSyntax(read('core/scheduling.mjs'), 'core/scheduling.mjs')
|
|
87
|
-
+ '\nconst SCHED_ALL_IDS = ' + JSON.stringify(ids) + '\n'
|
|
88
|
-
// 可选的多段内联:providers/<id>-*.mjs(如 <id>-catalog.mjs、<id>-models-tool.mjs)。
|
|
89
|
-
// 语义与 __SCHED__ 同构(模板没有该占位符 → 不注入,向后兼容)。
|
|
90
|
-
// 目的:providers/<id>.mjs 超 600 行红线时把纯逻辑拆出去,沙箱里仍是**单脚本**。
|
|
91
|
-
// 支持多个片文件(按文件名字典序拼接)而不是写死一个:每撞一次 600 行红线就改一次
|
|
92
|
-
// 构建器的做法不可持续,泛用性与顺序确定性都要有。
|
|
93
|
-
const extraFiles = readdirSync(join(ROOT, 'providers'))
|
|
94
|
-
.filter((f) => f.startsWith(id + '-') && f.endsWith('.mjs'))
|
|
95
|
-
.sort()
|
|
96
|
-
const extra = extraFiles.length
|
|
97
|
-
? extraFiles.map((f) => {
|
|
98
|
-
const rel = 'providers/' + f
|
|
99
|
-
return '// ── inlined from ' + rel + ' ──\n' + stripModuleSyntax(read(rel), rel)
|
|
100
|
-
}).join('\n')
|
|
101
|
-
: null
|
|
102
|
-
if (!tpl.includes('/*__KIT__*/') || !tpl.includes('/*__PROVIDER__*/')) {
|
|
103
|
-
throw new Error(tplRel + ' missing /*__KIT__*/ or /*__PROVIDER__*/ placeholder')
|
|
104
|
-
}
|
|
105
|
-
let out = tpl.replace('/*__KIT__*/', () => kit).replace('/*__PROVIDER__*/', () => prov)
|
|
106
|
-
// __EXTRA__ 放在 KIT/PROVIDER 之后替换:避免其内容里若含同名占位符被二次展开。
|
|
107
|
-
if (out.includes('/*__EXTRA__*/')) {
|
|
108
|
-
if (!extra) throw new Error(tplRel + ': __EXTRA__ placeholder present but no providers/' + id + '-*.mjs fragment found')
|
|
109
|
-
out = out.replace('/*__EXTRA__*/', () => extra)
|
|
110
|
-
} else if (tpl.includes('/*__EXTRA__*/')) throw new Error(tplRel + ': __EXTRA__ placeholder vanished after other replacements')
|
|
111
|
-
if (out.includes('/*__SCHED__*/')) out = out.replace('/*__SCHED__*/', () => sched)
|
|
112
|
-
else if (tpl.includes('/*__SCHED__*/')) throw new Error(tplRel + ': __SCHED__ placeholder vanished after other replacements')
|
|
113
|
-
return out
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
for (const id of ids) {
|
|
117
|
-
if (ONLY && id !== ONLY) continue
|
|
118
|
-
const out = buildDynamicHost(id)
|
|
119
|
-
if (out) emit('dynamic/' + id + '.host.js', out)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ── 2. agent.cordis.yml ← plugin-list.mjs ─────────────────────────────────────
|
|
123
|
-
function yamlString(v) {
|
|
124
|
-
const s = String(v)
|
|
125
|
-
if (s === '' || /[:#\n'"\[\]{}&*!|>%@`]/.test(s) || /^\s|\s$/.test(s)) {
|
|
126
|
-
return "'" + s.replace(/'/g, "''") + "'"
|
|
127
|
-
}
|
|
128
|
-
return s
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function yamlEntry(entry, indent) {
|
|
132
|
-
// 递归序列化一个插件条目。字段序与官方 patch.yml 习惯一致:
|
|
133
|
-
// id → name → entry → group → isolate → disabled/if → config。
|
|
134
|
-
const pad = ' '.repeat(indent)
|
|
135
|
-
const lines = [pad + '- id: ' + yamlString(entry.id)]
|
|
136
|
-
if (entry.name !== undefined) lines.push(pad + ' name: ' + yamlString(entry.name))
|
|
137
|
-
if (entry.entry !== undefined) lines.push(pad + ' entry: ' + yamlString(entry.entry))
|
|
138
|
-
if (entry.group === true) lines.push(pad + ' group: true')
|
|
139
|
-
if (entry.isolate && typeof entry.isolate === 'object') {
|
|
140
|
-
lines.push(pad + ' isolate:')
|
|
141
|
-
for (const k of Object.keys(entry.isolate)) lines.push(pad + ' ' + k + ': ' + String(entry.isolate[k]))
|
|
142
|
-
}
|
|
143
|
-
for (const k of ['disabled', 'if']) {
|
|
144
|
-
if (entry[k] === undefined) continue
|
|
145
|
-
const v = entry[k]
|
|
146
|
-
if (v && typeof v === 'object' && typeof v.__jsExpr === 'string') lines.push(pad + ' ' + k + ": !!js '" + String(v.__jsExpr).replace(/'/g, "''") + "'")
|
|
147
|
-
else lines.push(pad + ' ' + k + ': ' + String(v))
|
|
148
|
-
}
|
|
149
|
-
if (entry.config !== undefined) {
|
|
150
|
-
if (Array.isArray(entry.config)) {
|
|
151
|
-
lines.push(pad + ' config:')
|
|
152
|
-
if (!entry.config.length) lines[lines.length - 1] = pad + ' config: []'
|
|
153
|
-
for (const child of entry.config) lines.push(yamlEntry(child, indent + 4))
|
|
154
|
-
} else if (entry.config && typeof entry.config === 'object') {
|
|
155
|
-
lines.push(yamlConfig(entry.config, indent + 2))
|
|
156
|
-
} else {
|
|
157
|
-
lines.push(pad + ' config: ' + yamlString(entry.config))
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return lines.join('\n')
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function yamlValue(v) {
|
|
164
|
-
// '' 返回值 = 「值是嵌套结构/多行文本,需换行展开」;否则返回 ' 标量' 后缀。
|
|
165
|
-
if (v && typeof v === 'object' && typeof v.__jsExpr === 'string') return " !!js '" + String(v.__jsExpr).replace(/'/g, "''") + "'"
|
|
166
|
-
if (Array.isArray(v)) return v.length ? '' : ' []'
|
|
167
|
-
if (v && typeof v === 'object') return ''
|
|
168
|
-
if (typeof v === 'string') { if (v.indexOf('\n') >= 0) return ''; return ' ' + yamlString(v) }
|
|
169
|
-
return ' ' + String(v)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// 多行字符串 → YAML 块标量('|' 保留结尾单换行;'|-' 裁掉)。行内引号折叠会改变
|
|
173
|
-
// 文本语义(plan-mode 段含逐字换行),必须用块标量原样落地。
|
|
174
|
-
function yamlBlockKey(key, v, keyPad, valPad) {
|
|
175
|
-
const clip = v.endsWith('\n') ? '|' : '|-'
|
|
176
|
-
const body = (clip === '|' ? v.replace(/\n$/, '') : v).split('\n')
|
|
177
|
-
const lines = [keyPad + key + ': ' + clip]
|
|
178
|
-
for (const l of body) lines.push(l === '' ? '' : valPad + l)
|
|
179
|
-
return lines
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function yamlConfig(config, indent) {
|
|
183
|
-
// indent:'config:' 键本身的列;子键 +2。
|
|
184
|
-
const pad = ' '.repeat(indent)
|
|
185
|
-
const lines = [pad + 'config:']
|
|
186
|
-
for (const k of Object.keys(config)) {
|
|
187
|
-
const v = config[k]
|
|
188
|
-
const inline = yamlValue(v)
|
|
189
|
-
if (inline !== '') { lines.push(pad + ' ' + k + ':' + inline); continue }
|
|
190
|
-
if (typeof v === 'string') { lines.push(...yamlBlockKey(k, v, pad + ' ', pad + ' ')); continue }
|
|
191
|
-
if (Array.isArray(v)) {
|
|
192
|
-
lines.push(pad + ' ' + k + ':')
|
|
193
|
-
for (const item of v) {
|
|
194
|
-
if (item && typeof item === 'object' && !item.__jsExpr) {
|
|
195
|
-
const keys = Object.keys(item)
|
|
196
|
-
lines.push(pad + ' - ' + keys[0] + ': ' + yamlString(item[keys[0]]))
|
|
197
|
-
for (let i = 1; i < keys.length; i++) lines.push(pad + ' ' + keys[i] + ': ' + yamlString(item[keys[i]]))
|
|
198
|
-
} else lines.push(pad + ' - ' + yamlString(item))
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
lines.push(pad + ' ' + k + ':')
|
|
202
|
-
lines.push(yamlInlineObject(v, indent + 4))
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return lines.join('\n')
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function yamlInlineObject(obj, indent) {
|
|
209
|
-
const pad = ' '.repeat(indent)
|
|
210
|
-
const lines = []
|
|
211
|
-
for (const k of Object.keys(obj)) {
|
|
212
|
-
const v = obj[k]
|
|
213
|
-
const inline = yamlValue(v)
|
|
214
|
-
if (inline !== '') lines.push(pad + k + ':' + inline)
|
|
215
|
-
else if (typeof v === 'string') lines.push(...yamlBlockKey(k, v, pad, pad + ' '))
|
|
216
|
-
else {
|
|
217
|
-
lines.push(pad + k + ':')
|
|
218
|
-
if (Array.isArray(v)) { for (const item of v) lines.push(pad + ' - ' + yamlString(item)) }
|
|
219
|
-
else lines.push(yamlInlineObject(v, indent + 2))
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return lines.join('\n')
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
'#
|
|
244
|
-
'#
|
|
245
|
-
'
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
emit('preset/' + dir + '/
|
|
250
|
-
|
|
251
|
-
'
|
|
252
|
-
'
|
|
253
|
-
'
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (MODE === 'check') console.log('[build] up to date')
|
|
1
|
+
// scripts/build.mjs — 派生产物生成器(keystone)。
|
|
2
|
+
//
|
|
3
|
+
// 生成物(勿手改;`--check` 模式漂移即失败,npm test 里的 build-sync 测试调用它):
|
|
4
|
+
// 1. dynamic/<id>.host.js ← dynamic/<id>.host.template.mjs
|
|
5
|
+
// 把 core/bridge-kit.mjs 内联进 /*__KIT__*/、providers/<id>.mjs 内联进
|
|
6
|
+
// /*__PROVIDER__*/、core/scheduling.mjs(+SCHED_ALL_IDS 常量)内联进
|
|
7
|
+
// /*__SCHED__*/(动态沙箱禁止 import,只能文本注入;单标记替换防递归错乱)。
|
|
8
|
+
// 注入时剥离各段的 `import ... from '...mjs'` 行并把 `export ` 关键字去掉。
|
|
9
|
+
// 2. preset/<id>/agent.cordis.yml ← preset/<id>/plugin-list.mjs(唯一权威源)
|
|
10
|
+
// 纯 JS 插件清单落地为 legacy 目录形态 YAML({__jsExpr} → !!js 块)。
|
|
11
|
+
// 两者同源生成,结构性消除「yml 与声明制漂移」这一历史风险。
|
|
12
|
+
// 3. home-plugin/cli-bridge-indicator/lib/preset-definitions/<id>.mjs
|
|
13
|
+
// ← 同上(声明制形态:插件数组直接给 agentPresets.register 消费)。
|
|
14
|
+
// ⚠️ 这一份由 preset-declarations.mjs 直接 import plugin-list,本脚本不再复制;
|
|
15
|
+
// 保留目录仅作兼容占位(旧版生成物清理)。
|
|
16
|
+
//
|
|
17
|
+
// 用法:node scripts/build.mjs [--check]
|
|
18
|
+
|
|
19
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync } from 'node:fs'
|
|
20
|
+
import { dirname, join, resolve } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
import { pathToFileURL } from 'node:url'
|
|
23
|
+
import { PROVIDER_IDS, PRESET_DIRS } from '../providers/registry.mjs'
|
|
24
|
+
|
|
25
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
26
|
+
// 单一 id 轴:新 provider 只改 registry(PROVIDER-CONTRACT.md §3.1),build 派生
|
|
27
|
+
// 物(dynamic 宿主 / SCHED_ALL_IDS 内联 / legacy YAML / plugin-list)自动跟上。
|
|
28
|
+
const ids = PROVIDER_IDS
|
|
29
|
+
const PKG = '@luchenglong/cli-bridge'
|
|
30
|
+
|
|
31
|
+
const MODE = process.argv.includes('--check') ? 'check' : 'write'
|
|
32
|
+
// --only=<id> 让单个 provider 的作者只生成/校验自己的派生物,避免并发写他人产物。
|
|
33
|
+
const onlyArg = process.argv.find((a) => a.startsWith('--only='))
|
|
34
|
+
const ONLY = onlyArg ? onlyArg.slice('--only='.length) : null
|
|
35
|
+
const drift = []
|
|
36
|
+
|
|
37
|
+
function read(rel) { return readFileSync(join(ROOT, rel), 'utf8') }
|
|
38
|
+
|
|
39
|
+
function emit(rel, content) {
|
|
40
|
+
const abs = join(ROOT, rel)
|
|
41
|
+
if (MODE === 'check') {
|
|
42
|
+
const cur = existsSync(abs) ? readFileSync(abs, 'utf8') : null
|
|
43
|
+
if (cur !== content) drift.push(rel)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
mkdirSync(dirname(abs), { recursive: true })
|
|
47
|
+
writeFileSync(abs, content, 'utf8')
|
|
48
|
+
console.log('generated ' + rel)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── 1. dynamic host(文本内联,沙箱禁 import)──────────────────────────────────
|
|
52
|
+
function stripModuleSyntax(src, label) {
|
|
53
|
+
// 多行 import(`import {\n a, b\n} from '...'`)在单行过滤下会残留 `import ` 前缀并
|
|
54
|
+
// 触发下面的守卫误报 —— 先把跨行 import 折叠成一行再过滤(与单行同规则)。
|
|
55
|
+
const collapsed = src.replace(
|
|
56
|
+
/^\s*import\s*\{[^}]*\}\s*from\s*['"][^'"]+['"];?\s*$/gms,
|
|
57
|
+
(m) => '// ' + m.replace(/\s+/g, ' ').trim()
|
|
58
|
+
)
|
|
59
|
+
const out = collapsed
|
|
60
|
+
.split('\n')
|
|
61
|
+
.filter((l) => !/^\s*import\s[^(]*from\s*['"][^'"]+['"];?\s*$/.test(l) && !/^\s*import\s*['"]/.test(l))
|
|
62
|
+
.join('\n')
|
|
63
|
+
// 缩进形态也要剥:`export function f()` 可能带前导空格(从 descriptor 闭包里
|
|
64
|
+
// 搬出来的函数保留原缩进),只匹配行首会漏掉 → 残留 export 在脚本体里直接语法错。
|
|
65
|
+
.replace(/^(\s*)export\s+(const|let|var|function|async function|class)\s/gm, '$1$2 ')
|
|
66
|
+
.replace(/^(\s*)export\s+default\s+(async\s+)?function\s+([A-Za-z0-9_$]+)\s*/gm, '$1$2function $3 ')
|
|
67
|
+
.replace(/^(\s*)export\s+default\s+(async\s+)?function\s*\(/gm, '$1$2function __default__(')
|
|
68
|
+
.replace(/^(\s*)export\s*\{[^}]*\};?\s*$/gm, '')
|
|
69
|
+
// 内联目标是「脚本体」:残留的 export default/import 说明源用了本转换器不支持
|
|
70
|
+
// 的形态。静默产出坏代码会让 dynamic host 在沙箱里炸得莫名其妙,必须大声失败。
|
|
71
|
+
if (/^\s*(export\s+default|import\s|import\()/m.test(out)) {
|
|
72
|
+
throw new Error('build: ' + (label || 'source') + ' 用了沙箱内联不支持的模块形态(provider 需具名 `export default function defineProvider(...)`;bridge-kit 不得 import)')
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildDynamicHost(id) {
|
|
78
|
+
const tplRel = 'dynamic/' + id + '.host.template.mjs'
|
|
79
|
+
if (!existsSync(join(ROOT, tplRel))) return null
|
|
80
|
+
const tpl = read(tplRel)
|
|
81
|
+
const kit = stripModuleSyntax(read('core/bridge-kit.mjs'), 'core/bridge-kit.mjs')
|
|
82
|
+
const prov = stripModuleSyntax(read('providers/' + id + '.mjs'), 'providers/' + id + '.mjs')
|
|
83
|
+
// 调度单源(0.2.0):core/scheduling.mjs 无 import,可安全内联进沙箱作用域。
|
|
84
|
+
// SCHED_ALL_IDS 由构建注入(全量 provider 清单)—— 沙箱禁 import,拿不到 registry。
|
|
85
|
+
// 模板没有 /*__SCHED__*/ 占位(旧模板)→ 不注入,保持向后兼容。
|
|
86
|
+
const sched = stripModuleSyntax(read('core/scheduling.mjs'), 'core/scheduling.mjs')
|
|
87
|
+
+ '\nconst SCHED_ALL_IDS = ' + JSON.stringify(ids) + '\n'
|
|
88
|
+
// 可选的多段内联:providers/<id>-*.mjs(如 <id>-catalog.mjs、<id>-models-tool.mjs)。
|
|
89
|
+
// 语义与 __SCHED__ 同构(模板没有该占位符 → 不注入,向后兼容)。
|
|
90
|
+
// 目的:providers/<id>.mjs 超 600 行红线时把纯逻辑拆出去,沙箱里仍是**单脚本**。
|
|
91
|
+
// 支持多个片文件(按文件名字典序拼接)而不是写死一个:每撞一次 600 行红线就改一次
|
|
92
|
+
// 构建器的做法不可持续,泛用性与顺序确定性都要有。
|
|
93
|
+
const extraFiles = readdirSync(join(ROOT, 'providers'))
|
|
94
|
+
.filter((f) => f.startsWith(id + '-') && f.endsWith('.mjs'))
|
|
95
|
+
.sort()
|
|
96
|
+
const extra = extraFiles.length
|
|
97
|
+
? extraFiles.map((f) => {
|
|
98
|
+
const rel = 'providers/' + f
|
|
99
|
+
return '// ── inlined from ' + rel + ' ──\n' + stripModuleSyntax(read(rel), rel)
|
|
100
|
+
}).join('\n')
|
|
101
|
+
: null
|
|
102
|
+
if (!tpl.includes('/*__KIT__*/') || !tpl.includes('/*__PROVIDER__*/')) {
|
|
103
|
+
throw new Error(tplRel + ' missing /*__KIT__*/ or /*__PROVIDER__*/ placeholder')
|
|
104
|
+
}
|
|
105
|
+
let out = tpl.replace('/*__KIT__*/', () => kit).replace('/*__PROVIDER__*/', () => prov)
|
|
106
|
+
// __EXTRA__ 放在 KIT/PROVIDER 之后替换:避免其内容里若含同名占位符被二次展开。
|
|
107
|
+
if (out.includes('/*__EXTRA__*/')) {
|
|
108
|
+
if (!extra) throw new Error(tplRel + ': __EXTRA__ placeholder present but no providers/' + id + '-*.mjs fragment found')
|
|
109
|
+
out = out.replace('/*__EXTRA__*/', () => extra)
|
|
110
|
+
} else if (tpl.includes('/*__EXTRA__*/')) throw new Error(tplRel + ': __EXTRA__ placeholder vanished after other replacements')
|
|
111
|
+
if (out.includes('/*__SCHED__*/')) out = out.replace('/*__SCHED__*/', () => sched)
|
|
112
|
+
else if (tpl.includes('/*__SCHED__*/')) throw new Error(tplRel + ': __SCHED__ placeholder vanished after other replacements')
|
|
113
|
+
return out
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const id of ids) {
|
|
117
|
+
if (ONLY && id !== ONLY) continue
|
|
118
|
+
const out = buildDynamicHost(id)
|
|
119
|
+
if (out) emit('dynamic/' + id + '.host.js', out)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── 2. agent.cordis.yml ← plugin-list.mjs ─────────────────────────────────────
|
|
123
|
+
function yamlString(v) {
|
|
124
|
+
const s = String(v)
|
|
125
|
+
if (s === '' || /[:#\n'"\[\]{}&*!|>%@`]/.test(s) || /^\s|\s$/.test(s)) {
|
|
126
|
+
return "'" + s.replace(/'/g, "''") + "'"
|
|
127
|
+
}
|
|
128
|
+
return s
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function yamlEntry(entry, indent) {
|
|
132
|
+
// 递归序列化一个插件条目。字段序与官方 patch.yml 习惯一致:
|
|
133
|
+
// id → name → entry → group → isolate → disabled/if → config。
|
|
134
|
+
const pad = ' '.repeat(indent)
|
|
135
|
+
const lines = [pad + '- id: ' + yamlString(entry.id)]
|
|
136
|
+
if (entry.name !== undefined) lines.push(pad + ' name: ' + yamlString(entry.name))
|
|
137
|
+
if (entry.entry !== undefined) lines.push(pad + ' entry: ' + yamlString(entry.entry))
|
|
138
|
+
if (entry.group === true) lines.push(pad + ' group: true')
|
|
139
|
+
if (entry.isolate && typeof entry.isolate === 'object') {
|
|
140
|
+
lines.push(pad + ' isolate:')
|
|
141
|
+
for (const k of Object.keys(entry.isolate)) lines.push(pad + ' ' + k + ': ' + String(entry.isolate[k]))
|
|
142
|
+
}
|
|
143
|
+
for (const k of ['disabled', 'if']) {
|
|
144
|
+
if (entry[k] === undefined) continue
|
|
145
|
+
const v = entry[k]
|
|
146
|
+
if (v && typeof v === 'object' && typeof v.__jsExpr === 'string') lines.push(pad + ' ' + k + ": !!js '" + String(v.__jsExpr).replace(/'/g, "''") + "'")
|
|
147
|
+
else lines.push(pad + ' ' + k + ': ' + String(v))
|
|
148
|
+
}
|
|
149
|
+
if (entry.config !== undefined) {
|
|
150
|
+
if (Array.isArray(entry.config)) {
|
|
151
|
+
lines.push(pad + ' config:')
|
|
152
|
+
if (!entry.config.length) lines[lines.length - 1] = pad + ' config: []'
|
|
153
|
+
for (const child of entry.config) lines.push(yamlEntry(child, indent + 4))
|
|
154
|
+
} else if (entry.config && typeof entry.config === 'object') {
|
|
155
|
+
lines.push(yamlConfig(entry.config, indent + 2))
|
|
156
|
+
} else {
|
|
157
|
+
lines.push(pad + ' config: ' + yamlString(entry.config))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return lines.join('\n')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function yamlValue(v) {
|
|
164
|
+
// '' 返回值 = 「值是嵌套结构/多行文本,需换行展开」;否则返回 ' 标量' 后缀。
|
|
165
|
+
if (v && typeof v === 'object' && typeof v.__jsExpr === 'string') return " !!js '" + String(v.__jsExpr).replace(/'/g, "''") + "'"
|
|
166
|
+
if (Array.isArray(v)) return v.length ? '' : ' []'
|
|
167
|
+
if (v && typeof v === 'object') return ''
|
|
168
|
+
if (typeof v === 'string') { if (v.indexOf('\n') >= 0) return ''; return ' ' + yamlString(v) }
|
|
169
|
+
return ' ' + String(v)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 多行字符串 → YAML 块标量('|' 保留结尾单换行;'|-' 裁掉)。行内引号折叠会改变
|
|
173
|
+
// 文本语义(plan-mode 段含逐字换行),必须用块标量原样落地。
|
|
174
|
+
function yamlBlockKey(key, v, keyPad, valPad) {
|
|
175
|
+
const clip = v.endsWith('\n') ? '|' : '|-'
|
|
176
|
+
const body = (clip === '|' ? v.replace(/\n$/, '') : v).split('\n')
|
|
177
|
+
const lines = [keyPad + key + ': ' + clip]
|
|
178
|
+
for (const l of body) lines.push(l === '' ? '' : valPad + l)
|
|
179
|
+
return lines
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function yamlConfig(config, indent) {
|
|
183
|
+
// indent:'config:' 键本身的列;子键 +2。
|
|
184
|
+
const pad = ' '.repeat(indent)
|
|
185
|
+
const lines = [pad + 'config:']
|
|
186
|
+
for (const k of Object.keys(config)) {
|
|
187
|
+
const v = config[k]
|
|
188
|
+
const inline = yamlValue(v)
|
|
189
|
+
if (inline !== '') { lines.push(pad + ' ' + k + ':' + inline); continue }
|
|
190
|
+
if (typeof v === 'string') { lines.push(...yamlBlockKey(k, v, pad + ' ', pad + ' ')); continue }
|
|
191
|
+
if (Array.isArray(v)) {
|
|
192
|
+
lines.push(pad + ' ' + k + ':')
|
|
193
|
+
for (const item of v) {
|
|
194
|
+
if (item && typeof item === 'object' && !item.__jsExpr) {
|
|
195
|
+
const keys = Object.keys(item)
|
|
196
|
+
lines.push(pad + ' - ' + keys[0] + ': ' + yamlString(item[keys[0]]))
|
|
197
|
+
for (let i = 1; i < keys.length; i++) lines.push(pad + ' ' + keys[i] + ': ' + yamlString(item[keys[i]]))
|
|
198
|
+
} else lines.push(pad + ' - ' + yamlString(item))
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
lines.push(pad + ' ' + k + ':')
|
|
202
|
+
lines.push(yamlInlineObject(v, indent + 4))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return lines.join('\n')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function yamlInlineObject(obj, indent) {
|
|
209
|
+
const pad = ' '.repeat(indent)
|
|
210
|
+
const lines = []
|
|
211
|
+
for (const k of Object.keys(obj)) {
|
|
212
|
+
const v = obj[k]
|
|
213
|
+
const inline = yamlValue(v)
|
|
214
|
+
if (inline !== '') lines.push(pad + k + ':' + inline)
|
|
215
|
+
else if (typeof v === 'string') lines.push(...yamlBlockKey(k, v, pad, pad + ' '))
|
|
216
|
+
else {
|
|
217
|
+
lines.push(pad + k + ':')
|
|
218
|
+
if (Array.isArray(v)) { for (const item of v) lines.push(pad + ' - ' + yamlString(item)) }
|
|
219
|
+
else lines.push(yamlInlineObject(v, indent + 2))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return lines.join('\n')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async function loadPluginList(id) {
|
|
227
|
+
const rel = 'preset/' + PRESET_DIRS[id] + '/plugin-list.mjs'
|
|
228
|
+
const abs = join(ROOT, rel)
|
|
229
|
+
if (!existsSync(abs)) return null
|
|
230
|
+
const mod = await import(pathToFileURL(abs).href)
|
|
231
|
+
return mod
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const id of ids) {
|
|
235
|
+
if (ONLY && id !== ONLY) continue
|
|
236
|
+
const mod = await loadPluginList(id)
|
|
237
|
+
if (!mod || typeof mod.buildPlugins !== 'function') continue
|
|
238
|
+
const dir = PRESET_DIRS[id]
|
|
239
|
+
const meta = mod.meta || {}
|
|
240
|
+
const plugins = mod.buildPlugins({ bridgeName: PKG + '/preset-' + id })
|
|
241
|
+
const head = [
|
|
242
|
+
'# 生成物 — 由 node scripts/build.mjs 从 preset/' + dir + '/plugin-list.mjs 生成,勿手改。',
|
|
243
|
+
'# legacy 目录形态($DSH_HOME/.agent-presets/' + meta.id + '/);新 DSH 走声明制',
|
|
244
|
+
'# (home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs 直接 import 同一源)。',
|
|
245
|
+
''
|
|
246
|
+
].join('\n')
|
|
247
|
+
const body = plugins.map((e) => yamlEntry(e, 0)).join('\n')
|
|
248
|
+
emit('preset/' + dir + '/agent.cordis.yml', head + body + '\n')
|
|
249
|
+
emit('preset/' + dir + '/preset.yml', [
|
|
250
|
+
'name: ' + yamlString(meta.name || meta.id),
|
|
251
|
+
'description: ' + yamlString(meta.description || ''),
|
|
252
|
+
'order: ' + String(meta.order != null ? meta.order : 10),
|
|
253
|
+
''
|
|
254
|
+
].join('\n'))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (drift.length) {
|
|
258
|
+
console.error('[build] drift detected: ' + drift.join(', ') + ' — run `node scripts/build.mjs`')
|
|
259
|
+
process.exit(1)
|
|
260
|
+
}
|
|
261
|
+
if (MODE === 'check') console.log('[build] up to date')
|
package/scripts/verify.mjs
CHANGED
|
@@ -54,7 +54,7 @@ for (const [spec, rel] of Object.entries(pkg.exports || {})) {
|
|
|
54
54
|
ok.push('exports targets exist (' + Object.keys(pkg.exports || {}).length + ')')
|
|
55
55
|
|
|
56
56
|
// 4. preset 桥接说明符
|
|
57
|
-
const { PROVIDER_IDS, createProvider } = await import(pathToFileURL(join(ROOT, 'providers', 'registry.mjs')).href)
|
|
57
|
+
const { PROVIDER_IDS, PRESET_DIRS, createProvider } = await import(pathToFileURL(join(ROOT, 'providers', 'registry.mjs')).href)
|
|
58
58
|
for (const id of PROVIDER_IDS) {
|
|
59
59
|
const exportKey = './preset-' + id
|
|
60
60
|
if (!pkg.exports || !pkg.exports[exportKey]) errors.push('package.json exports[' + exportKey + '] missing (preset bridge row needs it)')
|
|
@@ -81,8 +81,14 @@ const patchRel = pkg.dsh && pkg.dsh.bundle && pkg.dsh.bundle.patch
|
|
|
81
81
|
if (!patchRel || !existsSync(join(ROOT, String(patchRel).replace(/^\.\//, '')))) errors.push('package.json dsh.bundle.patch target missing')
|
|
82
82
|
|
|
83
83
|
// 7. 生成物存在(dynamic host / agent yml 至少 plugin-list 在;fresh clone 需先 build)
|
|
84
|
+
// ⚠ 目录映射单一来源:`providers/registry.mjs` 的 PRESET_DIRS。此处曾自带一份
|
|
85
|
+
// 三处重复的硬编码映射(build.mjs / preset-declarations.mjs / 本文件各一份),
|
|
86
|
+
// 新增 provider 时漏改本文件就会报「plugin-list missing」这种误导性错误
|
|
87
|
+
// (claude/codex 接入时实际踩到)。统一到一处后再加第 N 家 provider 零改动。
|
|
84
88
|
for (const id of PROVIDER_IDS) {
|
|
85
|
-
|
|
89
|
+
const dir = PRESET_DIRS[id]
|
|
90
|
+
if (!dir) { errors.push('provider ' + id + ': no PRESET_DIRS entry in providers/registry.mjs'); continue }
|
|
91
|
+
if (!read('preset/' + dir + '/plugin-list.mjs')) errors.push('preset/' + id + ' plugin-list.mjs missing (expected preset/' + dir + '/plugin-list.mjs)')
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
// 8. 版本双锁:mcp server const + CHANGELOG 顶部条目 === package.json version
|