@klhapp/skillmux 0.2.1 → 0.4.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/CHANGELOG.md +68 -0
- package/README.md +72 -3
- package/config.example.toml +5 -0
- package/docs/configuration.md +58 -4
- package/docs/releasing.md +49 -21
- package/package.json +1 -1
- package/src/adapters.ts +381 -0
- package/src/calibrate.ts +797 -0
- package/src/cli.ts +866 -385
- package/src/clients.ts +3 -1
- package/src/completions.ts +84 -0
- package/src/config-service.ts +408 -0
- package/src/config-watcher.ts +179 -0
- package/src/config.ts +7 -0
- package/src/context.ts +183 -0
- package/src/dataset-generator.ts +140 -0
- package/src/doctor.ts +43 -0
- package/src/eval.ts +22 -4
- package/src/init.ts +1 -1
- package/src/manifest.ts +124 -10
- package/src/output.ts +151 -0
- package/src/router-core.ts +60 -12
- package/src/server.ts +119 -0
- package/src/snapshot.ts +135 -0
- package/src/sync.ts +29 -8
- package/src/types.ts +7 -0
- package/src/vault.ts +59 -1
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
|
-
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { generateDataset } from "./dataset-generator";
|
|
6
|
+
|
|
5
7
|
import { createClients } from "./clients";
|
|
6
8
|
import { loadConfig } from "./config";
|
|
7
9
|
import { expandHome } from "./config";
|
|
@@ -17,7 +19,16 @@ import {
|
|
|
17
19
|
resolveSkillDir,
|
|
18
20
|
validateSkillCandidate,
|
|
19
21
|
} from "./install";
|
|
20
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
parseManifest,
|
|
24
|
+
pinCore,
|
|
25
|
+
pinProject,
|
|
26
|
+
resolveManifestPath,
|
|
27
|
+
serializeManifest,
|
|
28
|
+
unpinCore,
|
|
29
|
+
unpinProject,
|
|
30
|
+
validateManifest,
|
|
31
|
+
} from "./manifest";
|
|
21
32
|
import { downloadLocalModels } from "./models";
|
|
22
33
|
import { backfillEmbeddings, configure, rebuildIndex } from "./router-core";
|
|
23
34
|
import { renderScanJson, renderScanText, scanExitCode, scanPath, type ScanSeverity } from "./scan";
|
|
@@ -27,456 +38,926 @@ import {
|
|
|
27
38
|
restoreMonolith as restoreMonolithTarget,
|
|
28
39
|
syncProjectTargets,
|
|
29
40
|
syncTarget,
|
|
41
|
+
writeLocalVaultMarker,
|
|
30
42
|
} from "./sync";
|
|
31
|
-
import { scanVault } from "./vault";
|
|
43
|
+
import { scanVault, vaultResolutionOrder } from "./vault";
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
import {
|
|
46
|
+
addContext,
|
|
47
|
+
getCurrentContext,
|
|
48
|
+
listContexts,
|
|
49
|
+
removeContext,
|
|
50
|
+
resolveTarget,
|
|
51
|
+
useContext,
|
|
52
|
+
type ResolvedTarget,
|
|
53
|
+
} from "./context";
|
|
54
|
+
import { createTargetAdapter, type TargetAdapter } from "./adapters";
|
|
55
|
+
import { formatJsonEnvelope, isInteractive, mapExitCode, renderTable, renderTargetBanner, suggestCorrection } from "./output";
|
|
56
|
+
import { generateCompletions, type ShellType } from "./completions";
|
|
57
|
+
|
|
58
|
+
const KNOWN_COMMANDS = [
|
|
59
|
+
"context",
|
|
60
|
+
"config",
|
|
61
|
+
"calibrate",
|
|
62
|
+
"completions",
|
|
63
|
+
"serve",
|
|
64
|
+
"index",
|
|
65
|
+
"sync",
|
|
66
|
+
"init",
|
|
67
|
+
"report",
|
|
68
|
+
"scan",
|
|
69
|
+
"install",
|
|
70
|
+
"eval",
|
|
71
|
+
"doctor",
|
|
72
|
+
"models",
|
|
73
|
+
"which",
|
|
74
|
+
"manifest",
|
|
75
|
+
"local-vault",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
async function main() {
|
|
79
|
+
const rawArgv = Bun.argv.slice(2);
|
|
80
|
+
|
|
81
|
+
let isJson = process.env.SKILLMUX_JSON === "true";
|
|
82
|
+
let allowInsecure = false;
|
|
83
|
+
let isVerbose = false;
|
|
84
|
+
let flagContext: string | undefined;
|
|
85
|
+
let flagServer: string | undefined;
|
|
86
|
+
let isDryRun = false;
|
|
87
|
+
|
|
88
|
+
const command = rawArgv[0];
|
|
89
|
+
if (!command || command === "--help" || command === "-h") {
|
|
90
|
+
printHelp();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Parse global flags for context/config/calibrate
|
|
95
|
+
for (let i = 0; i < rawArgv.length; i++) {
|
|
96
|
+
const arg = rawArgv[i];
|
|
97
|
+
if (arg === "--json") isJson = true;
|
|
98
|
+
else if (arg === "--allow-insecure") allowInsecure = true;
|
|
99
|
+
else if (arg === "--verbose") isVerbose = true;
|
|
100
|
+
else if (arg === "--dry-run") isDryRun = true;
|
|
101
|
+
else if (arg === "--context") flagContext = rawArgv[++i];
|
|
102
|
+
else if (arg === "--server") flagServer = rawArgv[++i];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let resolvedTarget: ResolvedTarget = { type: "local", name: "local" };
|
|
106
|
+
|
|
107
|
+
// Only resolve target if command is target-aware or context/config/calibrate
|
|
108
|
+
if (["context", "config", "calibrate"].includes(command) || flagContext || flagServer) {
|
|
109
|
+
try {
|
|
110
|
+
resolvedTarget = await resolveTarget({ context: flagContext, server: flagServer });
|
|
111
|
+
} catch (err: any) {
|
|
112
|
+
handleError(err, { target: resolvedTarget, isJson, isVerbose });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
35
116
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
117
|
+
const adapter = createTargetAdapter(resolvedTarget, { allowInsecure });
|
|
118
|
+
const subCommand = rawArgv[1] ?? "";
|
|
119
|
+
const commandArgs = rawArgv.slice(2);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
switch (command) {
|
|
123
|
+
case "context":
|
|
124
|
+
await handleContextCommand(subCommand, commandArgs, { target: resolvedTarget, isJson });
|
|
125
|
+
break;
|
|
126
|
+
case "config":
|
|
127
|
+
await handleConfigCommand(adapter, subCommand, commandArgs, { target: resolvedTarget, isJson, dryRun: isDryRun });
|
|
128
|
+
break;
|
|
129
|
+
case "calibrate":
|
|
130
|
+
await handleCalibrateCommand(adapter, subCommand, rawArgv.slice(1), { target: resolvedTarget, isJson });
|
|
131
|
+
break;
|
|
132
|
+
case "completions":
|
|
133
|
+
await handleCompletionsCommand(subCommand);
|
|
134
|
+
break;
|
|
135
|
+
case "serve": {
|
|
136
|
+
const { startServer } = await import("./server");
|
|
137
|
+
const { transport, port } = parseServeArgs(rawArgv.slice(1));
|
|
138
|
+
const handle = await startServer({ transport, port });
|
|
139
|
+
let stopping = false;
|
|
140
|
+
const shutdown = async () => {
|
|
141
|
+
if (stopping) return;
|
|
142
|
+
stopping = true;
|
|
143
|
+
const timeout = setTimeout(() => process.exit(1), 10_000);
|
|
144
|
+
timeout.unref();
|
|
145
|
+
await handle.stop();
|
|
146
|
+
clearTimeout(timeout);
|
|
147
|
+
process.exit(0);
|
|
148
|
+
};
|
|
149
|
+
process.once("SIGTERM", shutdown);
|
|
150
|
+
process.once("SIGINT", shutdown);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "index":
|
|
154
|
+
await runIndex();
|
|
155
|
+
break;
|
|
156
|
+
case "sync":
|
|
157
|
+
await runSync(rawArgv.slice(1));
|
|
158
|
+
break;
|
|
159
|
+
case "init":
|
|
160
|
+
await runInit(rawArgv.slice(1));
|
|
161
|
+
break;
|
|
162
|
+
case "report":
|
|
163
|
+
await runReport(rawArgv.slice(1));
|
|
164
|
+
break;
|
|
165
|
+
case "scan":
|
|
166
|
+
await runScan(rawArgv.slice(1));
|
|
167
|
+
break;
|
|
168
|
+
case "install":
|
|
169
|
+
await runInstall(rawArgv.slice(1));
|
|
170
|
+
break;
|
|
171
|
+
case "eval":
|
|
172
|
+
await runEval();
|
|
173
|
+
break;
|
|
174
|
+
case "doctor":
|
|
175
|
+
await runDoctor();
|
|
176
|
+
break;
|
|
177
|
+
case "which":
|
|
178
|
+
await runWhich(rawArgv.slice(1));
|
|
179
|
+
break;
|
|
180
|
+
case "manifest":
|
|
181
|
+
await runManifest(subCommand, commandArgs);
|
|
182
|
+
break;
|
|
183
|
+
case "local-vault":
|
|
184
|
+
if (subCommand !== "init") throw new Error("usage: skillmux local-vault init <path>");
|
|
185
|
+
await runLocalVaultInit(commandArgs);
|
|
186
|
+
break;
|
|
187
|
+
case "models":
|
|
188
|
+
if (subCommand !== "download") throw new Error("usage: skillmux models download");
|
|
189
|
+
await runModelDownload();
|
|
190
|
+
break;
|
|
191
|
+
default: {
|
|
192
|
+
const suggestion = suggestCorrection(command, KNOWN_COMMANDS);
|
|
193
|
+
const msg = suggestion
|
|
194
|
+
? `Unknown command "${command}". Did you mean "${suggestion}"?`
|
|
195
|
+
: `usage: skillmux <serve|index|sync|init|report|scan|install|eval|doctor|which|manifest pin/unpin|local-vault init|config show|models download|calibrate generate-dataset>`;
|
|
196
|
+
throw new Error(msg);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (err: any) {
|
|
200
|
+
handleError(err, { target: resolvedTarget, isJson, isVerbose });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function handleContextCommand(
|
|
205
|
+
sub: string,
|
|
206
|
+
args: string[],
|
|
207
|
+
ctx: { target: ResolvedTarget; isJson: boolean }
|
|
208
|
+
) {
|
|
209
|
+
if (sub === "list") {
|
|
210
|
+
const contexts = await listContexts();
|
|
211
|
+
if (ctx.isJson) {
|
|
212
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: contexts })));
|
|
213
|
+
} else {
|
|
214
|
+
renderTargetBanner(ctx.target);
|
|
215
|
+
renderTable(
|
|
216
|
+
[
|
|
217
|
+
{ key: "name", header: "NAME" },
|
|
218
|
+
{ key: "server", header: "SERVER" },
|
|
219
|
+
{ key: "token_env", header: "TOKEN_ENV" },
|
|
220
|
+
{ key: "isDefault", header: "DEFAULT" },
|
|
221
|
+
],
|
|
222
|
+
contexts.map((c) => ({ ...c, token_env: c.token_env ?? "-", isDefault: c.isDefault ? "*" : "" }))
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (sub === "current") {
|
|
229
|
+
const current = await getCurrentContext();
|
|
230
|
+
if (ctx.isJson) {
|
|
231
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: current })));
|
|
232
|
+
} else {
|
|
233
|
+
renderTargetBanner(ctx.target);
|
|
234
|
+
console.log(`Current context: ${current.name} (${current.server})`);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (sub === "add") {
|
|
240
|
+
const name = args[0];
|
|
241
|
+
let server: string | undefined;
|
|
242
|
+
let tokenEnv: string | undefined;
|
|
243
|
+
for (let i = 1; i < args.length; i++) {
|
|
244
|
+
if (args[i] === "--server") server = args[++i];
|
|
245
|
+
else if (args[i] === "--token-env") tokenEnv = args[++i];
|
|
246
|
+
}
|
|
247
|
+
if (!name || !server) {
|
|
248
|
+
throw new Error("usage: skillmux context add <name> --server <url> [--token-env <env_name>]");
|
|
249
|
+
}
|
|
250
|
+
await addContext(name, { server, token_env: tokenEnv });
|
|
251
|
+
if (ctx.isJson) {
|
|
252
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: { name, server, token_env: tokenEnv } })));
|
|
253
|
+
} else {
|
|
254
|
+
console.log(`Added context "${name}" -> ${server}`);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (sub === "use") {
|
|
260
|
+
const name = args[0];
|
|
261
|
+
if (!name) throw new Error("usage: skillmux context use <name>");
|
|
262
|
+
await useContext(name);
|
|
263
|
+
if (ctx.isJson) {
|
|
264
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: { default_context: name } })));
|
|
265
|
+
} else {
|
|
266
|
+
console.log(`Switched default context to "${name}"`);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (sub === "remove") {
|
|
272
|
+
const name = args[0];
|
|
273
|
+
if (!name) throw new Error("usage: skillmux context remove <name>");
|
|
274
|
+
await removeContext(name);
|
|
275
|
+
if (ctx.isJson) {
|
|
276
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: { removed: name } })));
|
|
277
|
+
} else {
|
|
278
|
+
console.log(`Removed context "${name}"`);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
throw new Error("usage: skillmux context <add|list|current|use|remove>");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function handleConfigCommand(
|
|
287
|
+
adapter: TargetAdapter,
|
|
288
|
+
sub: string,
|
|
289
|
+
args: string[],
|
|
290
|
+
ctx: { target: ResolvedTarget; isJson: boolean; dryRun: boolean }
|
|
291
|
+
) {
|
|
292
|
+
if (sub === "show") {
|
|
293
|
+
const data = await adapter.getConfigShow();
|
|
294
|
+
if (ctx.isJson) {
|
|
295
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data })));
|
|
296
|
+
} else {
|
|
297
|
+
renderTargetBanner(ctx.target);
|
|
298
|
+
console.log(JSON.stringify(data.effective, null, 2));
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (sub === "get") {
|
|
304
|
+
const key = args[0];
|
|
305
|
+
if (!key) throw new Error("usage: skillmux config get <key>");
|
|
306
|
+
const val = await adapter.getConfigGet(key);
|
|
307
|
+
if (ctx.isJson) {
|
|
308
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: { key, value: val } })));
|
|
309
|
+
} else {
|
|
310
|
+
console.log(typeof val === "object" ? JSON.stringify(val) : String(val));
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (sub === "validate") {
|
|
316
|
+
const res = await adapter.configValidate();
|
|
317
|
+
if (ctx.isJson) {
|
|
318
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
319
|
+
} else {
|
|
320
|
+
console.log(res.valid ? "Configuration is valid." : "Configuration is invalid.");
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (sub === "diff") {
|
|
326
|
+
const res = await adapter.configDiff();
|
|
327
|
+
if (ctx.isJson) {
|
|
328
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
329
|
+
} else {
|
|
330
|
+
renderTargetBanner(ctx.target);
|
|
331
|
+
console.log(JSON.stringify(res.diff, null, 2));
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (sub === "set") {
|
|
337
|
+
const key = args[0];
|
|
338
|
+
const value = args[1];
|
|
339
|
+
if (!key || value === undefined) {
|
|
340
|
+
throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
|
|
341
|
+
}
|
|
342
|
+
const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
|
|
343
|
+
if (ctx.isJson) {
|
|
344
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
345
|
+
} else {
|
|
346
|
+
renderTargetBanner(ctx.target);
|
|
347
|
+
const prefix = ctx.dryRun ? "[dry-run] " : "";
|
|
348
|
+
console.log(`${prefix}${key}: ${JSON.stringify(res.prior_val)} -> ${JSON.stringify(res.resulting_val)}`);
|
|
349
|
+
console.log(`Persistence: ${res.persistence}, Application: ${res.application}`);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (sub === "status") {
|
|
355
|
+
const res = await adapter.configStatus();
|
|
356
|
+
if (ctx.isJson) {
|
|
357
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
358
|
+
} else {
|
|
359
|
+
renderTargetBanner(ctx.target);
|
|
360
|
+
console.log(`Runtime: ${res.runtime}`);
|
|
361
|
+
console.log(`Active revision: ${res.active_revision}`);
|
|
362
|
+
console.log(`Readiness: ${res.readiness.status}`);
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
throw new Error("usage: skillmux config show");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function handleCalibrateCommand(
|
|
371
|
+
adapter: TargetAdapter,
|
|
372
|
+
sub: string,
|
|
373
|
+
args: string[],
|
|
374
|
+
ctx: { target: ResolvedTarget; isJson: boolean }
|
|
375
|
+
) {
|
|
376
|
+
if (sub === "run") {
|
|
377
|
+
let datasetPath: string | undefined;
|
|
39
378
|
for (let i = 0; i < args.length; i++) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
379
|
+
if (args[i] === "--dataset") datasetPath = args[++i];
|
|
380
|
+
}
|
|
381
|
+
const res = await adapter.calibrateRun({ datasetPath });
|
|
382
|
+
if (ctx.isJson) {
|
|
383
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
384
|
+
} else {
|
|
385
|
+
renderTargetBanner(ctx.target);
|
|
386
|
+
console.log(`Calibration run complete.`);
|
|
387
|
+
if (res.result) console.log(JSON.stringify(res.result, null, 2));
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (sub === "list") {
|
|
393
|
+
const res = await adapter.calibrateList();
|
|
394
|
+
if (ctx.isJson) {
|
|
395
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
396
|
+
} else {
|
|
397
|
+
renderTargetBanner(ctx.target);
|
|
398
|
+
renderTable(
|
|
399
|
+
[
|
|
400
|
+
{ key: "run_id", header: "RUN_ID" },
|
|
401
|
+
{ key: "created_at", header: "CREATED_AT" },
|
|
402
|
+
{ key: "status", header: "STATUS" },
|
|
403
|
+
],
|
|
404
|
+
res
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (sub === "show") {
|
|
411
|
+
const runId = args[1];
|
|
412
|
+
if (!runId) throw new Error("usage: skillmux calibrate show <run_id>");
|
|
413
|
+
const res = await adapter.calibrateShow(runId);
|
|
414
|
+
if (ctx.isJson) {
|
|
415
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
416
|
+
} else {
|
|
417
|
+
renderTargetBanner(ctx.target);
|
|
418
|
+
console.log(JSON.stringify(res, null, 2));
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (sub === "apply") {
|
|
424
|
+
const runId = args[1];
|
|
425
|
+
if (!runId) throw new Error("usage: skillmux calibrate apply <run_id>");
|
|
426
|
+
const res = await adapter.calibrateApply(runId);
|
|
427
|
+
if (ctx.isJson) {
|
|
428
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
429
|
+
} else {
|
|
430
|
+
renderTargetBanner(ctx.target);
|
|
431
|
+
console.log(`Applied calibration run "${runId}"`);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (sub === "generate-dataset") {
|
|
437
|
+
await runCalibrateGenerateDataset(args.slice(1));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
throw new Error("usage: skillmux calibrate generate-dataset [--vault <path>] [--out <file>]");
|
|
60
442
|
}
|
|
61
443
|
|
|
62
|
-
async function
|
|
63
|
-
|
|
64
|
-
|
|
444
|
+
async function handleCompletionsCommand(shell: string) {
|
|
445
|
+
if (shell !== "bash" && shell !== "zsh" && shell !== "fish") {
|
|
446
|
+
throw new Error("usage: skillmux completions <bash|zsh|fish>");
|
|
447
|
+
}
|
|
448
|
+
console.log(generateCompletions(shell as ShellType));
|
|
449
|
+
}
|
|
65
450
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
451
|
+
function handleError(
|
|
452
|
+
err: any,
|
|
453
|
+
opts: { target: ResolvedTarget; isJson: boolean; isVerbose: boolean }
|
|
454
|
+
) {
|
|
455
|
+
const code = mapExitCode(err);
|
|
456
|
+
process.exitCode = code;
|
|
457
|
+
|
|
458
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
459
|
+
|
|
460
|
+
if (opts.isJson) {
|
|
461
|
+
const env = formatJsonEnvelope({
|
|
462
|
+
ok: false,
|
|
463
|
+
target: opts.target,
|
|
464
|
+
error: { code: `EXIT_${code}`, message: msg },
|
|
70
465
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
466
|
+
console.log(JSON.stringify(env));
|
|
467
|
+
} else {
|
|
468
|
+
console.error(msg.startsWith("usage:") || msg.startsWith("Unknown") || msg.startsWith("error:") ? msg : `error: ${msg}`);
|
|
469
|
+
if (opts.isVerbose && err instanceof Error && err.stack) {
|
|
470
|
+
console.error(err.stack);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
76
474
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
475
|
+
function printHelp(): void {
|
|
476
|
+
console.log(`usage: skillmux <serve|index|sync|init|report|scan|install|eval|doctor|which|manifest pin/unpin|local-vault init|config show|models download|calibrate generate-dataset> [--transport stdio|http] [--port N] [--dry-run|--restore-monolith|--install-hook] [--target name --yes] [--server url|--db path] --since window [<path>] [--format text|json] [--fail-on low|medium|high] [<repo>[/path] [--force]] [--vault path] [--out file]`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
// Implementation of commands: serve, index, sync, init, report, scan, install, eval, doctor, models
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
|
|
483
|
+
type Transport = "stdio" | "http";
|
|
484
|
+
|
|
485
|
+
function parseServeArgs(args: string[]): { transport: Transport; port?: number } {
|
|
486
|
+
let transport: Transport = "stdio";
|
|
487
|
+
let port: number | undefined;
|
|
488
|
+
for (let i = 0; i < args.length; i++) {
|
|
489
|
+
const option = args[i];
|
|
490
|
+
const value = args[i + 1];
|
|
491
|
+
if (option === "--transport") {
|
|
492
|
+
if (value !== "stdio" && value !== "http") {
|
|
493
|
+
throw new Error("--transport must be stdio or http");
|
|
494
|
+
}
|
|
495
|
+
transport = value;
|
|
496
|
+
i++;
|
|
497
|
+
} else if (option === "--port") {
|
|
498
|
+
const parsed = Number(value);
|
|
499
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65_535) {
|
|
500
|
+
throw new Error("--port must be an integer between 0 and 65535");
|
|
501
|
+
}
|
|
502
|
+
port = parsed;
|
|
503
|
+
i++;
|
|
504
|
+
} else {
|
|
505
|
+
throw new Error(`unknown serve option: ${option}`);
|
|
84
506
|
}
|
|
507
|
+
}
|
|
508
|
+
return { transport, port };
|
|
85
509
|
}
|
|
86
510
|
|
|
87
|
-
async function
|
|
88
|
-
|
|
89
|
-
|
|
511
|
+
async function runIndex(): Promise<void> {
|
|
512
|
+
const config = await loadConfig();
|
|
513
|
+
configure({ config, clients: createClients(config) });
|
|
514
|
+
const report = await rebuildIndex((skillId, error) => {
|
|
515
|
+
console.error(`warning: keeping previous index entry for ${skillId}: ${error}`);
|
|
516
|
+
});
|
|
517
|
+
const retainedNote =
|
|
518
|
+
report.retained.length > 0
|
|
519
|
+
? ` (${report.retained.length} retained after parse errors)`
|
|
520
|
+
: "";
|
|
521
|
+
console.log(`indexed ${report.indexed} skills${retainedNote}`);
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
const backfilled = await backfillEmbeddings();
|
|
525
|
+
console.log(`embeddings: ${backfilled} backfilled`);
|
|
526
|
+
} catch {
|
|
527
|
+
console.log("embeddings: skipped (endpoint unreachable; lexical-only recall until next index)");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
90
530
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
531
|
+
async function runEval(): Promise<void> {
|
|
532
|
+
const config = await loadConfig();
|
|
533
|
+
configure({ config, clients: createClients(config) });
|
|
534
|
+
|
|
535
|
+
const report = await evalVault().catch((error: unknown) => {
|
|
536
|
+
throw new Error(`eval requires local embeddings: ${String(error)}`);
|
|
537
|
+
});
|
|
538
|
+
console.log(`holdout queries: ${report.queries}`);
|
|
539
|
+
console.log(`lexical recall@3: ${report.lexical.recall_at_3.toFixed(3)}`);
|
|
540
|
+
console.log(`lexical recall@5: ${report.lexical.recall_at_5.toFixed(3)}`);
|
|
541
|
+
console.log(`lexical MRR: ${report.lexical.mrr.toFixed(3)}`);
|
|
542
|
+
console.log(`hybrid recall@3: ${report.hybrid.recall_at_3.toFixed(3)}`);
|
|
543
|
+
console.log(`hybrid recall@5: ${report.hybrid.recall_at_5.toFixed(3)}`);
|
|
544
|
+
console.log(`hybrid MRR: ${report.hybrid.mrr.toFixed(3)}`);
|
|
101
545
|
}
|
|
102
546
|
|
|
103
547
|
async function runDoctor(): Promise<void> {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
);
|
|
111
|
-
if (report.checks.some((check) => !check.ok)) process.exitCode = 1;
|
|
548
|
+
const report = await diagnose(await loadConfig());
|
|
549
|
+
console.log(`inference mode: ${report.mode}`);
|
|
550
|
+
console.log(`routing capability: ${report.capability}`);
|
|
551
|
+
for (const check of report.checks)
|
|
552
|
+
console.log(`${check.ok ? "ok" : "fail"}: ${check.name} - ${check.detail}`);
|
|
553
|
+
if (report.checks.some((check) => !check.ok)) process.exitCode = 1;
|
|
112
554
|
}
|
|
113
555
|
|
|
114
|
-
async function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
556
|
+
async function runWhich(args: string[]): Promise<void> {
|
|
557
|
+
const skillId = args[0];
|
|
558
|
+
if (!skillId) throw new Error("usage: skillmux which <skill_id>");
|
|
559
|
+
const config = await loadConfig();
|
|
560
|
+
const vaultPath = expandHome(config.vault_path);
|
|
561
|
+
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
562
|
+
const roots = vaultResolutionOrder(vaultPath, localVaultPaths).filter((root) =>
|
|
563
|
+
existsSync(join(root, skillId, "SKILL.md")),
|
|
564
|
+
);
|
|
565
|
+
if (roots.length === 0) {
|
|
566
|
+
console.log(`${skillId}: not found in vault_path or local_vault_paths`);
|
|
567
|
+
process.exitCode = 1;
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
console.log(`${skillId}: serving from ${roots[0]}`);
|
|
571
|
+
for (const shadowedRoot of roots.slice(1)) console.log(` shadows: ${shadowedRoot}`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const MANIFEST_USAGE = "usage: skillmux manifest <pin|unpin> <skill_id> (--core | --project <group> [--repo <path>...])";
|
|
575
|
+
|
|
576
|
+
function parseManifestPinArgs(args: string[]): { skillId: string; core: boolean; project?: string; repos: string[] } {
|
|
577
|
+
const skillId = args[0];
|
|
578
|
+
if (!skillId) throw new Error(MANIFEST_USAGE);
|
|
579
|
+
let core = false;
|
|
580
|
+
let project: string | undefined;
|
|
581
|
+
const repos: string[] = [];
|
|
582
|
+
for (let i = 1; i < args.length; i++) {
|
|
583
|
+
const arg = args[i];
|
|
584
|
+
if (arg === "--core") core = true;
|
|
585
|
+
else if (arg === "--project") {
|
|
586
|
+
const value = args[++i];
|
|
587
|
+
if (!value) throw new Error("--project requires a group name");
|
|
588
|
+
project = value;
|
|
589
|
+
} else if (arg === "--repo") {
|
|
590
|
+
const value = args[++i];
|
|
591
|
+
if (!value) throw new Error("--repo requires a path");
|
|
592
|
+
repos.push(value);
|
|
593
|
+
} else throw new Error(`unknown manifest option: ${arg}`);
|
|
594
|
+
}
|
|
595
|
+
if (core === (project !== undefined)) throw new Error(MANIFEST_USAGE);
|
|
596
|
+
return { skillId, core, project, repos };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function runManifest(subCommand: string, args: string[]): Promise<void> {
|
|
600
|
+
if (subCommand !== "pin" && subCommand !== "unpin") throw new Error(MANIFEST_USAGE);
|
|
601
|
+
const { skillId, core, project, repos } = parseManifestPinArgs(args);
|
|
602
|
+
const config = await loadConfig();
|
|
603
|
+
const vaultPath = expandHome(config.vault_path);
|
|
604
|
+
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
605
|
+
const manifestPath = resolveManifestPath(vaultPath);
|
|
606
|
+
if (!manifestPath) throw new Error(`no skillmux.toml found at ${vaultPath}`);
|
|
607
|
+
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
608
|
+
|
|
609
|
+
let updated;
|
|
610
|
+
if (core) {
|
|
611
|
+
updated = subCommand === "pin" ? pinCore(manifest, skillId) : unpinCore(manifest, skillId);
|
|
612
|
+
} else {
|
|
613
|
+
updated =
|
|
614
|
+
subCommand === "pin"
|
|
615
|
+
? pinProject(manifest, skillId, project!, repos)
|
|
616
|
+
: unpinProject(manifest, skillId, project!);
|
|
617
|
+
}
|
|
618
|
+
validateManifest(updated, vaultPath, localVaultPaths);
|
|
619
|
+
await Bun.write(manifestPath, serializeManifest(updated));
|
|
620
|
+
console.log(`${subCommand === "pin" ? "pinned" : "unpinned"} "${skillId}" ${core ? "[core]" : `[project.${project}]`}`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function runLocalVaultInit(args: string[]): Promise<void> {
|
|
624
|
+
const path = args[0];
|
|
625
|
+
if (!path) throw new Error("usage: skillmux local-vault init <path>");
|
|
626
|
+
const expanded = expandHome(path);
|
|
627
|
+
const config = await loadConfig();
|
|
628
|
+
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
629
|
+
if (!localVaultPaths.includes(expanded)) {
|
|
630
|
+
throw new Error(`"${path}" is not one of the configured local_vault_paths — add it to config.toml first`);
|
|
631
|
+
}
|
|
632
|
+
if (!existsSync(expanded)) throw new Error(`"${path}" does not exist`);
|
|
633
|
+
writeLocalVaultMarker(expanded, expandHome(config.vault_path));
|
|
634
|
+
console.log(`wrote ${join(expanded, ".skillmux")} (role: local_vault, vault_path: ${expandHome(config.vault_path)})`);
|
|
135
635
|
}
|
|
136
636
|
|
|
137
637
|
async function runModelDownload(): Promise<void> {
|
|
138
|
-
|
|
139
|
-
|
|
638
|
+
const cacheDir = await downloadLocalModels(await loadConfig());
|
|
639
|
+
console.log(`models ready in ${cacheDir}`);
|
|
140
640
|
}
|
|
141
641
|
|
|
142
642
|
function parseSyncArgs(args: string[]): {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
643
|
+
dryRun: boolean;
|
|
644
|
+
restoreMonolith: boolean;
|
|
645
|
+
installHook: boolean;
|
|
146
646
|
} {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
647
|
+
let dryRun = false;
|
|
648
|
+
let restoreMonolith = false;
|
|
649
|
+
let installHook = false;
|
|
650
|
+
for (const arg of args) {
|
|
651
|
+
if (arg === "--dry-run") dryRun = true;
|
|
652
|
+
else if (arg === "--restore-monolith") restoreMonolith = true;
|
|
653
|
+
else if (arg === "--install-hook") installHook = true;
|
|
654
|
+
else throw new Error(`unknown sync option: ${arg}`);
|
|
655
|
+
}
|
|
656
|
+
return { dryRun, restoreMonolith, installHook };
|
|
157
657
|
}
|
|
158
658
|
|
|
159
659
|
async function runSync(args: string[]): Promise<void> {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (installHook) {
|
|
165
|
-
const result = installPostMergeHook(vaultPath);
|
|
166
|
-
console.log(
|
|
167
|
-
result.installed
|
|
168
|
-
? "installed post-merge hook"
|
|
169
|
-
: "post-merge hook already installed",
|
|
170
|
-
);
|
|
171
|
-
}
|
|
660
|
+
const { dryRun, restoreMonolith, installHook } = parseSyncArgs(args);
|
|
661
|
+
const config = await loadConfig();
|
|
662
|
+
const vaultPath = expandHome(config.vault_path);
|
|
172
663
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
664
|
+
if (installHook) {
|
|
665
|
+
const result = installPostMergeHook(vaultPath);
|
|
666
|
+
console.log(
|
|
667
|
+
result.installed
|
|
668
|
+
? "installed post-merge hook"
|
|
669
|
+
: "post-merge hook already installed",
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const manifestPath = resolveManifestPath(vaultPath);
|
|
674
|
+
if (!manifestPath) {
|
|
675
|
+
console.log("no skillmux.toml found at vault root — nothing to sync");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
680
|
+
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
681
|
+
const { notes } = validateManifest(manifest, vaultPath, localVaultPaths);
|
|
682
|
+
for (const note of notes) console.log(`note: ${note}`);
|
|
683
|
+
|
|
684
|
+
for (const [targetName, target] of Object.entries(manifest.targets)) {
|
|
685
|
+
const targetDir = expandHome(target.dir);
|
|
686
|
+
|
|
687
|
+
if (restoreMonolith) {
|
|
688
|
+
const result = restoreMonolithTarget(targetDir, vaultPath);
|
|
689
|
+
console.log(
|
|
690
|
+
result.restored
|
|
691
|
+
? `${targetName}: restored to a vault symlink`
|
|
692
|
+
: `${targetName}: not owned by skillmux, left untouched`,
|
|
693
|
+
);
|
|
694
|
+
continue;
|
|
177
695
|
}
|
|
178
696
|
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
697
|
+
const suffix = dryRun ? " (dry-run)" : "";
|
|
698
|
+
const result = syncTarget(
|
|
699
|
+
{ vaultPath, targetDir, targetName, coreSkillIds: manifest.core.skills, localVaultPaths },
|
|
700
|
+
{ dryRun },
|
|
182
701
|
);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const suffix = dryRun ? " (dry-run)" : "";
|
|
200
|
-
const result = syncTarget(
|
|
201
|
-
{ vaultPath, targetDir, targetName, coreSkillIds: manifest.core.skills },
|
|
202
|
-
{ dryRun },
|
|
702
|
+
console.log(`${targetName}: +${result.added.length} -${result.removed.length}${suffix}`);
|
|
703
|
+
|
|
704
|
+
if (target.project_groups.length > 0) {
|
|
705
|
+
const allGroups = manifest.project ?? {};
|
|
706
|
+
const projectGroups = Object.fromEntries(
|
|
707
|
+
target.project_groups.map((name) => [name, allGroups[name]!]),
|
|
708
|
+
);
|
|
709
|
+
const projectResults = syncProjectTargets(
|
|
710
|
+
{ vaultPath, targetDir, targetName, projectGroups, localVaultPaths },
|
|
711
|
+
{ dryRun },
|
|
712
|
+
);
|
|
713
|
+
for (const projectResult of projectResults) {
|
|
714
|
+
console.log(
|
|
715
|
+
` ${projectResult.group} -> ${projectResult.pinDir}: +${projectResult.added.length} -${projectResult.removed.length}${suffix}`,
|
|
203
716
|
);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (target.project) {
|
|
207
|
-
const projectResults = syncProjectTargets(
|
|
208
|
-
{ vaultPath, targetDir, targetName, projectGroups: manifest.project ?? {} },
|
|
209
|
-
{ dryRun },
|
|
210
|
-
);
|
|
211
|
-
for (const projectResult of projectResults) {
|
|
212
|
-
console.log(
|
|
213
|
-
` ${projectResult.group} -> ${projectResult.pinDir}: +${projectResult.added.length} -${projectResult.removed.length}${suffix}`,
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
717
|
+
}
|
|
217
718
|
}
|
|
719
|
+
}
|
|
218
720
|
}
|
|
219
721
|
|
|
220
722
|
function parseInitArgs(args: string[]): { targets: string[]; yes: boolean } {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
723
|
+
const targets: string[] = [];
|
|
724
|
+
let yes = false;
|
|
725
|
+
for (let i = 0; i < args.length; i++) {
|
|
726
|
+
const option = args[i];
|
|
727
|
+
if (option === "--target") {
|
|
728
|
+
const value = args[i + 1];
|
|
729
|
+
if (!value) throw new Error("--target requires a name");
|
|
730
|
+
targets.push(value);
|
|
731
|
+
i++;
|
|
732
|
+
} else if (option === "--yes") {
|
|
733
|
+
yes = true;
|
|
734
|
+
} else {
|
|
735
|
+
throw new Error(`unknown init option: ${option}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return { targets, yes };
|
|
237
739
|
}
|
|
238
740
|
|
|
239
741
|
async function runInit(args: string[]): Promise<void> {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
const kind = candidate.isSymlink ? "symlink" : "real dir";
|
|
252
|
-
const marked = candidate.alreadyMarked ? ", already skillmux-managed" : "";
|
|
253
|
-
console.log(`${name} (${candidate.path}): ${kind}, ${candidate.skillCount} skills${marked}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (requestedTargets.length === 0) {
|
|
257
|
-
console.log("\nno --target specified — nothing written. Re-run with --target <name> --yes to adopt a surface.");
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (!yes) {
|
|
262
|
-
throw new Error(
|
|
263
|
-
"usage: skillmux init --target <name> [--target <name>...] --yes (interactive per-target confirm is not available non-interactively)",
|
|
264
|
-
);
|
|
742
|
+
const { targets: requestedTargets, yes } = parseInitArgs(args);
|
|
743
|
+
const config = await loadConfig();
|
|
744
|
+
const vaultPath = expandHome(config.vault_path);
|
|
745
|
+
|
|
746
|
+
const candidates = detectSurfaces(surfaceCandidates().map(expandHome));
|
|
747
|
+
for (const candidate of candidates) {
|
|
748
|
+
const name = deriveTargetName(candidate.path);
|
|
749
|
+
if (!candidate.exists) {
|
|
750
|
+
console.log(`${name} (${candidate.path}): not found`);
|
|
751
|
+
continue;
|
|
265
752
|
}
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
753
|
+
const kind = candidate.isSymlink ? "symlink" : "real dir";
|
|
754
|
+
const marked = candidate.alreadyMarked ? ", already skillmux-managed" : "";
|
|
755
|
+
console.log(`${name} (${candidate.path}): ${kind}, ${candidate.skillCount} skills${marked}`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (requestedTargets.length === 0) {
|
|
759
|
+
console.log("\nno --target specified — nothing written. Re-run with --target <name> --yes to adopt a surface.");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (!yes) {
|
|
764
|
+
throw new Error(
|
|
765
|
+
"usage: skillmux init --target <name> [--target <name>...] --yes (interactive per-target confirm is not available non-interactively)",
|
|
269
766
|
);
|
|
270
|
-
|
|
271
|
-
if (!byName.has(name)) throw new Error(`unknown --target "${name}": not among detected surfaces`);
|
|
272
|
-
}
|
|
767
|
+
}
|
|
273
768
|
|
|
274
|
-
|
|
275
|
-
|
|
769
|
+
const byName = new Map(
|
|
770
|
+
candidates.filter((c) => c.exists).map((c) => [deriveTargetName(c.path), c] as const),
|
|
771
|
+
);
|
|
772
|
+
for (const name of requestedTargets) {
|
|
773
|
+
if (!byName.has(name)) throw new Error(`unknown --target "${name}": not among detected surfaces`);
|
|
774
|
+
}
|
|
276
775
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
776
|
+
const confirmedTargets = requestedTargets.map((name) => ({ name, dir: byName.get(name)!.path }));
|
|
777
|
+
applyInit(vaultPath, confirmedTargets);
|
|
778
|
+
|
|
779
|
+
console.log(`\nwrote ${join(vaultPath, "skillmux.toml")}, adopted: ${confirmedTargets.map((t) => t.name).join(", ")}`);
|
|
780
|
+
console.log(`run "skillmux sync" next to materialize [core] skills into these targets.\n`);
|
|
781
|
+
console.log(printLastMile());
|
|
280
782
|
}
|
|
281
783
|
|
|
282
784
|
function parseReportArgs(args: string[]): { server?: string; db?: string; since?: string } {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
785
|
+
let server: string | undefined;
|
|
786
|
+
let db: string | undefined;
|
|
787
|
+
let since: string | undefined;
|
|
788
|
+
for (let i = 0; i < args.length; i++) {
|
|
789
|
+
const option = args[i];
|
|
790
|
+
const value = args[i + 1];
|
|
791
|
+
if (option === "--server") {
|
|
792
|
+
if (!value) throw new Error("--server requires a URL");
|
|
793
|
+
server = value;
|
|
794
|
+
i++;
|
|
795
|
+
} else if (option === "--db") {
|
|
796
|
+
if (!value) throw new Error("--db requires a path");
|
|
797
|
+
db = value;
|
|
798
|
+
i++;
|
|
799
|
+
} else if (option === "--since") {
|
|
800
|
+
if (!value) throw new Error("--since requires a window");
|
|
801
|
+
since = value;
|
|
802
|
+
i++;
|
|
803
|
+
} else {
|
|
804
|
+
throw new Error(`unknown report option: ${option}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (server && db) throw new Error("--server and --db are mutually exclusive");
|
|
808
|
+
return { server, db, since };
|
|
307
809
|
}
|
|
308
810
|
|
|
309
811
|
async function runReport(args: string[]): Promise<void> {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
812
|
+
const { server, db: dbPath, since } = parseReportArgs(args);
|
|
813
|
+
if (!since) throw new Error("usage: skillmux report [--server <url> | --db <path>] --since <window>");
|
|
814
|
+
|
|
815
|
+
if (server) {
|
|
816
|
+
const url = `${server.replace(/\/$/, "")}/stats?since=${encodeURIComponent(since)}`;
|
|
817
|
+
const res = await fetch(url);
|
|
818
|
+
if (!res.ok) throw new Error(`skillmux report --server failed: ${res.status} ${await res.text()}`);
|
|
819
|
+
console.log(renderStatsText((await res.json()) as StatsResponse));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const db = dbPath ? new Database(dbPath, { readonly: true }) : openIndex(expandHome((await loadConfig()).state_dir));
|
|
824
|
+
console.log(renderStatsText(getStats(db, since)));
|
|
825
|
+
db.close();
|
|
324
826
|
}
|
|
325
827
|
|
|
326
828
|
function parseScanArgs(args: string[]): { path?: string; format: "text" | "json"; failOn?: ScanSeverity } {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
829
|
+
let path: string | undefined;
|
|
830
|
+
let format: "text" | "json" = "text";
|
|
831
|
+
let failOn: ScanSeverity | undefined;
|
|
832
|
+
for (let i = 0; i < args.length; i++) {
|
|
833
|
+
const option = args[i];
|
|
834
|
+
if (option === "--format") {
|
|
835
|
+
const value = args[++i];
|
|
836
|
+
if (value !== "text" && value !== "json") throw new Error("--format must be text or json");
|
|
837
|
+
format = value;
|
|
838
|
+
} else if (option === "--fail-on") {
|
|
839
|
+
const value = args[++i];
|
|
840
|
+
if (value !== "low" && value !== "medium" && value !== "high") {
|
|
841
|
+
throw new Error("--fail-on must be low, medium, or high");
|
|
842
|
+
}
|
|
843
|
+
failOn = value;
|
|
844
|
+
} else if (option?.startsWith("--")) {
|
|
845
|
+
throw new Error(`unknown scan option: ${option}`);
|
|
846
|
+
} else if (path !== undefined) {
|
|
847
|
+
throw new Error("skillmux scan accepts at most one <path> argument");
|
|
848
|
+
} else {
|
|
849
|
+
path = option;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return { path, format, failOn };
|
|
351
853
|
}
|
|
352
854
|
|
|
353
855
|
async function runScan(args: string[]): Promise<void> {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
856
|
+
const { path, format, failOn } = parseScanArgs(args);
|
|
857
|
+
const rootPath = path ? expandHome(path) : expandHome((await loadConfig()).vault_path);
|
|
858
|
+
const result = await scanPath(rootPath);
|
|
859
|
+
console.log(format === "json" ? renderScanJson(result) : renderScanText(result));
|
|
860
|
+
process.exitCode = scanExitCode(result.findings, failOn);
|
|
359
861
|
}
|
|
360
862
|
|
|
361
863
|
function parseInstallArgs(args: string[]): {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
864
|
+
repo?: string;
|
|
865
|
+
force: boolean;
|
|
866
|
+
dryRun: boolean;
|
|
867
|
+
failOn?: ScanSeverity;
|
|
366
868
|
} {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
869
|
+
let repo: string | undefined;
|
|
870
|
+
let force = false;
|
|
871
|
+
let dryRun = false;
|
|
872
|
+
let failOn: ScanSeverity | undefined;
|
|
873
|
+
for (let i = 0; i < args.length; i++) {
|
|
874
|
+
const option = args[i];
|
|
875
|
+
if (option === "--force") force = true;
|
|
876
|
+
else if (option === "--dry-run") dryRun = true;
|
|
877
|
+
else if (option === "--fail-on") {
|
|
878
|
+
const value = args[++i];
|
|
879
|
+
if (value !== "low" && value !== "medium" && value !== "high") {
|
|
880
|
+
throw new Error("--fail-on must be low, medium, or high");
|
|
881
|
+
}
|
|
882
|
+
failOn = value;
|
|
883
|
+
} else if (option?.startsWith("--")) {
|
|
884
|
+
throw new Error(`unknown install option: ${option}`);
|
|
885
|
+
} else if (repo !== undefined) {
|
|
886
|
+
throw new Error("skillmux install accepts at most one <repo> argument");
|
|
887
|
+
} else {
|
|
888
|
+
repo = option;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return { repo, force, dryRun, failOn };
|
|
390
892
|
}
|
|
391
893
|
|
|
392
894
|
async function runInstall(args: string[]): Promise<void> {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
895
|
+
const { repo, force, dryRun, failOn } = parseInstallArgs(args);
|
|
896
|
+
if (!repo) {
|
|
897
|
+
throw new Error("usage: skillmux install <repo>[/path] [--force] [--fail-on low|medium|high] [--dry-run]");
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const source = resolveRepoSource(repo);
|
|
901
|
+
const cloneDir = await cloneToTemp(source.url);
|
|
902
|
+
try {
|
|
903
|
+
const resolved = resolveSkillDir(cloneDir, deriveRepoName(source.url), source.skillPath);
|
|
904
|
+
const { findings } = await validateSkillCandidate(resolved.skillId, resolved.dir);
|
|
905
|
+
console.log(renderScanText({ scanned: 1, findings }));
|
|
906
|
+
|
|
907
|
+
if (scanExitCode(findings, failOn) !== 0) {
|
|
908
|
+
process.exitCode = 1;
|
|
909
|
+
console.error(`aborting install: a finding met the --fail-on ${failOn} threshold`);
|
|
910
|
+
return;
|
|
396
911
|
}
|
|
397
912
|
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const { findings } = await validateSkillCandidate(resolved.skillId, resolved.dir);
|
|
403
|
-
console.log(renderScanText({ scanned: 1, findings }));
|
|
404
|
-
|
|
405
|
-
if (scanExitCode(findings, failOn) !== 0) {
|
|
406
|
-
process.exitCode = 1;
|
|
407
|
-
console.error(`aborting install: a finding met the --fail-on ${failOn} threshold`);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const vaultPath = expandHome((await loadConfig()).vault_path);
|
|
412
|
-
if (dryRun) {
|
|
413
|
-
console.log(`dry-run: would install "${resolved.skillId}" into ${join(vaultPath, resolved.skillId)}`);
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const targetDir = installIntoVault(vaultPath, resolved.skillId, resolved.dir, force);
|
|
418
|
-
console.log(`installed "${resolved.skillId}" into ${targetDir}`);
|
|
419
|
-
} finally {
|
|
420
|
-
rmSync(cloneDir, { recursive: true, force: true });
|
|
913
|
+
const vaultPath = expandHome((await loadConfig()).vault_path);
|
|
914
|
+
if (dryRun) {
|
|
915
|
+
console.log(`dry-run: would install "${resolved.skillId}" into ${join(vaultPath, resolved.skillId)}`);
|
|
916
|
+
return;
|
|
421
917
|
}
|
|
918
|
+
|
|
919
|
+
const targetDir = installIntoVault(vaultPath, resolved.skillId, resolved.dir, force);
|
|
920
|
+
console.log(`installed "${resolved.skillId}" into ${targetDir}`);
|
|
921
|
+
} finally {
|
|
922
|
+
rmSync(cloneDir, { recursive: true, force: true });
|
|
923
|
+
}
|
|
422
924
|
}
|
|
423
925
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
process.once("SIGTERM", shutdown);
|
|
440
|
-
process.once("SIGINT", shutdown);
|
|
441
|
-
break;
|
|
926
|
+
function parseCalibrateGenerateDatasetArgs(args: string[]): { vault?: string; out?: string } {
|
|
927
|
+
let vault: string | undefined;
|
|
928
|
+
let out: string | undefined;
|
|
929
|
+
for (let i = 0; i < args.length; i++) {
|
|
930
|
+
const option = args[i];
|
|
931
|
+
if (option === "--vault") {
|
|
932
|
+
const value = args[++i];
|
|
933
|
+
if (!value) throw new Error("--vault requires a path value");
|
|
934
|
+
vault = value;
|
|
935
|
+
} else if (option === "--out") {
|
|
936
|
+
const value = args[++i];
|
|
937
|
+
if (!value) throw new Error("--out requires a file path value");
|
|
938
|
+
out = value;
|
|
939
|
+
} else {
|
|
940
|
+
throw new Error(`unknown calibrate option: ${option}`);
|
|
442
941
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
case "doctor":
|
|
465
|
-
await runDoctor();
|
|
466
|
-
break;
|
|
467
|
-
case "config":
|
|
468
|
-
if (Bun.argv[3] !== "show")
|
|
469
|
-
throw new Error("usage: skillmux config show");
|
|
470
|
-
await showConfig();
|
|
471
|
-
break;
|
|
472
|
-
case "models":
|
|
473
|
-
if (Bun.argv[3] !== "download")
|
|
474
|
-
throw new Error("usage: skillmux models download");
|
|
475
|
-
await runModelDownload();
|
|
476
|
-
break;
|
|
477
|
-
default:
|
|
478
|
-
console.error(
|
|
479
|
-
"usage: skillmux <serve|index|sync|init|report|scan|install|eval|doctor|config show|models download> [--transport stdio|http] [--port N] [--dry-run|--restore-monolith|--install-hook] [--target name --yes] [--server url|--db path] --since window [<path>] [--format text|json] [--fail-on low|medium|high] [<repo>[/path] [--force]]",
|
|
480
|
-
);
|
|
481
|
-
process.exit(2);
|
|
942
|
+
}
|
|
943
|
+
return { vault, out };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function runCalibrateGenerateDataset(args: string[]): Promise<void> {
|
|
947
|
+
const { vault: vaultArg, out: outArg } = parseCalibrateGenerateDatasetArgs(args);
|
|
948
|
+
const config = await loadConfig();
|
|
949
|
+
const vaultPath = expandHome(vaultArg ?? config.vault_path);
|
|
950
|
+
const outPath = expandHome(outArg ?? join(config.state_dir, "queries.json"));
|
|
951
|
+
|
|
952
|
+
const skills = await scanVault(vaultPath);
|
|
953
|
+
const dataset = generateDataset(skills);
|
|
954
|
+
|
|
955
|
+
const parentDir = join(outPath, "..");
|
|
956
|
+
mkdirSync(parentDir, { recursive: true });
|
|
957
|
+
await Bun.write(outPath, JSON.stringify(dataset, null, 2) + "\n");
|
|
958
|
+
console.log(`generated synthetic dataset with ${dataset.length} cases at ${outPath}`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (import.meta.main) {
|
|
962
|
+
await main();
|
|
482
963
|
}
|