dsh-vibe-math 2.0.21 → 2.1.0

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/installer.js CHANGED
@@ -1,290 +1,315 @@
1
- // dsh-vibe-math merged bundle installer — VERSIONED AUTO-UPDATE.
2
- // When this bundle is installed (e.g. `dsh plugin add dsh-vibe-math` or from the
3
- // dsh-market), this plugin copies ALL THREE agent presets out of the package into
4
- // the DSH preset root, so the user immediately gets three presets in the picker:
5
- // vibe-math-v2/ (probability-driven architecture)
6
- // vibe-math-v3/ (THIRD-generation: paper-style Markdown knowledge base +
7
- // planner-agent scheduling + universal theory/method library)
8
- // vibe-math-v4/ (FOURTH-generation: persistent self-organizing resident
9
- // subagents — message bus / meetings / unanimous-consensus
10
- // verification / per-resident libraries)
11
- //
12
- // (vibe-math-v1 — the classic pipeline — was removed at v2.0.0; this bundle now
13
- // ships v2/v3/v4 only.)
14
- //
15
- // UPDATE POLICY (state recorded in <presetRoot>/.vibe-math-installed.json):
16
- // - baseline (no state file — e.g. upgrading from an installer that predates
17
- // this mechanism): every existing owned file is refreshed to the current
18
- // package version and recorded as package-owned (user policy: auto-update
19
- // old installs; any manual edits made before this baseline are overwritten
20
- // once — from then on edits are protected).
21
- // - upgrade (recorded version != current package.json version): every owned
22
- // file that is byte-identical to the previously installed copy (i.e. NOT
23
- // user-edited since) is overwritten with the new version; user-edited files
24
- // are preserved and reported via the logger.
25
- // - same version: no-op (idempotent). Missing files are ALWAYS restored.
26
- // - force a full refresh at any time: delete the preset dirs and restart DSH.
27
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, unlinkSync, rmdirSync } from 'node:fs'
28
- import { createRequire } from 'node:module'
29
- import { createHash } from 'node:crypto'
30
- import { homedir } from 'node:os'
31
- import { dirname, join } from 'node:path'
32
- import { fileURLToPath } from 'node:url'
33
-
34
- export const name = 'vibe-math-preset-installer'
35
-
36
- const PRESETS = [
37
- {
38
- src: 'vibe-math-v2',
39
- dst: 'vibe-math-v2',
40
- files: ['agent.cordis.yml', 'preset.yml', 'vibe-math-v2.js', '实现方案.md'],
41
- },
42
- {
43
- src: 'vibe-math-v3',
44
- dst: 'vibe-math-v3',
45
- files: ['agent.cordis.yml', 'preset.yml', 'vibe-math-v3.js', '实现方案.md'],
46
- },
47
- {
48
- src: 'vibe-math-v4',
49
- dst: 'vibe-math-v4',
50
- files: ['agent.cordis.yml', 'preset.yml', 'vibe-math-v4.js', '实现方案.md'],
51
- },
52
- ]
53
-
54
- const STATE_FILE = '.vibe-math-installed.json'
55
-
56
- function sha256(buf) { return createHash('sha256').update(buf).digest('hex') }
57
-
58
- function readState(path) {
59
- try {
60
- const raw = readFileSync(path, 'utf8')
61
- const obj = JSON.parse(raw)
62
- if (obj && typeof obj === 'object' && obj.files && typeof obj.files === 'object') return obj
63
- } catch (e) { /* missing or corrupt — treat as no state (baseline) */ }
64
- return null
65
- }
66
-
67
- function writeState(path, state) {
68
- try {
69
- const tmp = path + '.tmp'
70
- writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8')
71
- renameSync(tmp, path)
72
- } catch (e) {
73
- // best-effort: state persistence failure must not break the copy step
74
- }
75
- }
76
-
77
- // DSH 适配性自检(能力检测,而非版本号——DSH 不向插件暴露版本)。
78
- // 检查 preset 运行时需要的宿主服务与关键 API 形状是否可用,缺失时打 warning。
79
- // Best-effort DSH host-version detection. DSH does NOT expose its version through a documented
80
- // service/context property or a guaranteed env var, so we probe in order: an explicit env var
81
- // (future-proofing), then the installed @deepseek-ai/dsh package.json. This is layout-dependent
82
- // (works for a typical global install where @deepseek-ai/dsh is a sibling of this plugin); when it
83
- // cannot resolve, the capability self-check below is still the authoritative gate.
84
- const __require = createRequire(import.meta.url)
85
- function detectDshVersion() {
86
- try { const v = process.env.DSH_VERSION; if (v && String(v).trim()) return String(v).trim() } catch (e) {}
87
- try {
88
- const p = __require.resolve('@deepseek-ai/dsh/package.json')
89
- const v = (JSON.parse(readFileSync(p, 'utf8')).version || '').trim()
90
- if (v) return v
91
- } catch (e) { /* host package not resolvable from here — rely on capability check */ }
92
- return undefined
93
- }
94
-
95
- async function checkHostCapabilities(ctx, logger) {
96
- const problems = []
97
- // 1) DSH version compatibility (best-effort, only when the version is detectable).
98
- // Declared under package.json dsh.compatibility.dshReleases (per the DSH STORE contract):
99
- // each full DSH release maps to 'compatible' | 'incompatible' | 'unknown'. A version that is
100
- // absent or 'unknown' is a soft warning; 'incompatible' is a hard "please use X" message.
101
- let dshRel = {}
102
- try { const m = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')); dshRel = (m.dsh && m.dsh.compatibility && m.dsh.compatibility.dshReleases) || {} } catch (e) {}
103
- const supported = Object.keys(dshRel).sort()
104
- const dshVersion = detectDshVersion()
105
- if (dshVersion) {
106
- const status = dshRel[dshVersion]
107
- if (status === 'incompatible') {
108
- problems.push('当前 DSH 版本 v' + dshVersion + ' 被本包声明为 incompatible;请使用 ' + supported.join(' / ') + '。')
109
- } else if (status === undefined || status === 'unknown') {
110
- problems.push('当前 DSH 版本 v' + dshVersion + ' 尚未被本包声明为兼容(dshReleases 仅声明 ' + supported.join(' / ') + ');建议使用 ' + supported.join(' / ') + ',或将该版本在 dshReleases 中标注后再自行验证。')
111
- }
112
- }
113
- // 2) capability self-check (the authoritative mounting gate; also covers hosts whose version
114
- // could not be read). subagents / agents / tools / commands / fs shapes + v4 capabilities.
115
- const checks = [
116
- // subagents 服务的续做/唤醒方法是 sendMessage(sender, targetId, content, {signal});
117
- // followup 不是 subagents 服务的方法(它只是 Agent 对象方法)。同时探测两者,能用一个即可。
118
- ['subagents', ['startContinuable', 'interrupt']],
119
- ['agents', ['roots']],
120
- ['tools', ['register']],
121
- ['commands', ['register']],
122
- ['fs', ['resolve', 'stat', 'readText', 'writeText', 'listDir']],
123
- ]
124
- for (let i = 0; i < checks.length; i++) {
125
- const svc = checks[i][0]
126
- const methods = checks[i][1]
127
- let s
128
- try { s = (ctx && ctx.get) ? ctx.get(svc) : undefined } catch (e) { s = undefined }
129
- if (s === undefined) { problems.push('宿主缺少服务 ' + svc); continue }
130
- for (let j = 0; j < methods.length; j++) {
131
- if (typeof s[methods[j]] !== 'function') problems.push(svc + '.' + methods[j] + ' 不可用(宿主版本可能过旧)')
132
- }
133
- // subagents continuation (wake) API: sendMessage (modern) OR followup (legacy) must exist.
134
- if (svc === 'subagents' && typeof s.sendMessage !== 'function' && typeof s.followup !== 'function') {
135
- problems.push('subagents 缺少续做/唤醒方法(需 sendMessage 或 followup 至少其一)')
136
- }
137
- }
138
- // fs API shape: DSH 0.1.1 起 resolve 返回 {targetKey, displayPath} 对象(旧版返回字符串路径)
139
- try {
140
- const f = (ctx && ctx.get) ? ctx.get('fs') : undefined
141
- if (f && typeof f.resolve === 'function') {
142
- const r = await f.resolve('x', { cwd: process.cwd() })
143
- if (typeof r !== 'object' || r === null || typeof r.targetKey !== 'string') {
144
- problems.push('fs.resolve 返回形状不符(期望 {targetKey, displayPath},v3/v4 预设要求 DSH ≥ 0.1.1)')
145
- }
146
- }
147
- } catch (e) { problems.push('fs.resolve 能力检测失败:' + String((e && e.message) || e)) }
148
- // v4 依赖 subagents.startContinuable 的 agentOptions / toolFilter 能力(DSH 0.1.2 起由
149
- // dsh-subagent 声明 SubagentCapabilities.agentOptions;spawn/fork 进程内 provider 均支持。
150
- // 缺省 provider 名按 spawn 探测;探测失败不视为致命(等价于回退到再试一次、只警告)。
151
- try {
152
- const sa = (ctx && ctx.get) ? ctx.get('subagents') : undefined
153
- if (sa && typeof sa.list === 'function') {
154
- const names = (sa.list ? sa.list() : [])
155
- const name = names.indexOf('spawn') !== -1 ? 'spawn' : (names[0] || '')
156
- if (name && typeof sa.getProvider === 'function') {
157
- const cap = (sa.getProvider(name) || {}).capabilities
158
- if (cap && cap.agentOptions === false) problems.push('subagents provider "' + name + '" 不支持 agentOptions(v4 指定常驻模型/路由需要)')
159
- if (cap && cap.toolFilter === false) problems.push('subagents provider "' + name + '" 不支持 toolFilter(v4 常驻工具权限需要)')
160
- }
161
- }
162
- } catch (e) { /* 探测失败不致命 */ }
163
- if (problems.length > 0) {
164
- logger?.warn?.('[dsh-vibe-math] 宿主自检:' + problems.length + ' 项不满足(' + problems.join(';') + ')。v2/v3/v4 预设依赖这些宿主服务/API,旧版或未经声明兼容的 DSH 可能无法挂载' + (dshVersion ? '(当前检测到 DSH v' + dshVersion + ',本包适配 ' + (supported.length ? supported.join(' / ') : '(未声明)') + ')' : '') + '。')
165
- } else {
166
- logger?.info?.('[dsh-vibe-math] 宿主自检通过:subagents / agents / tools / commands / fs 服务及关键 API 均可用' + (dshVersion ? '(当前 DSH v' + dshVersion + ',本包已声明兼容 ' + supported.join(' / ') + ')' : '') + '。')
167
- }
168
- }
169
-
170
- export async function apply(ctx) {
171
- const logger = ctx && ctx.logger
172
- try {
173
- const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh')
174
- const here = dirname(fileURLToPath(import.meta.url))
175
- const presetRoot = join(dshHome, '.agent-presets')
176
- const stateFile = join(presetRoot, STATE_FILE)
177
-
178
- // current package version (the source of truth for "is this an upgrade?")
179
- let pkgVersion = ''
180
- try { pkgVersion = String((JSON.parse(readFileSync(join(here, 'package.json'), 'utf8')).version) || '') } catch (e) { pkgVersion = '' }
181
-
182
- const state = readState(stateFile)
183
- const prevFiles = (state && state.files) || {}
184
- const isUpgrade = state !== null && pkgVersion !== '' && state.version !== pkgVersion
185
- const isBaseline = state === null // no recorded history → refresh everything (user policy: auto-update old installs)
186
-
187
- const nextFiles = {}
188
- let installed = 0, updated = 0, kept = 0
189
- const keptList = []
190
-
191
- for (const p of PRESETS) {
192
- const srcDir = join(here, p.src)
193
- const dstDir = join(presetRoot, p.dst)
194
- if (!existsSync(srcDir)) continue
195
- mkdirSync(dstDir, { recursive: true })
196
- for (const f of p.files) {
197
- const s = join(srcDir, f)
198
- const d = join(dstDir, f)
199
- if (!existsSync(s)) continue
200
- const key = p.src + '/' + f
201
- const cur = readFileSync(s)
202
- const curHash = sha256(cur)
203
- if (!existsSync(d)) {
204
- // missing file: always restore, whatever the version
205
- writeFileSync(d, cur)
206
- installed += 1
207
- nextFiles[key] = { hash: curHash, provenance: 'package' }
208
- continue
209
- }
210
- const destHash = sha256(readFileSync(d))
211
- if (isBaseline) {
212
- // no recorded history: refresh to the current package (one-time; edits
213
- // made before this mechanism are overwritten, later edits are protected)
214
- if (destHash === curHash) { nextFiles[key] = { hash: curHash, provenance: 'package' } }
215
- else { writeFileSync(d, cur); updated += 1; nextFiles[key] = { hash: curHash, provenance: 'package' } }
216
- continue
217
- }
218
- const prev = prevFiles[key]
219
- const prevRec = (prev && typeof prev === 'object') ? prev : { hash: prev, provenance: 'package' }
220
- const prevProv = (prevRec.provenance === 'user') ? 'user' : 'package' // 未知来源按包文件处理
221
- if (prevProv === 'package' && destHash === prevRec.hash) {
222
- // 包文件且未被改动 → 可安全升级(内容相同则跳过写入)
223
- if (destHash !== curHash) { writeFileSync(d, cur); updated += 1 }
224
- nextFiles[key] = { hash: curHash, provenance: 'package' }
225
- } else if (prevProv === 'user') {
226
- // 用户持有 → 永不覆盖
227
- kept += 1
228
- if (isUpgrade) keptList.push(key + ' (用户持有)')
229
- nextFiles[key] = { hash: destHash, provenance: 'user' }
230
- } else {
231
- // 包文件但自上次安装后已被用户改动
232
- kept += 1
233
- if (isUpgrade) keptList.push(key + ' (已修改)')
234
- nextFiles[key] = { hash: destHash, provenance: 'user' }
235
- }
236
- }
237
- }
238
-
239
- // Clean up preset dirs that this bundle NO LONGER manages (e.g. vibe-math-v1 after it was
240
- // removed at v2.0.0). The copy loop only adds/updates PRESETS; it never deletes a preset that
241
- // was dropped, so an old removed preset would linger in the picker forever. Here we remove the
242
- // files this installer previously recorded as package-owned under a prefix that is no longer in
243
- // PRESETS, then drop the dir if it became empty. User-owned files (provenance 'user') are kept.
244
- const currentPrefixes = new Set(PRESETS.map(p => p.src + '/'))
245
- let removedFiles = 0
246
- let removedDirs = []
247
- const stale = new Map() // prefix -> [keys]
248
- for (const key of Object.keys(prevFiles)) {
249
- const slash = key.indexOf('/')
250
- if (slash === -1) continue
251
- const prefix = key.slice(0, slash + 1)
252
- if (currentPrefixes.has(prefix)) continue
253
- if (!stale.has(prefix)) stale.set(prefix, [])
254
- stale.get(prefix).push(key)
255
- }
256
- for (const [prefix, keys] of stale) {
257
- let dirEmpty = true
258
- for (const key of keys) {
259
- const rec = (prevFiles[key] && typeof prevFiles[key] === 'object') ? prevFiles[key] : { provenance: 'package' }
260
- if (rec.provenance === 'user') { dirEmpty = false; continue } // 用户文件 → 保留
261
- const f = join(presetRoot, key)
262
- if (existsSync(f)) { try { unlinkSync(f); removedFiles += 1 } catch (e) {} }
263
- if (existsSync(f)) dirEmpty = false
264
- }
265
- const dir = join(presetRoot, prefix.slice(0, -1))
266
- if (dirEmpty && existsSync(dir)) { try { rmdirSync(dir); removedDirs.push(dir) } catch (e) {} }
267
- }
268
-
269
- writeState(stateFile, { version: pkgVersion, files: nextFiles, updatedAt: Date.now() })
270
-
271
- if (removedFiles > 0 || removedDirs.length > 0) {
272
- logger?.info?.('[dsh-vibe-math] preset cleanup: removed ' + removedFiles + ' file(s) from ' + removedDirs.length + ' stale preset dir(s) (' + removedDirs.map(d => d.split(/[\\/]/).pop()).join(', ') + ') that are no longer shipped.')
273
- }
274
-
275
- if (isUpgrade) {
276
- logger?.info?.('[dsh-vibe-math] preset auto-update: version ' + (state.version || '(none)') + ' → ' + pkgVersion +
277
- ' — 新增 ' + installed + ' 个文件,更新 ' + updated + ' 个文件' +
278
- (kept > 0 ? ',保留 ' + kept + ' 个未覆盖文件(' + keptList.join('; ') + ')' : '') +
279
- '。新版本 preset 将在新会话生效。')
280
- } else if (isBaseline) {
281
- logger?.info?.('[dsh-vibe-math] preset baseline: refreshed ' + (installed + updated) + ' file(s) to v' + pkgVersion +
282
- ' — 已启用自动更新(后续版本升级将自动替换未被手动修改的 preset 文件)。')
283
- } else if (installed > 0) {
284
- logger?.info?.('[dsh-vibe-math] restored ' + installed + ' missing preset file(s)')
285
- }
286
- await checkHostCapabilities(ctx, logger)
287
- } catch (err) {
288
- logger?.warn?.('[dsh-vibe-math] preset install/update failed: %s', String((err && err.message) || err))
289
- }
290
- }
1
+ // dsh-vibe-math merged bundle installer — VERSIONED AUTO-UPDATE.
2
+ // When this bundle is installed (e.g. `dsh plugin add dsh-vibe-math` or from the
3
+ // dsh-market), this plugin copies ALL THREE agent presets out of the package into
4
+ // the DSH preset root, so the user immediately gets three presets in the picker:
5
+ // vibe-math-v2/ (probability-driven architecture)
6
+ // vibe-math-v3/ (THIRD-generation: paper-style Markdown knowledge base +
7
+ // planner-agent scheduling + universal theory/method library)
8
+ // vibe-math-v4/ (FOURTH-generation: persistent self-organizing resident
9
+ // subagents — message bus / meetings / unanimous-consensus
10
+ // verification / per-resident libraries)
11
+ //
12
+ // (vibe-math-v1 — the classic pipeline — was removed at v2.0.0; this bundle now
13
+ // ships v2/v3/v4 only.)
14
+ //
15
+ // UPDATE POLICY (state recorded in <presetRoot>/.vibe-math-installed.json):
16
+ // - baseline (no state file — e.g. upgrading from an installer that predates
17
+ // this mechanism): every existing owned file is refreshed to the current
18
+ // package version and recorded as package-owned (user policy: auto-update
19
+ // old installs; any manual edits made before this baseline are overwritten
20
+ // once — from then on edits are protected).
21
+ // - upgrade (recorded version != current package.json version): every owned
22
+ // file that is byte-identical to the previously installed copy (i.e. NOT
23
+ // user-edited since) is overwritten with the new version; user-edited files
24
+ // are preserved and reported via the logger.
25
+ // - same version: no-op (idempotent). Missing files are ALWAYS restored.
26
+ // - force a full refresh at any time: delete the preset dirs and restart DSH.
27
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, unlinkSync, rmdirSync } from 'node:fs'
28
+ import { createRequire } from 'node:module'
29
+ import { createHash } from 'node:crypto'
30
+ import { homedir } from 'node:os'
31
+ import { dirname, join } from 'node:path'
32
+ import { fileURLToPath } from 'node:url'
33
+
34
+ export const name = 'vibe-math-preset-installer'
35
+
36
+ const PRESETS = [
37
+ {
38
+ src: 'vibe-math-v2',
39
+ dst: 'vibe-math-v2',
40
+ files: ['agent.cordis.yml', 'preset.yml', 'vibe-math-v2.js', '实现方案.md'],
41
+ },
42
+ {
43
+ src: 'vibe-math-v3',
44
+ dst: 'vibe-math-v3',
45
+ files: ['agent.cordis.yml', 'preset.yml', 'vibe-math-v3.js', '实现方案.md'],
46
+ },
47
+ {
48
+ src: 'vibe-math-v4',
49
+ dst: 'vibe-math-v4',
50
+ files: ['agent.cordis.yml', 'preset.yml', 'vibe-math-v4.js', '实现方案.md'],
51
+ },
52
+ {
53
+ src: 'vibe-math-v5',
54
+ dst: 'vibe-math-v5',
55
+ files: ['agent.cordis.yml', 'preset.yml', 'vibe-math-v5.js', '实现方案.md'],
56
+ },
57
+ ]
58
+
59
+ const STATE_FILE = '.vibe-math-installed.json'
60
+
61
+ function sha256(buf) { return createHash('sha256').update(buf).digest('hex') }
62
+
63
+ function readState(path) {
64
+ try {
65
+ const raw = readFileSync(path, 'utf8')
66
+ const obj = JSON.parse(raw)
67
+ if (obj && typeof obj === 'object' && obj.files && typeof obj.files === 'object') return obj
68
+ } catch (e) { /* missing or corrupt — treat as no state (baseline) */ }
69
+ return null
70
+ }
71
+
72
+ function writeState(path, state) {
73
+ try {
74
+ const tmp = path + '.tmp'
75
+ writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8')
76
+ renameSync(tmp, path)
77
+ } catch (e) {
78
+ // best-effort: state persistence failure must not break the copy step
79
+ }
80
+ }
81
+
82
+ // DSH 适配性自检(能力检测,而非版本号——DSH 不向插件暴露版本)。
83
+ // 检查 preset 运行时需要的宿主服务与关键 API 形状是否可用,缺失时打 warning。
84
+ // Best-effort DSH host-version detection. DSH does NOT expose its version through a documented
85
+ // service/context property or a guaranteed env var, so we probe in order: an explicit env var
86
+ // (future-proofing), then the installed @deepseek-ai/dsh package.json. This is layout-dependent
87
+ // (works for a typical global install where @deepseek-ai/dsh is a sibling of this plugin); when it
88
+ // cannot resolve, the capability self-check below is still the authoritative gate.
89
+ const __require = createRequire(import.meta.url)
90
+ function detectDshVersion() {
91
+ try { const v = process.env.DSH_VERSION; if (v && String(v).trim()) return String(v).trim() } catch (e) {}
92
+ try {
93
+ const p = __require.resolve('@deepseek-ai/dsh/package.json')
94
+ const v = (JSON.parse(readFileSync(p, 'utf8')).version || '').trim()
95
+ if (v) return v
96
+ } catch (e) { /* host package not resolvable from here — rely on capability check */ }
97
+ return undefined
98
+ }
99
+
100
+ async function checkHostCapabilities(ctx, logger) {
101
+ const problems = []
102
+ // 1) DSH version compatibility (best-effort, only when the version is detectable).
103
+ // Declared under package.json dsh.compatibility.dshReleases (per the DSH STORE contract):
104
+ // each full DSH release maps to 'compatible' | 'incompatible' | 'unknown'. A version that is
105
+ // absent or 'unknown' is a soft warning; 'incompatible' is a hard "please use X" message.
106
+ let dshRel = {}
107
+ try { const m = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')); dshRel = (m.dsh && m.dsh.compatibility && m.dsh.compatibility.dshReleases) || {} } catch (e) {}
108
+ const supported = Object.keys(dshRel).sort()
109
+ const dshVersion = detectDshVersion()
110
+ if (dshVersion) {
111
+ const status = dshRel[dshVersion]
112
+ if (status === 'incompatible') {
113
+ problems.push('当前 DSH 版本 v' + dshVersion + ' 被本包声明为 incompatible;请使用 ' + supported.join(' / ') + '。')
114
+ } else if (status === undefined || status === 'unknown') {
115
+ problems.push('当前 DSH 版本 v' + dshVersion + ' 尚未被本包声明为兼容(dshReleases 仅声明 ' + supported.join(' / ') + ');建议使用 ' + supported.join(' / ') + ',或将该版本在 dshReleases 中标注后再自行验证。')
116
+ }
117
+ }
118
+ // 2) capability self-check (the authoritative mounting gate; also covers hosts whose version
119
+ // could not be read). subagents / agents / tools / commands / fs shapes + v4 capabilities.
120
+ // `required: true` services are the mounting gate; the rest are optional services the presets
121
+ // read with ctx.get(). Their absence does not stop a mount but degrades SILENTLY, so report
122
+ // them instead of letting the field discover them: without `subprocess` no directory-creation
123
+ // shell runs, without `sandboxPolicy` writes carry no explicit fence, without `compaction` the
124
+ // v4 real /compact path is inert.
125
+ const checks = [
126
+ // subagents 服务的续做/唤醒方法是 sendMessage(sender, targetId, content, {signal});
127
+ // followup 不是 subagents 服务的方法(它只是 Agent 对象方法)。同时探测两者,能用一个即可。
128
+ { svc: 'subagents', methods: ['startContinuable', 'interrupt'], required: true },
129
+ { svc: 'agents', methods: ['roots'], required: true },
130
+ { svc: 'tools', methods: ['register'], required: true },
131
+ { svc: 'commands', methods: ['register'], required: true },
132
+ { svc: 'fs', methods: ['resolve', 'stat', 'readText', 'writeText', 'listDir'], required: true },
133
+ { svc: 'subprocess', methods: ['spawn'], required: false },
134
+ { svc: 'sandboxPolicy', methods: ['resolve'], required: false },
135
+ { svc: 'compaction', methods: ['compactIfNeeded'], required: false },
136
+ // v5 keeps its institute state in a HOST-ONLY session projection unit, so it wants
137
+ // the projection registry and the session store. Both are mounted by dsh-base; if
138
+ // either is absent v5 falls back to a hardened JSON state file, so this is a
139
+ // degradation rather than a mounting gate.
140
+ { svc: 'sessionProjections', methods: ['register', 'stateOf'], required: false },
141
+ { svc: 'sessions', methods: ['flush'], required: false },
142
+ ]
143
+ const degradations = []
144
+ for (let i = 0; i < checks.length; i++) {
145
+ const svc = checks[i].svc
146
+ const methods = checks[i].methods
147
+ const required = checks[i].required === true
148
+ const report = required ? ((m) => problems.push(m)) : ((m) => degradations.push(m))
149
+ let s
150
+ try { s = (ctx && ctx.get) ? ctx.get(svc) : undefined } catch (e) { s = undefined }
151
+ if (s === undefined) { report('宿主缺少服务 ' + svc); continue }
152
+ for (let j = 0; j < methods.length; j++) {
153
+ if (typeof s[methods[j]] !== 'function') report(svc + '.' + methods[j] + ' 不可用(宿主版本可能过旧)')
154
+ }
155
+ // subagents continuation (wake) API: sendMessage (modern) OR followup (legacy) must exist.
156
+ if (svc === 'subagents' && typeof s.sendMessage !== 'function' && typeof s.followup !== 'function') {
157
+ problems.push('subagents 缺少续做/唤醒方法(需 sendMessage 或 followup 至少其一)')
158
+ }
159
+ }
160
+ // fs API shape: DSH 0.1.1 起 resolve 返回 {targetKey, displayPath} 对象(旧版返回字符串路径)
161
+ try {
162
+ const f = (ctx && ctx.get) ? ctx.get('fs') : undefined
163
+ if (f && typeof f.resolve === 'function') {
164
+ const r = await f.resolve('x', { cwd: process.cwd() })
165
+ if (typeof r !== 'object' || r === null || typeof r.targetKey !== 'string') {
166
+ problems.push('fs.resolve 返回形状不符(期望 {targetKey, displayPath},v3/v4 预设要求 DSH ≥ 0.1.1)')
167
+ }
168
+ }
169
+ } catch (e) { problems.push('fs.resolve 能力检测失败:' + String((e && e.message) || e)) }
170
+ // v4 依赖 subagents.startContinuable 的 agentOptions / toolFilter 能力(DSH 0.1.2 起由
171
+ // dsh-subagent 声明 SubagentCapabilities.agentOptions;spawn/fork 进程内 provider 均支持。
172
+ // 缺省 provider 名按 spawn 探测;探测失败不视为致命(等价于回退到再试一次、只警告)。
173
+ try {
174
+ const sa = (ctx && ctx.get) ? ctx.get('subagents') : undefined
175
+ if (sa && typeof sa.list === 'function') {
176
+ const names = (sa.list ? sa.list() : [])
177
+ const name = names.indexOf('spawn') !== -1 ? 'spawn' : (names[0] || '')
178
+ if (name && typeof sa.getProvider === 'function') {
179
+ const cap = (sa.getProvider(name) || {}).capabilities
180
+ if (cap && cap.agentOptions === false) problems.push('subagents provider "' + name + '" 不支持 agentOptions(v4 指定常驻模型/路由需要)')
181
+ if (cap && cap.toolFilter === false) problems.push('subagents provider "' + name + '" 不支持 toolFilter(v4 常驻工具权限需要)')
182
+ }
183
+ }
184
+ } catch (e) { /* 探测失败不致命 */ }
185
+ if (degradations.length > 0) {
186
+ logger?.warn?.('[dsh-vibe-math] 可选宿主服务缺失,功能会静默降级(不影响挂载):' + degradations.join(';') + '。subprocess 缺失则无法用 shell 创建目录树(仅靠 fs 自动建父目录兜底);sandboxPolicy 缺失则插件写入不带显式围栏;compaction 缺失则 v4 的真实 /compact 路径与 v5 的真实压缩不生效(v5 回退到自述浓缩);sessionProjections 缺失则 v5 的研究所状态回退到加固 JSON 文件(权威源从会话日志投影变为 State/<institute>.v5state.json,跨进程恢复能力下降)。')
187
+ }
188
+ if (problems.length > 0) {
189
+ logger?.warn?.('[dsh-vibe-math] 宿主自检:' + problems.length + ' 项不满足(' + problems.join(';') + ')。v2/v3/v4/v5 预设依赖这些宿主服务/API,旧版或未经声明兼容的 DSH 可能无法挂载' + (dshVersion ? '(当前检测到 DSH v' + dshVersion + ',本包适配 ' + (supported.length ? supported.join(' / ') : '(未声明)') + ')' : '') + '。')
190
+ } else {
191
+ logger?.info?.('[dsh-vibe-math] 宿主自检通过:subagents / agents / tools / commands / fs 服务及关键 API 均可用' + (degradations.length === 0 ? ',可选服务 subprocess / sandboxPolicy / compaction / sessionProjections / sessions 亦齐备' : '(可选服务有缺失,见上方警告)') + (dshVersion ? '(当前 DSH v' + dshVersion + ',本包已声明兼容 ' + supported.join(' / ') + ')' : '') + '。')
192
+ }
193
+ }
194
+
195
+ export async function apply(ctx) {
196
+ const logger = ctx && ctx.logger
197
+ try {
198
+ const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh')
199
+ const here = dirname(fileURLToPath(import.meta.url))
200
+ const presetRoot = join(dshHome, '.agent-presets')
201
+ const stateFile = join(presetRoot, STATE_FILE)
202
+
203
+ // current package version (the source of truth for "is this an upgrade?")
204
+ let pkgVersion = ''
205
+ try { pkgVersion = String((JSON.parse(readFileSync(join(here, 'package.json'), 'utf8')).version) || '') } catch (e) { pkgVersion = '' }
206
+
207
+ const state = readState(stateFile)
208
+ const prevFiles = (state && state.files) || {}
209
+ const isUpgrade = state !== null && pkgVersion !== '' && state.version !== pkgVersion
210
+ const isBaseline = state === null // no recorded history → refresh everything (user policy: auto-update old installs)
211
+
212
+ const nextFiles = {}
213
+ let installed = 0, updated = 0, kept = 0
214
+ const keptList = []
215
+
216
+ for (const p of PRESETS) {
217
+ const srcDir = join(here, p.src)
218
+ const dstDir = join(presetRoot, p.dst)
219
+ if (!existsSync(srcDir)) continue
220
+ mkdirSync(dstDir, { recursive: true })
221
+ for (const f of p.files) {
222
+ const s = join(srcDir, f)
223
+ const d = join(dstDir, f)
224
+ if (!existsSync(s)) continue
225
+ const key = p.src + '/' + f
226
+ const cur = readFileSync(s)
227
+ const curHash = sha256(cur)
228
+ if (!existsSync(d)) {
229
+ // missing file: always restore, whatever the version
230
+ writeFileSync(d, cur)
231
+ installed += 1
232
+ nextFiles[key] = { hash: curHash, provenance: 'package' }
233
+ continue
234
+ }
235
+ const destHash = sha256(readFileSync(d))
236
+ if (isBaseline) {
237
+ // no recorded history: refresh to the current package (one-time; edits
238
+ // made before this mechanism are overwritten, later edits are protected)
239
+ if (destHash === curHash) { nextFiles[key] = { hash: curHash, provenance: 'package' } }
240
+ else { writeFileSync(d, cur); updated += 1; nextFiles[key] = { hash: curHash, provenance: 'package' } }
241
+ continue
242
+ }
243
+ const prev = prevFiles[key]
244
+ const prevRec = (prev && typeof prev === 'object') ? prev : { hash: prev, provenance: 'package' }
245
+ const prevProv = (prevRec.provenance === 'user') ? 'user' : 'package' // 未知来源按包文件处理
246
+ if (prevProv === 'package' && destHash === prevRec.hash) {
247
+ // 包文件且未被改动 → 可安全升级(内容相同则跳过写入)
248
+ if (destHash !== curHash) { writeFileSync(d, cur); updated += 1 }
249
+ nextFiles[key] = { hash: curHash, provenance: 'package' }
250
+ } else if (prevProv === 'user') {
251
+ // 用户持有 → 永不覆盖
252
+ kept += 1
253
+ if (isUpgrade) keptList.push(key + ' (用户持有)')
254
+ nextFiles[key] = { hash: destHash, provenance: 'user' }
255
+ } else {
256
+ // 包文件但自上次安装后已被用户改动
257
+ kept += 1
258
+ if (isUpgrade) keptList.push(key + ' (已修改)')
259
+ nextFiles[key] = { hash: destHash, provenance: 'user' }
260
+ }
261
+ }
262
+ }
263
+
264
+ // Clean up preset dirs that this bundle NO LONGER manages (e.g. vibe-math-v1 after it was
265
+ // removed at v2.0.0). The copy loop only adds/updates PRESETS; it never deletes a preset that
266
+ // was dropped, so an old removed preset would linger in the picker forever. Here we remove the
267
+ // files this installer previously recorded as package-owned under a prefix that is no longer in
268
+ // PRESETS, then drop the dir if it became empty. User-owned files (provenance 'user') are kept.
269
+ const currentPrefixes = new Set(PRESETS.map(p => p.src + '/'))
270
+ let removedFiles = 0
271
+ let removedDirs = []
272
+ const stale = new Map() // prefix -> [keys]
273
+ for (const key of Object.keys(prevFiles)) {
274
+ const slash = key.indexOf('/')
275
+ if (slash === -1) continue
276
+ const prefix = key.slice(0, slash + 1)
277
+ if (currentPrefixes.has(prefix)) continue
278
+ if (!stale.has(prefix)) stale.set(prefix, [])
279
+ stale.get(prefix).push(key)
280
+ }
281
+ for (const [prefix, keys] of stale) {
282
+ let dirEmpty = true
283
+ for (const key of keys) {
284
+ const rec = (prevFiles[key] && typeof prevFiles[key] === 'object') ? prevFiles[key] : { provenance: 'package' }
285
+ if (rec.provenance === 'user') { dirEmpty = false; continue } // 用户文件 → 保留
286
+ const f = join(presetRoot, key)
287
+ if (existsSync(f)) { try { unlinkSync(f); removedFiles += 1 } catch (e) {} }
288
+ if (existsSync(f)) dirEmpty = false
289
+ }
290
+ const dir = join(presetRoot, prefix.slice(0, -1))
291
+ if (dirEmpty && existsSync(dir)) { try { rmdirSync(dir); removedDirs.push(dir) } catch (e) {} }
292
+ }
293
+
294
+ writeState(stateFile, { version: pkgVersion, files: nextFiles, updatedAt: Date.now() })
295
+
296
+ if (removedFiles > 0 || removedDirs.length > 0) {
297
+ logger?.info?.('[dsh-vibe-math] preset cleanup: removed ' + removedFiles + ' file(s) from ' + removedDirs.length + ' stale preset dir(s) (' + removedDirs.map(d => d.split(/[\\/]/).pop()).join(', ') + ') that are no longer shipped.')
298
+ }
299
+
300
+ if (isUpgrade) {
301
+ logger?.info?.('[dsh-vibe-math] preset auto-update: version ' + (state.version || '(none)') + ' → ' + pkgVersion +
302
+ ' — 新增 ' + installed + ' 个文件,更新 ' + updated + ' 个文件' +
303
+ (kept > 0 ? ',保留 ' + kept + ' 个未覆盖文件(' + keptList.join('; ') + ')' : '') +
304
+ '。新版本 preset 将在新会话生效。')
305
+ } else if (isBaseline) {
306
+ logger?.info?.('[dsh-vibe-math] preset baseline: refreshed ' + (installed + updated) + ' file(s) to v' + pkgVersion +
307
+ ' — 已启用自动更新(后续版本升级将自动替换未被手动修改的 preset 文件)。')
308
+ } else if (installed > 0) {
309
+ logger?.info?.('[dsh-vibe-math] restored ' + installed + ' missing preset file(s)')
310
+ }
311
+ await checkHostCapabilities(ctx, logger)
312
+ } catch (err) {
313
+ logger?.warn?.('[dsh-vibe-math] preset install/update failed: %s', String((err && err.message) || err))
314
+ }
315
+ }