@ljwei-stak/dsh-model-router 0.13.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.
Files changed (37) hide show
  1. package/.dsh-plugin/client.js +3092 -0
  2. package/.dsh-plugin/index.mjs +1651 -0
  3. package/.dsh-plugin/official-tools-remote-service.mjs +104 -0
  4. package/.dsh-plugin/shared/harness-plan.mjs +179 -0
  5. package/.dsh-plugin/shared/livebench.mjs +264 -0
  6. package/.dsh-plugin/shared/model-profiles.mjs +142 -0
  7. package/.dsh-plugin/shared/official-team-runtime.mjs +411 -0
  8. package/.dsh-plugin/shared/official-tool-executor.mjs +801 -0
  9. package/.dsh-plugin/shared/official-tool-registry.mjs +138 -0
  10. package/.dsh-plugin/shared/official-tools-remote.mjs +173 -0
  11. package/.dsh-plugin/shared/official-tools-runtime.mjs +642 -0
  12. package/.dsh-plugin/shared/router-state.mjs +207 -0
  13. package/.dsh-plugin/shared/router.mjs +1134 -0
  14. package/.dsh-plugin/shared/routing-presets.mjs +49 -0
  15. package/.dsh-plugin/shared/run-ledger.mjs +348 -0
  16. package/.dsh-plugin/shared/security-boundaries.mjs +54 -0
  17. package/.dsh-plugin/shared/subscription-billing.mjs +340 -0
  18. package/.dsh-plugin/shared/task-executors.mjs +1154 -0
  19. package/.dsh-plugin/shared/tool-health.mjs +311 -0
  20. package/.dsh-plugin/shared/vendor-mimo-grok-adapter.mjs +308 -0
  21. package/.dsh-plugin/shared/vendor-minimax-adapter.mjs +247 -0
  22. package/.dsh-plugin/shared/zcode-bundle.mjs +208 -0
  23. package/.dsh-plugin/shared/zcode-installer.mjs +247 -0
  24. package/CHANGELOG.md +36 -0
  25. package/INSTALLATION_GUIDE.zh.md +134 -0
  26. package/LICENSE +21 -0
  27. package/MIGRATION.md +53 -0
  28. package/README.i18n.yaml +3 -0
  29. package/README.md +424 -0
  30. package/README.zh.md +413 -0
  31. package/cordis.patch.yml +12 -0
  32. package/docs/assets/candidate-pruning.svg +80 -0
  33. package/docs/assets/desktop-official-tools-0.9.0.png +0 -0
  34. package/docs/assets/router-only-0.12.0.png +0 -0
  35. package/docs/assets/routing-workflow.svg +96 -0
  36. package/docs/assets/workbench-usage.svg +119 -0
  37. package/package.json +161 -0
@@ -0,0 +1,247 @@
1
+ /**
2
+ * MiniMax Code 0.5.5 headless adapter.
3
+ *
4
+ * Source contracts:
5
+ * https://github.com/MiniMax-AI/minimax-code/blob/v0.5.5/packages/tui/src/cli/contract.ts
6
+ * https://github.com/MiniMax-AI/minimax-code/blob/v0.5.5/packages/tui/src/headless/events.ts
7
+ * https://github.com/MiniMax-AI/minimax-code/blob/v0.5.5/packages/tui/src/headless/contract.ts
8
+ *
9
+ * The Host verifies the official npm bundle, Node runtime, account, and
10
+ * workspace. This module never resolves a PATH shim or accepts free-form args.
11
+ */
12
+ import { basename, isAbsolute } from 'node:path'
13
+
14
+ const TYPES = new Set([
15
+ 'exec.started', 'session.started', 'session.resumed', 'turn.started',
16
+ 'item.started', 'item.updated', 'item.completed',
17
+ 'turn.completed', 'turn.failed', 'exec.completed',
18
+ ])
19
+ const STATUSES = new Set(['succeeded', 'failed', 'timeout', 'cancelled', 'limit_exceeded'])
20
+
21
+ function object(value) {
22
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
23
+ }
24
+
25
+ function id(value, maximum = 240) {
26
+ return typeof value === 'string' && value.length > 0 && value.length <= maximum && !value.includes('\0')
27
+ }
28
+
29
+ function absolute(value, label, filename) {
30
+ if (typeof value !== 'string' || !isAbsolute(value) || value.includes('\0')) {
31
+ throw new TypeError(label + ' must be an absolute path')
32
+ }
33
+ if (filename && basename(value).toLowerCase() !== filename) {
34
+ throw new TypeError(label + ' must identify ' + filename)
35
+ }
36
+ return value
37
+ }
38
+
39
+ function checkedModel(value) {
40
+ if (value == null || value === '') return null
41
+ if (typeof value !== 'string' || value.length > 240 || value.includes('\0')
42
+ || !/^[A-Za-z0-9][A-Za-z0-9_.:-]*\/[A-Za-z0-9][A-Za-z0-9_.:/#-]*$/.test(value)) {
43
+ throw new TypeError('MiniMax model must use provider/model')
44
+ }
45
+ return value
46
+ }
47
+
48
+ /**
49
+ * Build the fixed invocation. The caller sends the task on stdin. MiniMax
50
+ * headless has no read-only switch: the outer Host sandbox enforces that mode.
51
+ * Full vendor permission is available only for a Host-approved isolated run.
52
+ */
53
+ export function buildMiniMaxInvocation({
54
+ nodeExecutable, cliEntry, workspace, mode = 'read-only',
55
+ modelReference, permission = 'smart', timeoutMs, maxSteps,
56
+ }) {
57
+ const file = absolute(nodeExecutable, 'nodeExecutable', process.platform === 'win32' ? 'node.exe' : 'node')
58
+ const entry = absolute(cliEntry, 'cliEntry', 'cli.js')
59
+ const cwd = absolute(workspace, 'workspace')
60
+ if (mode !== 'read-only' && mode !== 'workspace-write') throw new TypeError('invalid execution mode')
61
+ if (permission !== 'smart' && permission !== 'full') throw new TypeError('invalid MiniMax permission')
62
+ if (permission === 'full' && mode !== 'workspace-write') {
63
+ throw new TypeError('full MiniMax permission requires an isolated workspace-write run')
64
+ }
65
+ const model = checkedModel(modelReference)
66
+ if (timeoutMs !== undefined && (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 86_400_000)) {
67
+ throw new TypeError('timeoutMs must be between 1000 and 86400000')
68
+ }
69
+ if (maxSteps !== undefined && (!Number.isSafeInteger(maxSteps) || maxSteps < 1 || maxSteps > 10_000)) {
70
+ throw new TypeError('maxSteps must be between 1 and 10000')
71
+ }
72
+ const args = [entry, 'exec', '--input', '-', '--cwd', cwd,
73
+ '--permission', permission, '--output-format', 'stream-json']
74
+ if (model) args.push('--model', model)
75
+ if (timeoutMs !== undefined) args.push('--timeout', String(timeoutMs) + 'ms')
76
+ if (maxSteps !== undefined) args.push('--max-steps', String(maxSteps))
77
+ return { file, args, format: 'minimax-stream-json',
78
+ source: 'verified-official-npm-bundle', requestedModel: model }
79
+ }
80
+
81
+ function validUsage(value) {
82
+ return value === undefined || (object(value) && Object.values(value).every(
83
+ field => field === undefined || (typeof field === 'number' && Number.isFinite(field) && field >= 0)))
84
+ }
85
+
86
+ function validModel(value) {
87
+ return value === undefined || (object(value) && id(value.providerId) && id(value.modelId))
88
+ }
89
+
90
+ function validResult(value) {
91
+ return object(value) && value.schemaVersion === 1 && value.type === 'exec.result'
92
+ && id(value.runId) && id(value.sessionId) && id(value.turnId)
93
+ && STATUSES.has(value.status) && Number.isFinite(value.durationMs)
94
+ && value.durationMs >= 0 && validModel(value.model) && validUsage(value.usage)
95
+ && (value.usageSource === undefined || [
96
+ 'completed_responses', 'analytics_fallback', 'unavailable',
97
+ ].includes(value.usageSource))
98
+ && (value.usageIncomplete === undefined || typeof value.usageIncomplete === 'boolean')
99
+ && (value.error === undefined || (object(value.error)
100
+ && ['config', 'runtime', 'internal'].includes(value.error.category)
101
+ && typeof value.error.message === 'string'))
102
+ }
103
+
104
+ function answer(result, lastMessage) {
105
+ if (typeof result.output === 'string') return result.output
106
+ if (result.output !== undefined && result.output !== null) {
107
+ try { return JSON.stringify(result.output) } catch { /* malformed vendor payload */ }
108
+ }
109
+ return lastMessage
110
+ }
111
+
112
+ /**
113
+ * Call push with decoded UTF-8 stdout and finish after close. Only a valid
114
+ * exec.completed result, matching turn terminal, and exit code 0 succeeds.
115
+ */
116
+ export function createMiniMaxStreamParser({ maxLineChars = 4_000_000 } = {}) {
117
+ if (!Number.isSafeInteger(maxLineChars) || maxLineChars < 1024 || maxLineChars > 16_000_000) {
118
+ throw new TypeError('maxLineChars is out of range')
119
+ }
120
+ let pending = ''
121
+ let error = null
122
+ let sequence = 0
123
+ let identity = null
124
+ let stage = 'initial'
125
+ let terminal = null
126
+ let terminalStatus = null
127
+ let result = null
128
+ let lastMessage = ''
129
+
130
+ function reject(message) { error ??= message }
131
+
132
+ function accept(line) {
133
+ if (error || !line.trim()) return
134
+ let event
135
+ try { event = JSON.parse(line) } catch { reject('MiniMax 输出了无效 JSONL。'); return }
136
+ if (!object(event) || event.schemaVersion !== 1 || !TYPES.has(event.type)
137
+ || !Number.isSafeInteger(event.sequence) || event.sequence !== sequence + 1
138
+ || !Number.isFinite(event.timestampMs) || event.timestampMs < 0
139
+ || !id(event.runId) || !id(event.sessionId) || !id(event.turnId)) {
140
+ reject('MiniMax JSONL 事件结构或顺序无效。')
141
+ return
142
+ }
143
+ sequence = event.sequence
144
+ const current = { runId: event.runId, sessionId: event.sessionId, turnId: event.turnId }
145
+ if (identity && Object.keys(current).some(key => current[key] !== identity[key])) {
146
+ reject('MiniMax 会话身份在运行中改变。')
147
+ return
148
+ }
149
+ identity ??= current
150
+ if (stage === 'finished' || (terminal && event.type !== 'exec.completed')) {
151
+ reject('MiniMax 在终态后继续输出事件。')
152
+ return
153
+ }
154
+ if (stage === 'initial') {
155
+ if (event.type === 'exec.started') stage = 'started'
156
+ else reject('MiniMax 缺少 exec.started 起始事件。')
157
+ return
158
+ }
159
+ if (event.type === 'session.started' || event.type === 'session.resumed') {
160
+ if (stage === 'started') stage = 'session'
161
+ else reject('MiniMax 会话起始事件顺序无效。')
162
+ return
163
+ }
164
+ if (event.type === 'turn.started') {
165
+ if (stage === 'session') stage = 'turn'
166
+ else reject('MiniMax turn.started 事件顺序无效。')
167
+ return
168
+ }
169
+ if (stage !== 'turn') { reject('MiniMax 缺少会话或任务起始事件。'); return }
170
+ if (event.type.startsWith('item.')) {
171
+ if (!object(event.item) || !id(event.item.id, 500)
172
+ || !['agent_message', 'reasoning', 'tool_call'].includes(event.item.type)) {
173
+ reject('MiniMax item 事件结构无效。')
174
+ } else if (event.type === 'item.completed' && event.item.type === 'agent_message'
175
+ && typeof event.item.content === 'string') {
176
+ lastMessage = event.item.content
177
+ }
178
+ return
179
+ }
180
+ if (event.type === 'turn.completed' || event.type === 'turn.failed') {
181
+ if (!Number.isFinite(event.durationMs) || event.durationMs < 0
182
+ || (event.type === 'turn.failed'
183
+ && (!STATUSES.has(event.status) || event.status === 'succeeded'))) {
184
+ reject('MiniMax turn 终态结构无效。')
185
+ } else {
186
+ terminal = event.type
187
+ terminalStatus = event.type === 'turn.completed' ? 'succeeded' : event.status
188
+ }
189
+ return
190
+ }
191
+ if (event.type === 'exec.completed') {
192
+ if (!terminal || !validResult(event.result)
193
+ || Object.keys(identity).some(key => event.result[key] !== identity[key])
194
+ || event.result.status !== terminalStatus) {
195
+ reject('MiniMax exec.completed 与任务终态不一致。')
196
+ } else {
197
+ result = event.result
198
+ stage = 'finished'
199
+ }
200
+ return
201
+ }
202
+ reject('MiniMax JSONL 出现意外事件。')
203
+ }
204
+
205
+ return {
206
+ push(chunk) {
207
+ if (typeof chunk !== 'string') throw new TypeError('push expects decoded UTF-8 text')
208
+ if (error) return { events: sequence, protocolError: error }
209
+ pending += chunk
210
+ for (;;) {
211
+ const newline = pending.indexOf('\n')
212
+ if (newline < 0) break
213
+ const line = pending.slice(0, newline).replace(/\r$/, '')
214
+ pending = pending.slice(newline + 1)
215
+ if (line.length > maxLineChars) { reject('MiniMax JSONL 单行超出限制。'); break }
216
+ accept(line)
217
+ if (error) break
218
+ }
219
+ if (!error && pending.length > maxLineChars) reject('MiniMax JSONL 单行超出限制。')
220
+ if (error) pending = ''
221
+ return { events: sequence, protocolError: error }
222
+ },
223
+ finish(exitCode) {
224
+ if (!error && pending.trim()) accept(pending.replace(/\r$/, ''))
225
+ pending = ''
226
+ if (error) return { status: 'failed', exitCode, error, terminalEvent: terminal }
227
+ if (!result || !terminal || stage !== 'finished') {
228
+ return { status: 'failed', exitCode, error: 'MiniMax 未返回完整的 exec.completed 结果。', terminalEvent: terminal }
229
+ }
230
+ const finalText = answer(result, lastMessage)
231
+ const details = {
232
+ exitCode, vendorStatus: result.status, terminalEvent: terminal,
233
+ runId: result.runId, sessionId: result.sessionId, turnId: result.turnId,
234
+ reportedModel: result.model ?? null, usage: result.usage ?? null,
235
+ usageSource: result.usageSource ?? null,
236
+ usageIncomplete: result.usageIncomplete ?? null,
237
+ durationMs: result.durationMs, finalText,
238
+ }
239
+ if (exitCode !== 0) return { ...details, status: 'failed', error: 'MiniMax CLI 退出码非零。' }
240
+ if (result.status !== 'succeeded') {
241
+ return { ...details, status: 'failed',
242
+ error: String(result.error?.message ?? result.status).slice(0, 500) }
243
+ }
244
+ return { ...details, status: 'succeeded' }
245
+ },
246
+ }
247
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Locate the pinned ZCode desktop bundle on Windows without trusting PATH.
3
+ *
4
+ * Discovery accepts a deliberate operator override, Windows uninstall records,
5
+ * and the conventional installation directories. An install is usable only if
6
+ * its canonical ZCode.exe has a valid signature from the expected publisher,
7
+ * its signed product version is 3.14.3, and the bundled GLM entry remains
8
+ * inside that same canonical installation root.
9
+ */
10
+ import { execFile } from 'node:child_process'
11
+ import { createReadStream } from 'node:fs'
12
+ import { createHash } from 'node:crypto'
13
+ import { realpath, stat } from 'node:fs/promises'
14
+ import { promisify } from 'node:util'
15
+ import { dirname, isAbsolute, join, relative, sep, win32 } from 'node:path'
16
+
17
+ const execFileAsync = promisify(execFile)
18
+
19
+ export const ZCODE_SUPPORTED_VERSION = '3.14.3'
20
+ export const ZCODE_SIGNER = '北京智谱华章科技股份有限公司'
21
+ export const ZCODE_INSTALL_ENV_VAR = 'MODEL_ROUTER_ZCODE_HOME'
22
+ export const ZCODE_WINDOWS_INSTALLER_URL =
23
+ 'https://cdn-zcode.z.ai/zcode/electron/releases/3.14.3/windows-x64/ZCode-3.14.3-win-x64.exe'
24
+ export const ZCODE_WINDOWS_INSTALLER_SHA256 =
25
+ '404895B46DD1E3066B10A9AB9C187CA07F3082AFC8E108A9D02D86B87186CB93'
26
+ // Extracted from the hash-checked, Authenticode-valid pinned Windows installer.
27
+ // The Desktop EXE signature alone does not authenticate this mutable JS file.
28
+ export const ZCODE_CLI_SHA256 =
29
+ 'B1DF2EF3E5BD76C4AF3ECB296BC003A10D3F13191A26610BD0BA940FEADAD529'
30
+ const ZCODE_CLI_SIZE = 14_820_819
31
+
32
+ const REGISTRY_PATHS = Object.freeze([
33
+ 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
34
+ 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
35
+ 'HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
36
+ ])
37
+
38
+ function powershellPath(env) {
39
+ const systemRoot = env.SystemRoot || env.windir || 'C:\\Windows'
40
+ return join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
41
+ }
42
+
43
+ async function powershellJson(script, env, extraEnv = {}) {
44
+ if (process.platform !== 'win32') return null
45
+ const command = Buffer.from(
46
+ '[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); '
47
+ + '$ErrorActionPreference = "Stop"; ' + script,
48
+ 'utf16le',
49
+ ).toString('base64')
50
+ try {
51
+ const { stdout } = await execFileAsync(powershellPath(env), [
52
+ '-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', command,
53
+ ], {
54
+ windowsHide: true,
55
+ shell: false,
56
+ encoding: 'utf8',
57
+ timeout: 10_000,
58
+ maxBuffer: 256_000,
59
+ env: { ...env, ...extraEnv },
60
+ })
61
+ const output = stdout.trim().replace(/^\uFEFF/, '')
62
+ return output ? JSON.parse(output) : null
63
+ } catch {
64
+ return null
65
+ }
66
+ }
67
+
68
+ async function uninstallRecords(env) {
69
+ const paths = REGISTRY_PATHS.map(path => `'${path}'`).join(',')
70
+ const script = `$records = @(Get-ItemProperty -Path @(${paths}) -ErrorAction SilentlyContinue |
71
+ Where-Object { [string]$_.DisplayName -match '^ZCode(?:\\s|$)' } |
72
+ ForEach-Object { [pscustomobject]@{
73
+ displayName = [string]$_.DisplayName
74
+ displayVersion = [string]$_.DisplayVersion
75
+ installLocation = [string]$_.InstallLocation
76
+ displayIcon = [string]$_.DisplayIcon
77
+ uninstallString = [string]$_.UninstallString
78
+ } }); ConvertTo-Json -InputObject $records -Compress -Depth 3`
79
+ const value = await powershellJson(script, env)
80
+ return Array.isArray(value) ? value : value ? [value] : []
81
+ }
82
+
83
+ function rootFromCommand(value) {
84
+ if (typeof value !== 'string') return null
85
+ const trimmed = value.trim()
86
+ const quoted = /^"([^"]+)"/.exec(trimmed)
87
+ const executable = quoted?.[1] ?? /^(.+?\.(?:exe|ico))(?=,|\s|$)/i.exec(trimmed)?.[1]
88
+ return executable && win32.isAbsolute(executable) ? dirname(executable) : null
89
+ }
90
+
91
+ function candidateRoots(env, records) {
92
+ const candidates = []
93
+ const add = (path, source, registryVersion = null) => {
94
+ if (typeof path !== 'string' || !path.trim()) return
95
+ candidates.push({ path: path.trim().replace(/^"|"$/g, ''), source, registryVersion })
96
+ }
97
+ add(env[ZCODE_INSTALL_ENV_VAR], 'operator-env')
98
+ for (const record of records) {
99
+ const version = String(record.displayVersion || '') || null
100
+ add(record.installLocation, 'uninstall-registry', version)
101
+ add(rootFromCommand(record.displayIcon), 'uninstall-registry', version)
102
+ add(rootFromCommand(record.uninstallString), 'uninstall-registry', version)
103
+ }
104
+ for (const parent of [env.LOCALAPPDATA, env.ProgramFiles, env['ProgramFiles(x86)'], env.ProgramW6432]) {
105
+ if (typeof parent !== 'string' || !win32.isAbsolute(parent)) continue
106
+ add(join(parent, 'Programs', 'ZCode'), 'standard-directory')
107
+ add(join(parent, 'ZCode'), 'standard-directory')
108
+ }
109
+ return candidates
110
+ }
111
+
112
+ function isWithin(root, child) {
113
+ const part = relative(root, child)
114
+ return part !== '' && part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part)
115
+ }
116
+
117
+ function localWindowsPath(path) {
118
+ if (typeof path !== 'string' || path.includes('\0')) return false
119
+ const local = path.startsWith('\\\\?\\') ? path.slice(4) : path
120
+ return /^[A-Za-z]:[\\/]/.test(local)
121
+ }
122
+
123
+ async function verifiedCliEntry(path) {
124
+ if ((await stat(path)).size !== ZCODE_CLI_SIZE) return false
125
+ const digest = createHash('sha256')
126
+ for await (const chunk of createReadStream(path)) digest.update(chunk)
127
+ return digest.digest('hex').toUpperCase() === ZCODE_CLI_SHA256
128
+ }
129
+
130
+ async function signedExecutable(executable, env) {
131
+ const script = `$s = Get-AuthenticodeSignature -LiteralPath $env:MODEL_ROUTER_ZCODE_VERIFY_PATH;
132
+ $v = (Get-Item -LiteralPath $env:MODEL_ROUTER_ZCODE_VERIFY_PATH).VersionInfo;
133
+ [pscustomobject]@{
134
+ signatureStatus = [string]$s.Status
135
+ signerSubject = [string]$s.SignerCertificate.Subject
136
+ productVersion = [string]$v.ProductVersion
137
+ fileVersion = [string]$v.FileVersion
138
+ } | ConvertTo-Json -Compress`
139
+ const value = await powershellJson(script, env, { MODEL_ROUTER_ZCODE_VERIFY_PATH: executable })
140
+ if (!value || value.signatureStatus !== 'Valid'
141
+ || typeof value.signerSubject !== 'string'
142
+ || !value.signerSubject.includes(ZCODE_SIGNER)) return null
143
+ const buildVersion = String(value.productVersion || value.fileVersion || '')
144
+ if (!new RegExp(`^${ZCODE_SUPPORTED_VERSION.replaceAll('.', '\\.')}(?:\\.|$)`).test(buildVersion)) return null
145
+ return { buildVersion, fileVersion: String(value.fileVersion || ''), publisher: value.signerSubject }
146
+ }
147
+
148
+ async function checkedBundle(candidate, env) {
149
+ const input = candidate.path
150
+ if (!localWindowsPath(input)) return null
151
+ try {
152
+ const initial = input.toLowerCase().endsWith('\\zcode.exe') ? dirname(input) : input
153
+ const root = await realpath(initial)
154
+ if (!localWindowsPath(root) || !(await stat(root)).isDirectory()) return null
155
+ const executable = await realpath(join(root, 'ZCode.exe'))
156
+ const cliEntry = await realpath(join(root, 'resources', 'glm', 'zcode.cjs'))
157
+ if (!isWithin(root, executable) || !isWithin(root, cliEntry)
158
+ || !(await stat(executable)).isFile() || !(await stat(cliEntry)).isFile()) return null
159
+ const signature = await signedExecutable(executable, env)
160
+ if (!signature || !(await verifiedCliEntry(cliEntry))) return null
161
+ return Object.freeze({
162
+ id: 'zcode',
163
+ version: ZCODE_SUPPORTED_VERSION,
164
+ buildVersion: signature.buildVersion,
165
+ fileVersion: signature.fileVersion,
166
+ registryVersion: candidate.registryVersion,
167
+ root,
168
+ executable,
169
+ cliEntry,
170
+ cliSha256: ZCODE_CLI_SHA256,
171
+ publisher: signature.publisher,
172
+ signatureStatus: 'Valid',
173
+ source: candidate.source,
174
+ })
175
+ } catch {
176
+ return null
177
+ }
178
+ }
179
+
180
+ /** Return the first verified local 3.14.3 bundle, or null when none is usable. */
181
+ export async function discoverZCodeBundle({ env = process.env } = {}) {
182
+ if (process.platform !== 'win32') return null
183
+ const records = await uninstallRecords(env)
184
+ const seen = new Set()
185
+ for (const candidate of candidateRoots(env, records)) {
186
+ const key = win32.normalize(candidate.path).toLowerCase()
187
+ if (seen.has(key)) continue
188
+ seen.add(key)
189
+ const bundle = await checkedBundle(candidate, env)
190
+ if (bundle) return bundle
191
+ }
192
+ return null
193
+ }
194
+
195
+ /**
196
+ * ZCode entries in the uninstall registry that did not pass verification, e.g. a
197
+ * newer release the plugin has not pinned yet. Only used to explain why the
198
+ * tool shows as unavailable; nothing found here is ever executed.
199
+ */
200
+ export async function unverifiedZCodeInstalls({ env = process.env } = {}) {
201
+ if (process.platform !== 'win32') return []
202
+ const records = await uninstallRecords(env)
203
+ return records.map(record => ({
204
+ version: String(record.displayVersion || '') || null,
205
+ root: [record.installLocation, rootFromCommand(record.displayIcon), rootFromCommand(record.uninstallString)]
206
+ .find(value => typeof value === 'string' && value.trim()) ?? null,
207
+ }))
208
+ }