ai-optimize-master 0.0.0-stage → 0.3.2
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/.codex-plugin/plugin.json +35 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -0
- package/README.md +97 -2
- package/README.zh-CN.md +104 -0
- package/SECURITY.md +89 -0
- package/bin/dsh-optimizer.mjs +205 -0
- package/cordis.patch.yml +3 -0
- package/index.js +90 -0
- package/lib/apply.mjs +264 -0
- package/lib/archive.mjs +130 -0
- package/lib/evolve.mjs +128 -0
- package/lib/manifest.mjs +63 -0
- package/lib/scan.mjs +436 -0
- package/lib/util.mjs +130 -0
- package/mcp-server.mjs +65 -0
- package/package.json +82 -4
- package/skill/SKILL.md +45 -0
package/lib/scan.mjs
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// Read-only inventory of AI-tool assets: skill roots, skill duplication, broken
|
|
2
|
+
// local references, MCP configuration surfaces, and tool versions.
|
|
3
|
+
// Invariant (O1): this module never writes. It only reads files and runs the
|
|
4
|
+
// small set of version probes named in PROBES.
|
|
5
|
+
import { readdir, stat } from 'node:fs/promises'
|
|
6
|
+
import { join, basename } from 'node:path'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { readTextIfExists, sha256Hex, expandHome } from './util.mjs'
|
|
9
|
+
|
|
10
|
+
// Version probes are an allowlist, not a shell: we only ever spawn these names.
|
|
11
|
+
// A binary that exists but fails to run must NOT be reported as usable (O3).
|
|
12
|
+
const PROBES = [
|
|
13
|
+
{ id: 'node', command: 'node', args: ['--version'] },
|
|
14
|
+
{ id: 'git', command: 'git', args: ['--version'] },
|
|
15
|
+
{ id: 'pwsh', command: process.platform === 'win32' ? 'pwsh' : 'pwsh', args: ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.Major'] },
|
|
16
|
+
{ id: 'claude', command: 'claude', args: ['--version'] },
|
|
17
|
+
{ id: 'codex', command: 'codex', args: ['--version'] }
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
// Known skill roots. `optional: true` means "use it if present, stay quiet if
|
|
21
|
+
// absent": a machine that has never installed Cursor should not get a finding for
|
|
22
|
+
// it. Roots supplied explicitly by the caller are always reported when unreadable.
|
|
23
|
+
export function defaultRoots(platform = process.platform) {
|
|
24
|
+
const home = homedir()
|
|
25
|
+
const dir = (...parts) => join(home, ...parts)
|
|
26
|
+
const roots = [
|
|
27
|
+
{ id: 'dsh', label: 'DeepSeek Harness', dir: dir('.dsh', 'skills'), optional: true },
|
|
28
|
+
{ id: 'agents', label: 'Agent Skills (cross-tool)', dir: dir('.agents', 'skills'), optional: true },
|
|
29
|
+
{ id: 'agents-canonical', label: 'Agent Skills canonical source', dir: dir('.agents_skills'), optional: true },
|
|
30
|
+
{ id: 'claude', label: 'Claude Code', dir: dir('.claude', 'skills'), optional: true },
|
|
31
|
+
{ id: 'codex', label: 'Codex CLI', dir: dir('.codex', 'skills'), optional: true },
|
|
32
|
+
{ id: 'openclaw', label: 'OpenClaw / ClawX', dir: dir('.openclaw', 'skills'), optional: true },
|
|
33
|
+
{ id: 'cursor', label: 'Cursor', dir: dir('.cursor', 'skills'), optional: true },
|
|
34
|
+
{ id: 'uking', label: 'U-King', dir: dir('.uking', 'skills'), optional: true },
|
|
35
|
+
// Other agents that follow the same <root>/<name>/SKILL.md convention.
|
|
36
|
+
// Paths are only used when they already exist; nothing is created.
|
|
37
|
+
{ id: 'hermes', label: 'Hermes', dir: dir('.hermes', 'skills'), optional: true },
|
|
38
|
+
{ id: 'opencode', label: 'OpenCode', dir: dir('.opencode', 'skills'), optional: true },
|
|
39
|
+
{ id: 'pi', label: 'Pi', dir: dir('.pi', 'agent', 'skills'), optional: true },
|
|
40
|
+
{ id: 'gemini', label: 'Gemini CLI', dir: dir('.gemini', 'skills'), optional: true },
|
|
41
|
+
{ id: 'qwen', label: 'Qwen Code', dir: dir('.qwen', 'skills'), optional: true },
|
|
42
|
+
{ id: 'trae', label: 'Trae', dir: dir('.trae', 'skills'), optional: true },
|
|
43
|
+
{ id: 'windsurf', label: 'Windsurf', dir: dir('.windsurf', 'skills'), optional: true },
|
|
44
|
+
{ id: 'qoder', label: 'Qoder', dir: dir('.qoder', 'skills'), optional: true },
|
|
45
|
+
{ id: 'codebuddy', label: 'CodeBuddy', dir: dir('.codebuddy', 'skills'), optional: true },
|
|
46
|
+
{ id: 'commandcode', label: 'CommandCode', dir: dir('.commandcode', 'skills'), optional: true },
|
|
47
|
+
{ id: 'grok', label: 'Grok', dir: dir('.grok', 'skills'), optional: true },
|
|
48
|
+
{ id: 'iflow', label: 'iFlow', dir: dir('.iflow', 'skills'), optional: true },
|
|
49
|
+
{ id: 'lingma', label: 'Lingma', dir: dir('.lingma', 'skills'), optional: true },
|
|
50
|
+
{ id: 'zcode', label: 'ZCode', dir: dir('.zcode', 'skills'), optional: true }
|
|
51
|
+
]
|
|
52
|
+
// Extra roots (e.g. a shared Windows workstation tree) are explicit: an
|
|
53
|
+
// unreadable explicit root is reported, never silently ignored.
|
|
54
|
+
for (const extra of (process.env.DSH_OPTIMIZER_ROOTS ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean)) {
|
|
55
|
+
roots.push({ id: `extra:${basename(extra)}`, label: `Extra root ${extra}`, dir: expandHome(extra), optional: false })
|
|
56
|
+
}
|
|
57
|
+
return roots
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function defaultMcpConfigs(platform = process.platform) {
|
|
61
|
+
const home = homedir()
|
|
62
|
+
return [
|
|
63
|
+
{ id: 'claude', label: 'Claude Code', file: join(home, '.claude.json') },
|
|
64
|
+
{ id: 'codex', label: 'Codex CLI', file: join(home, '.codex', 'config.toml') },
|
|
65
|
+
{ id: 'cursor', label: 'Cursor', file: join(home, '.cursor', 'mcp.json') }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function listSkillDirs(rootDir) {
|
|
70
|
+
let items
|
|
71
|
+
try {
|
|
72
|
+
items = await readdir(rootDir, { withFileTypes: true })
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error?.code === 'ENOENT' || error?.code === 'EACCES' || error?.code === 'EPERM') return { skills: [], error: error.code }
|
|
75
|
+
throw error
|
|
76
|
+
}
|
|
77
|
+
const skills = []
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
if (!item.isDirectory()) continue
|
|
80
|
+
const dir = join(rootDir, item.name)
|
|
81
|
+
const skillMd = join(dir, 'SKILL.md')
|
|
82
|
+
const text = await readTextIfExists(skillMd)
|
|
83
|
+
if (text === null) {
|
|
84
|
+
skills.push({ name: item.name, dir, hasSkillMd: false, skillMdHash: null, bytes: 0 })
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
skills.push({ name: item.name, dir, hasSkillMd: true, skillMdHash: sha256Hex(text), bytes: Buffer.byteLength(text) })
|
|
88
|
+
}
|
|
89
|
+
return { skills }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Relative local references such as `scripts/foo.mjs` or `references/bar.md`
|
|
93
|
+
// that the skill body tells an agent to open.
|
|
94
|
+
const REF_RE = /`((?:scripts|references|examples|assets|templates|docs)\/[^`\s)]+)`/g
|
|
95
|
+
|
|
96
|
+
async function checkReferences(dir, text) {
|
|
97
|
+
const broken = []
|
|
98
|
+
const seen = new Set()
|
|
99
|
+
for (const match of text.matchAll(REF_RE)) {
|
|
100
|
+
const ref = match[1]
|
|
101
|
+
if (seen.has(ref)) continue
|
|
102
|
+
seen.add(ref)
|
|
103
|
+
if (ref.includes('*') || ref.includes('<') || ref.endsWith('/')) continue
|
|
104
|
+
try {
|
|
105
|
+
await stat(join(dir, ref))
|
|
106
|
+
} catch {
|
|
107
|
+
broken.push(ref)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return broken
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// A skill that hardcodes Windows-only absolute paths while being read on POSIX
|
|
114
|
+
// is a portability smell, never an automatic deletion (it may target a remote host).
|
|
115
|
+
function platformHints(text, platform) {
|
|
116
|
+
const hints = []
|
|
117
|
+
if (/[A-Za-z]:\\\\|\\bC:\\\\Users\\\\|\\bD:\\\\/.test(text)) hints.push('windows-absolute-paths')
|
|
118
|
+
if (platform !== 'win32' && hints.length) hints.push('read-on-non-windows')
|
|
119
|
+
return hints
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function probeTools(probes = PROBES) {
|
|
123
|
+
const { spawn } = await import('node:child_process')
|
|
124
|
+
const results = []
|
|
125
|
+
for (const probe of probes) {
|
|
126
|
+
const result = await new Promise(resolve => {
|
|
127
|
+
let settled = false
|
|
128
|
+
const done = value => { if (!settled) { settled = true; resolve(value) } }
|
|
129
|
+
try {
|
|
130
|
+
const child = spawn(probe.command, probe.args, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
131
|
+
let out = ''
|
|
132
|
+
let err = ''
|
|
133
|
+
child.stdout.on('data', d => { out += d })
|
|
134
|
+
child.stderr.on('data', d => { err += d })
|
|
135
|
+
child.on('error', error => done({ id: probe.id, found: false, ran: false, reason: error.code === 'ENOENT' ? 'not-on-path' : String(error.code ?? error.message) }))
|
|
136
|
+
child.on('close', code => {
|
|
137
|
+
const text = (out || err).trim()
|
|
138
|
+
if (code === 0 && text) done({ id: probe.id, found: true, ran: true, version: text.split(/\r?\n/)[0].slice(0, 80) })
|
|
139
|
+
else done({ id: probe.id, found: true, ran: false, reason: code === 0 ? 'empty-output' : `exit-${code}` })
|
|
140
|
+
})
|
|
141
|
+
setTimeout(() => { try { child.kill('SIGKILL') } catch {} ; done({ id: probe.id, found: true, ran: false, reason: 'timeout' }) }, 5000)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
done({ id: probe.id, found: false, ran: false, reason: String(error.message) })
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
results.push(result)
|
|
147
|
+
}
|
|
148
|
+
return results
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseMcpSurface(id, text) {
|
|
152
|
+
if (text === null) return { id, present: false, servers: [], parsable: false }
|
|
153
|
+
if (id === 'codex') {
|
|
154
|
+
// TOML: count [mcp_servers.<name>] tables without a TOML dependency.
|
|
155
|
+
const servers = [...text.matchAll(/^\[mcp_servers\.([^\]]+)\]/gm)].map(m => m[1].replace(/^["']|["']$/g, ''))
|
|
156
|
+
return { id, present: true, servers: [...new Set(servers)], parsable: true, format: 'toml' }
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const json = JSON.parse(text)
|
|
160
|
+
const servers = Object.keys(json?.mcpServers ?? {})
|
|
161
|
+
return { id, present: true, servers, parsable: true, format: 'json' }
|
|
162
|
+
} catch {
|
|
163
|
+
return { id, present: true, servers: [], parsable: false, format: 'json' }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function pathStateOf(dir) {
|
|
168
|
+
try {
|
|
169
|
+
const info = await stat(dir)
|
|
170
|
+
return { exists: true, directory: info.isDirectory() }
|
|
171
|
+
} catch {
|
|
172
|
+
return { exists: false, directory: false }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function scan(options = {}) {
|
|
177
|
+
const platform = options.platform ?? process.platform
|
|
178
|
+
const roots = options.roots ?? defaultRoots(platform)
|
|
179
|
+
const mcpConfigs = options.mcpConfigs ?? defaultMcpConfigs(platform)
|
|
180
|
+
const includeProbes = options.probes !== false
|
|
181
|
+
|
|
182
|
+
const inventories = []
|
|
183
|
+
const bySkillName = new Map()
|
|
184
|
+
|
|
185
|
+
for (const root of roots) {
|
|
186
|
+
if (root.optional) {
|
|
187
|
+
// Absent optional roots are "not installed", not an error and not a finding.
|
|
188
|
+
const state = await pathStateOf(root.dir)
|
|
189
|
+
if (!state.exists) continue
|
|
190
|
+
}
|
|
191
|
+
const { skills, error } = await listSkillDirs(root.dir)
|
|
192
|
+
const rootResult = {
|
|
193
|
+
id: root.id,
|
|
194
|
+
label: root.label,
|
|
195
|
+
dir: root.dir,
|
|
196
|
+
applicable: error === undefined,
|
|
197
|
+
notApplicableReason: error ? `unreadable: ${error}` : null,
|
|
198
|
+
skillCount: skills.length,
|
|
199
|
+
skills: []
|
|
200
|
+
}
|
|
201
|
+
for (const skill of skills) {
|
|
202
|
+
const record = {
|
|
203
|
+
name: skill.name,
|
|
204
|
+
dir: skill.dir,
|
|
205
|
+
hasSkillMd: skill.hasSkillMd,
|
|
206
|
+
skillMdHash: skill.skillMdHash,
|
|
207
|
+
bytes: skill.bytes,
|
|
208
|
+
brokenReferences: [],
|
|
209
|
+
platformHints: []
|
|
210
|
+
}
|
|
211
|
+
if (skill.hasSkillMd) {
|
|
212
|
+
const text = await readTextIfExists(join(skill.dir, 'SKILL.md'))
|
|
213
|
+
record.brokenReferences = await checkReferences(skill.dir, text)
|
|
214
|
+
record.platformHints = platformHints(text, platform)
|
|
215
|
+
}
|
|
216
|
+
rootResult.skills.push(record)
|
|
217
|
+
const list = bySkillName.get(skill.name) ?? []
|
|
218
|
+
list.push({ root: root.id, rootLabel: root.label, ...record })
|
|
219
|
+
bySkillName.set(skill.name, list)
|
|
220
|
+
}
|
|
221
|
+
// Context weight is what the agent pays every session. The estimate is
|
|
222
|
+
// explicitly labelled: bytes/4 is a heuristic, never a measured token count (O9).
|
|
223
|
+
rootResult.contextBytes = rootResult.skills.reduce((n, s) => n + (s.bytes ?? 0), 0)
|
|
224
|
+
rootResult.contextEstimate = {
|
|
225
|
+
value: Math.round(rootResult.contextBytes / 4),
|
|
226
|
+
estimator: 'bytes/4',
|
|
227
|
+
note: 'Estimate, not a measured tokenizer count.'
|
|
228
|
+
}
|
|
229
|
+
inventories.push(rootResult)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Duplicate analysis distinguishes "same name" from "same content".
|
|
233
|
+
const duplicates = []
|
|
234
|
+
for (const [name, copies] of bySkillName) {
|
|
235
|
+
if (copies.length < 2) continue
|
|
236
|
+
const hashes = new Set(copies.map(c => c.skillMdHash).filter(Boolean))
|
|
237
|
+
const identical = hashes.size === 1 && copies.every(c => c.hasSkillMd)
|
|
238
|
+
duplicates.push({
|
|
239
|
+
name,
|
|
240
|
+
copies: copies.map(c => ({ root: c.root, rootLabel: c.rootLabel, dir: c.dir, skillMdHash: c.skillMdHash })),
|
|
241
|
+
classification: identical ? 'identical-copies' : 'diverged-copies',
|
|
242
|
+
note: identical
|
|
243
|
+
? 'Same SKILL.md content in several tool roots. Duplicate definition only; it does not prove any copy is unused.'
|
|
244
|
+
: 'Same skill name but different SKILL.md content. Divergence must be reviewed before any cleanup.'
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const mcp = []
|
|
249
|
+
for (const config of mcpConfigs) {
|
|
250
|
+
const text = await readTextIfExists(config.file)
|
|
251
|
+
mcp.push({ ...parseMcpSurface(config.id, text), label: config.label, file: config.file })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const tools = includeProbes ? await probeTools(options.probesList ?? PROBES) : []
|
|
255
|
+
|
|
256
|
+
const findings = buildFindings({ inventories, duplicates, mcp, tools, platform })
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
schema: 'dsh-optimizer/scan/v0.2',
|
|
260
|
+
platform,
|
|
261
|
+
scannedAt: new Date().toISOString(),
|
|
262
|
+
readOnly: true,
|
|
263
|
+
roots: inventories,
|
|
264
|
+
duplicates,
|
|
265
|
+
mcp,
|
|
266
|
+
tools,
|
|
267
|
+
findings
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function finding(id, title, severity, platform, extra = {}) {
|
|
272
|
+
return {
|
|
273
|
+
id,
|
|
274
|
+
title,
|
|
275
|
+
severity,
|
|
276
|
+
platform,
|
|
277
|
+
applicable: extra.applicable ?? true,
|
|
278
|
+
notApplicableReason: extra.notApplicableReason ?? null,
|
|
279
|
+
verified: extra.verified ?? { method: 'read-file', evidence: '' },
|
|
280
|
+
current: extra.current ?? null,
|
|
281
|
+
recommended: extra.recommended ?? null,
|
|
282
|
+
remediation: extra.remediation ?? null
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildFindings({ inventories, duplicates, mcp, tools, platform }) {
|
|
287
|
+
const findings = []
|
|
288
|
+
|
|
289
|
+
// Unreadable roots must say so; "could not read" is never "clean" (O2).
|
|
290
|
+
for (const root of inventories) {
|
|
291
|
+
if (!root.applicable) {
|
|
292
|
+
findings.push(finding(
|
|
293
|
+
`skill-root.unreadable:${root.id}`,
|
|
294
|
+
`Skill root ${root.label} cannot be read`,
|
|
295
|
+
'info',
|
|
296
|
+
platform,
|
|
297
|
+
{
|
|
298
|
+
applicable: false,
|
|
299
|
+
notApplicableReason: root.notApplicableReason,
|
|
300
|
+
verified: { method: 'read-file', evidence: root.notApplicableReason },
|
|
301
|
+
current: root.dir
|
|
302
|
+
}
|
|
303
|
+
))
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const identical = duplicates.filter(d => d.classification === 'identical-copies')
|
|
308
|
+
if (identical.length) {
|
|
309
|
+
findings.push(finding(
|
|
310
|
+
'skills.identical-duplicates',
|
|
311
|
+
`${identical.length} skill name(s) exist as byte-identical copies in several tool roots`,
|
|
312
|
+
'info',
|
|
313
|
+
platform,
|
|
314
|
+
{
|
|
315
|
+
verified: { method: 'read-file', evidence: `${identical.length} groups share one SKILL.md hash` },
|
|
316
|
+
current: identical.map(d => ({ name: d.name, copies: d.copies.length })),
|
|
317
|
+
recommended: 'Review the owning tool for each copy before archiving. Identical content does not prove a copy is unused.',
|
|
318
|
+
remediation: null
|
|
319
|
+
}
|
|
320
|
+
))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const diverged = duplicates.filter(d => d.classification === 'diverged-copies')
|
|
324
|
+
if (diverged.length) {
|
|
325
|
+
findings.push(finding(
|
|
326
|
+
'skills.diverged-duplicates',
|
|
327
|
+
`${diverged.length} skill name(s) exist with different content across roots`,
|
|
328
|
+
'medium',
|
|
329
|
+
platform,
|
|
330
|
+
{
|
|
331
|
+
verified: { method: 'read-file', evidence: `${diverged.length} groups have more than one SKILL.md hash` },
|
|
332
|
+
current: diverged.map(d => ({ name: d.name, hashes: new Set(d.copies.map(c => c.skillMdHash)).size })),
|
|
333
|
+
recommended: 'Decide a single source of truth per skill; do not archive until the divergent copy is reviewed.'
|
|
334
|
+
}
|
|
335
|
+
))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const broken = []
|
|
339
|
+
for (const root of inventories) {
|
|
340
|
+
for (const skill of root.skills) {
|
|
341
|
+
if (skill.brokenReferences.length) broken.push({ root: root.id, name: skill.name, missing: skill.brokenReferences })
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (broken.length) {
|
|
345
|
+
findings.push(finding(
|
|
346
|
+
'skills.broken-references',
|
|
347
|
+
`${broken.length} skill(s) reference local files that do not exist`,
|
|
348
|
+
'medium',
|
|
349
|
+
platform,
|
|
350
|
+
{
|
|
351
|
+
verified: { method: 'read-file', evidence: `${broken.length} skill(s) with at least one missing relative reference` },
|
|
352
|
+
current: broken,
|
|
353
|
+
recommended: 'Repair the path or remove the dangling instruction. A skill that points at missing files misleads the agent.'
|
|
354
|
+
}
|
|
355
|
+
))
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const missingSkillMd = []
|
|
359
|
+
for (const root of inventories) {
|
|
360
|
+
for (const skill of root.skills) {
|
|
361
|
+
if (!skill.hasSkillMd) missingSkillMd.push({ root: root.id, name: skill.name, dir: skill.dir })
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (missingSkillMd.length) {
|
|
365
|
+
findings.push(finding(
|
|
366
|
+
'skills.no-skill-md',
|
|
367
|
+
`${missingSkillMd.length} directory/directories under a skill root have no SKILL.md`,
|
|
368
|
+
'info',
|
|
369
|
+
platform,
|
|
370
|
+
{
|
|
371
|
+
verified: { method: 'read-file', evidence: 'directory present, SKILL.md absent' },
|
|
372
|
+
current: missingSkillMd,
|
|
373
|
+
recommended: 'These are not discoverable as skills. Investigate before removing: another tool may use the directory.'
|
|
374
|
+
}
|
|
375
|
+
))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const mcpTotal = mcp.reduce((n, m) => n + m.servers.length, 0)
|
|
379
|
+
findings.push(finding(
|
|
380
|
+
'mcp.surface',
|
|
381
|
+
`MCP declarations found: ${mcpTotal} server entry/entries across ${mcp.filter(m => m.present).length} config file(s)`,
|
|
382
|
+
'info',
|
|
383
|
+
platform,
|
|
384
|
+
{
|
|
385
|
+
verified: { method: 'read-file', evidence: 'parsed MCP config surfaces without executing any server' },
|
|
386
|
+
current: mcp.map(m => ({ id: m.id, present: m.present, parsable: m.parsable, servers: m.servers })),
|
|
387
|
+
recommended: 'MCP surfaces are read-only in this version. Starting a server or removing one is out of scope; a timeout never proves a server is dead.',
|
|
388
|
+
remediation: null
|
|
389
|
+
}
|
|
390
|
+
))
|
|
391
|
+
|
|
392
|
+
for (const tool of tools) {
|
|
393
|
+
if (!tool.found) {
|
|
394
|
+
findings.push(finding(
|
|
395
|
+
`tool.missing:${tool.id}`,
|
|
396
|
+
`${tool.id} is not on PATH`,
|
|
397
|
+
'info',
|
|
398
|
+
platform,
|
|
399
|
+
{
|
|
400
|
+
applicable: false,
|
|
401
|
+
notApplicableReason: tool.reason,
|
|
402
|
+
verified: { method: 'ran', evidence: `spawn failed: ${tool.reason}` },
|
|
403
|
+
current: null
|
|
404
|
+
}
|
|
405
|
+
))
|
|
406
|
+
} else if (!tool.ran) {
|
|
407
|
+
// O3: presence on PATH is not proof that the tool works.
|
|
408
|
+
findings.push(finding(
|
|
409
|
+
`tool.unusable:${tool.id}`,
|
|
410
|
+
`${tool.id} exists on PATH but did not run successfully`,
|
|
411
|
+
'medium',
|
|
412
|
+
platform,
|
|
413
|
+
{
|
|
414
|
+
verified: { method: 'ran', evidence: `spawned ${tool.id}; ${tool.reason}` },
|
|
415
|
+
current: { found: true, usable: false },
|
|
416
|
+
recommended: 'Investigate the failing executable. A file that exists is not an installed, working tool.'
|
|
417
|
+
}
|
|
418
|
+
))
|
|
419
|
+
} else {
|
|
420
|
+
findings.push(finding(
|
|
421
|
+
`tool.version:${tool.id}`,
|
|
422
|
+
`${tool.id} reports a version`,
|
|
423
|
+
'info',
|
|
424
|
+
platform,
|
|
425
|
+
{
|
|
426
|
+
verified: { method: 'ran', evidence: tool.version },
|
|
427
|
+
current: { found: true, usable: true, version: tool.version }
|
|
428
|
+
}
|
|
429
|
+
))
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return findings
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export { PROBES }
|
package/lib/util.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Shared primitives: canonical JSON, sha256, byte-safe file io, filesystem snapshots.
|
|
2
|
+
// Invariant: hashes over structured values MUST be key-order independent (see O10).
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { readFile, writeFile, readdir, stat, unlink, rename, mkdir, realpath } from 'node:fs/promises'
|
|
5
|
+
import { join, relative, dirname, resolve, sep } from 'node:path'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
|
|
8
|
+
// Deterministic JSON: object keys sorted recursively, arrays keep order.
|
|
9
|
+
// Arrays are NOT sorted: step order and command arguments are semantic.
|
|
10
|
+
export function canonicalStringify(value) {
|
|
11
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value)
|
|
12
|
+
if (Array.isArray(value)) return `[${value.map(canonicalStringify).join(',')}]`
|
|
13
|
+
const keys = Object.keys(value).sort()
|
|
14
|
+
return `{${keys.map(key => `${JSON.stringify(key)}:${canonicalStringify(value[key])}`).join(',')}}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Structured values go through canonicalStringify so the hash never depends on key insertion order.
|
|
18
|
+
export function sha256Hex(value) {
|
|
19
|
+
const text = typeof value === 'string' ? value : canonicalStringify(value)
|
|
20
|
+
return createHash('sha256').update(text).digest('hex')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function home() {
|
|
24
|
+
return process.env.DSH_OPTIMIZER_HOME || join(homedir(), '.dsh-optimizer')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function expandHome(p) {
|
|
28
|
+
if (!p) return p
|
|
29
|
+
if (p === '~') return homedir()
|
|
30
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) return join(homedir(), p.slice(2))
|
|
31
|
+
return p
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function readBytesIfExists(file) {
|
|
35
|
+
try {
|
|
36
|
+
return await readFile(file)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error?.code === 'ENOENT') return null
|
|
39
|
+
throw error
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function writeBytes(file, bytes) {
|
|
44
|
+
await mkdir(dirname(file), { recursive: true })
|
|
45
|
+
await writeFile(file, bytes)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function readTextIfExists(file) {
|
|
49
|
+
const bytes = await readBytesIfExists(file)
|
|
50
|
+
return bytes === null ? null : bytes.toString('utf8')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Snapshot a tree as { relPath -> { size, mtimeMs, sha256 } }. Used by O1 (scan never writes).
|
|
54
|
+
export async function snapshotTree(root) {
|
|
55
|
+
const entries = new Map()
|
|
56
|
+
async function walk(dir) {
|
|
57
|
+
let items
|
|
58
|
+
try {
|
|
59
|
+
items = await readdir(dir, { withFileTypes: true })
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error?.code === 'ENOENT') return
|
|
62
|
+
throw error
|
|
63
|
+
}
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const absolute = join(dir, item.name)
|
|
66
|
+
if (item.isDirectory()) {
|
|
67
|
+
await walk(absolute)
|
|
68
|
+
} else if (item.isFile()) {
|
|
69
|
+
const info = await stat(absolute)
|
|
70
|
+
const bytes = await readFile(absolute)
|
|
71
|
+
entries.set(relative(root, absolute).replaceAll('\\', '/'), {
|
|
72
|
+
size: info.size,
|
|
73
|
+
mtimeMs: info.mtimeMs,
|
|
74
|
+
sha256: createHash('sha256').update(bytes).digest('hex')
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await walk(root)
|
|
80
|
+
return entries
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function snapshotEqual(a, b) {
|
|
84
|
+
if (a.size !== b.size) return false
|
|
85
|
+
for (const [key, value] of a) {
|
|
86
|
+
const other = b.get(key)
|
|
87
|
+
if (!other) return false
|
|
88
|
+
if (value.size !== other.size || value.mtimeMs !== other.mtimeMs || value.sha256 !== other.sha256) return false
|
|
89
|
+
}
|
|
90
|
+
for (const key of b.keys()) {
|
|
91
|
+
if (!a.has(key)) return false
|
|
92
|
+
}
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function sha256Of(file) {
|
|
97
|
+
const bytes = await readBytesIfExists(file)
|
|
98
|
+
if (bytes === null) return null
|
|
99
|
+
return createHash('sha256').update(bytes).digest('hex')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function removeIfExists(file) {
|
|
103
|
+
try {
|
|
104
|
+
await unlink(file)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error?.code !== 'ENOENT') throw error
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function movePath(from, to) {
|
|
111
|
+
await mkdir(dirname(to), { recursive: true })
|
|
112
|
+
await rename(from, to)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Guard: a write target must stay inside one of the declared roots.
|
|
116
|
+
export function isInside(root, target) {
|
|
117
|
+
const r = resolve(root)
|
|
118
|
+
const t = resolve(target)
|
|
119
|
+
return t === r || t.startsWith(r + sep)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function realpathOrNull(target) {
|
|
123
|
+
try { return await realpath(target) } catch { return null }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function parseBool(value, fallback = false) {
|
|
127
|
+
if (value === undefined || value === null || value === '') return fallback
|
|
128
|
+
if (typeof value === 'boolean') return value
|
|
129
|
+
return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase())
|
|
130
|
+
}
|
package/mcp-server.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// MCP surface — READ-ONLY by design.
|
|
3
|
+
// A remote MCP caller may inspect and plan, but must not modify this machine:
|
|
4
|
+
// apply and undo are deliberately absent. See SECURITY.md.
|
|
5
|
+
import readline from 'node:readline'
|
|
6
|
+
import { scan } from './lib/scan.mjs'
|
|
7
|
+
import { buildPlan } from './lib/apply.mjs'
|
|
8
|
+
|
|
9
|
+
const tools = [
|
|
10
|
+
{
|
|
11
|
+
name: 'optimizer_scan',
|
|
12
|
+
description: 'Read-only inventory of AI tool skill roots, duplicates, broken references, MCP declarations, and tool versions. Never writes.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: { includeProbes: { type: 'boolean' } },
|
|
16
|
+
additionalProperties: false
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'optimizer_plan',
|
|
21
|
+
description: 'Build a reviewable, hash-bound plan from a scan. Never modifies anything. Note: the MCP surface cannot apply a plan; apply/undo exist only on the local CLI and the DSH plugin.',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
select: { type: 'array', items: { type: 'string' } },
|
|
26
|
+
root: { type: 'array', items: { type: 'string' } }
|
|
27
|
+
},
|
|
28
|
+
additionalProperties: false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
async function call(name, args = {}) {
|
|
34
|
+
if (name === 'optimizer_scan') return scan({ probes: args.includeProbes ?? true })
|
|
35
|
+
if (name === 'optimizer_plan') {
|
|
36
|
+
const scanResult = await scan({ probes: false })
|
|
37
|
+
const selected = (args.select ?? []).map((skillName, index) => (args.root?.[index] ? { name: skillName, root: args.root[index] } : skillName))
|
|
38
|
+
return buildPlan(scanResult, { selections: selected })
|
|
39
|
+
}
|
|
40
|
+
throw Object.assign(new Error(`Unknown tool: ${name}`), { code: 'METHOD_NOT_FOUND' })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const send = value => process.stdout.write(`${JSON.stringify(value)}\n`)
|
|
44
|
+
const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity })
|
|
45
|
+
|
|
46
|
+
for await (const line of lines) {
|
|
47
|
+
if (!line.trim()) continue
|
|
48
|
+
let request
|
|
49
|
+
try { request = JSON.parse(line) } catch { continue }
|
|
50
|
+
if (request.id === undefined) continue
|
|
51
|
+
try {
|
|
52
|
+
if (request.method === 'initialize') {
|
|
53
|
+
send({ jsonrpc: '2.0', id: request.id, result: { protocolVersion: request.params?.protocolVersion ?? '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'ai-optimize-master', version: '0.3.2' } } })
|
|
54
|
+
} else if (request.method === 'tools/list') {
|
|
55
|
+
send({ jsonrpc: '2.0', id: request.id, result: { tools } })
|
|
56
|
+
} else if (request.method === 'tools/call') {
|
|
57
|
+
const value = await call(request.params?.name, request.params?.arguments ?? {})
|
|
58
|
+
send({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }], isError: false } })
|
|
59
|
+
} else {
|
|
60
|
+
send({ jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `Unknown method: ${request.method}` } })
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
send({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: error?.message ?? String(error) } })
|
|
64
|
+
}
|
|
65
|
+
}
|