@klhapp/skillmux 1.9.3 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +19 -19
- package/docs/README.md +4 -4
- package/docs/assets/architecture-dark.svg +39 -32
- package/docs/assets/architecture-light.svg +25 -18
- package/docs/cli.md +147 -36
- package/docs/concepts.md +11 -11
- package/docs/configuration.md +7 -5
- package/docs/deployment.md +10 -6
- package/docs/getting-started.md +18 -14
- package/docs/mcp-routing.md +1 -1
- package/docs/skill-management.md +17 -11
- package/docs/troubleshooting.md +4 -4
- package/package.json +1 -1
- package/src/adapters.ts +157 -11
- package/src/cli.ts +396 -1319
- package/src/commands/audit.ts +53 -56
- package/src/commands/config.ts +33 -26
- package/src/commands/context.ts +104 -0
- package/src/commands/core.ts +7 -3
- package/src/commands/doctor.ts +97 -0
- package/src/commands/eval.ts +22 -15
- package/src/commands/init.ts +672 -0
- package/src/commands/install.ts +132 -0
- package/src/commands/local-vault.ts +60 -0
- package/src/commands/models.ts +10 -0
- package/src/commands/outdated.ts +2 -1
- package/src/commands/project.ts +194 -51
- package/src/commands/report.ts +66 -0
- package/src/commands/scan.ts +61 -0
- package/src/commands/shared.ts +7 -14
- package/src/commands/skill.ts +33 -0
- package/src/commands/sync.ts +232 -0
- package/src/commands/target.ts +45 -15
- package/src/commands/update.ts +2 -1
- package/src/completions.ts +41 -15
- package/src/config-service.ts +4 -54
- package/src/context.ts +8 -3
- package/src/db-audit.ts +286 -0
- package/src/db-index.ts +238 -0
- package/src/db.ts +3 -521
- package/src/global-flags.ts +46 -0
- package/src/init-agents.ts +329 -0
- package/src/init-instructions.ts +47 -28
- package/src/logger.ts +26 -0
- package/src/mcp-registration.ts +89 -0
- package/src/output.ts +80 -18
- package/src/prompts.ts +75 -20
- package/src/router-core.ts +8 -27
- package/src/scan.ts +19 -19
- package/src/server.ts +161 -14
- package/src/toml-writer.ts +51 -0
- package/src/init-clients.ts +0 -220
package/src/commands/audit.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { expandHome, loadConfig } from "../config";
|
|
2
|
-
import { countPrunable, openAudit, pruneAuditBefore } from "../db";
|
|
3
|
-
import { parseSince } from "../stats";
|
|
4
1
|
import { emitSuccess } from "../output";
|
|
5
2
|
import { confirmIfNeeded } from "./shared";
|
|
3
|
+
import type { ContextAdapter } from "../adapters";
|
|
4
|
+
import type { ResolvedContext } from "../context";
|
|
5
|
+
import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
|
|
6
6
|
|
|
7
7
|
export async function runAudit(
|
|
8
8
|
subCommand: string,
|
|
9
9
|
args: string[],
|
|
10
|
-
options: { isJson: boolean; dryRun: boolean },
|
|
10
|
+
options: { isJson: boolean; dryRun: boolean; context: ResolvedContext; adapter: ContextAdapter },
|
|
11
11
|
): Promise<void> {
|
|
12
12
|
if (subCommand !== "prune") {
|
|
13
13
|
throw new Error("usage: skillmux audit prune [--older-than <window>] [--dry-run] [--yes] [--json]");
|
|
@@ -21,68 +21,65 @@ export async function runAudit(
|
|
|
21
21
|
if (arg === "--older-than") olderThan = args[++i];
|
|
22
22
|
else if (arg === "--dry-run") dryRun = true;
|
|
23
23
|
else if (arg === "--yes") yes = true;
|
|
24
|
-
else if (arg
|
|
25
|
-
// handled globally by main()'s
|
|
24
|
+
else if (isGlobalFlag(arg, "--json", "--allow-insecure", "--verbose")) {
|
|
25
|
+
// handled globally by main()'s flags; recognized here so it isn't rejected
|
|
26
|
+
} else if (isGlobalFlagWithValue(arg)) {
|
|
27
|
+
i++; // skip flag value
|
|
26
28
|
} else if (arg?.startsWith("--")) {
|
|
27
29
|
throw new Error(`unknown audit prune option: ${arg}`);
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let cutoff: Date;
|
|
35
|
-
if (olderThan) {
|
|
36
|
-
cutoff = parseSince(olderThan);
|
|
37
|
-
} else {
|
|
38
|
-
const retentionDays = config.audit?.retention_days ?? 90;
|
|
39
|
-
if (retentionDays <= 0) {
|
|
40
|
-
emitSuccess(
|
|
41
|
-
{ isJson: options.isJson },
|
|
42
|
-
{ audit_deleted: 0, fetch_deleted: 0, admin_audit_deleted: 0, dry_run: dryRun, cutoff: null },
|
|
43
|
-
() => console.log("prune: audit.retention_days is 0 (pruning disabled); nothing to do"),
|
|
44
|
-
);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
cutoff = new Date(Date.now() - retentionDays * 86_400_000);
|
|
48
|
-
}
|
|
49
|
-
const cutoffIso = cutoff.toISOString();
|
|
50
|
-
|
|
51
|
-
const db = openAudit(stateDir);
|
|
52
|
-
try {
|
|
53
|
-
if (dryRun) {
|
|
54
|
-
const counts = countPrunable(db, cutoffIso);
|
|
55
|
-
emitSuccess(
|
|
56
|
-
{ isJson: options.isJson },
|
|
57
|
-
{ ...counts, dry_run: true, cutoff: cutoffIso },
|
|
58
|
-
() =>
|
|
59
|
-
console.log(
|
|
60
|
-
`prune: audit=${counts.audit_deleted} fetch=${counts.fetch_deleted} admin_audit=${counts.admin_audit_deleted} (dry-run)`,
|
|
61
|
-
),
|
|
62
|
-
);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
!(await confirmIfNeeded({
|
|
68
|
-
confirmed: yes,
|
|
69
|
-
isJson: options.isJson,
|
|
70
|
-
prompt: `prune audit rows older than ${cutoffIso}?`,
|
|
71
|
-
nonInteractiveError: "skillmux audit prune requires --yes when run non-interactively",
|
|
72
|
-
}))
|
|
73
|
-
)
|
|
74
|
-
return;
|
|
75
|
-
|
|
76
|
-
const counts = pruneAuditBefore(db, cutoffIso);
|
|
33
|
+
if (dryRun) {
|
|
34
|
+
const counts = await options.adapter.auditCount(olderThan);
|
|
77
35
|
emitSuccess(
|
|
78
36
|
{ isJson: options.isJson },
|
|
79
|
-
|
|
37
|
+
counts,
|
|
80
38
|
() =>
|
|
81
39
|
console.log(
|
|
82
|
-
|
|
40
|
+
counts.cutoff === null
|
|
41
|
+
? "prune: audit.retention_days is 0 (pruning disabled); nothing to do"
|
|
42
|
+
: `prune: audit=${counts.audit_deleted} fetch=${counts.fetch_deleted} admin_audit=${counts.admin_audit_deleted} (dry-run)`,
|
|
83
43
|
),
|
|
84
44
|
);
|
|
85
|
-
|
|
86
|
-
db.close();
|
|
45
|
+
return;
|
|
87
46
|
}
|
|
47
|
+
|
|
48
|
+
// Pre-fetch count / cutoff info for confirmation prompt if local or remote
|
|
49
|
+
const countResult = await options.adapter.auditCount(olderThan);
|
|
50
|
+
if (countResult.cutoff === null) {
|
|
51
|
+
emitSuccess(
|
|
52
|
+
{ isJson: options.isJson },
|
|
53
|
+
{ audit_deleted: 0, fetch_deleted: 0, admin_audit_deleted: 0, dry_run: false, cutoff: null },
|
|
54
|
+
() => console.log("prune: audit.retention_days is 0 (pruning disabled); nothing to do"),
|
|
55
|
+
);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
!(await confirmIfNeeded({
|
|
61
|
+
confirmed: yes,
|
|
62
|
+
isJson: options.isJson,
|
|
63
|
+
prompt: `prune audit rows older than ${countResult.cutoff}?`,
|
|
64
|
+
nonInteractiveError: "skillmux audit prune requires --yes when run non-interactively",
|
|
65
|
+
}))
|
|
66
|
+
)
|
|
67
|
+
return;
|
|
68
|
+
|
|
69
|
+
// Reuse the exact cutoff already shown in the confirmation prompt — recomputing
|
|
70
|
+
// it here (if olderThan was left unset) could drift from what was confirmed,
|
|
71
|
+
// e.g. if audit.retention_days changed between the two calls on a remote target.
|
|
72
|
+
const counts = await options.adapter.auditPrune({
|
|
73
|
+
older_than: countResult.cutoff,
|
|
74
|
+
dry_run: false,
|
|
75
|
+
confirm: true,
|
|
76
|
+
});
|
|
77
|
+
emitSuccess(
|
|
78
|
+
{ isJson: options.isJson },
|
|
79
|
+
counts,
|
|
80
|
+
() =>
|
|
81
|
+
console.log(
|
|
82
|
+
`prune: audit=${counts.audit_deleted} fetch=${counts.fetch_deleted} admin_audit=${counts.admin_audit_deleted}`,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
88
85
|
}
|
package/src/commands/config.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { expandHome, migrateLegacyPaths, resolveConfigPath } from "../config";
|
|
2
|
-
import { type
|
|
3
|
-
import { type
|
|
2
|
+
import { type ContextAdapter } from "../adapters";
|
|
3
|
+
import { type ResolvedContext } from "../context";
|
|
4
4
|
import { applyConfigInit, planConfigInit, type ConfigInitPlan } from "../setup";
|
|
5
|
-
import { emitSuccess, isInteractive,
|
|
5
|
+
import { emitSuccess, isInteractive, renderContextBanner, unknownSubcommandError } from "../output";
|
|
6
6
|
import { confirmAction } from "./shared";
|
|
7
|
+
import { isGlobalFlag } from "../global-flags";
|
|
7
8
|
function emitConfigInitOutcome(
|
|
8
9
|
ctx: { isJson: boolean },
|
|
9
10
|
opts: {
|
|
@@ -37,10 +38,10 @@ function emitConfigInitOutcome(
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export async function handleConfigCommand(
|
|
40
|
-
adapter:
|
|
41
|
+
adapter: ContextAdapter,
|
|
41
42
|
sub: string,
|
|
42
43
|
args: string[],
|
|
43
|
-
ctx: {
|
|
44
|
+
ctx: { context: ResolvedContext; isJson: boolean; dryRun: boolean },
|
|
44
45
|
) {
|
|
45
46
|
if (sub === "init") {
|
|
46
47
|
let vaultPath: string | undefined;
|
|
@@ -53,7 +54,7 @@ export async function handleConfigCommand(
|
|
|
53
54
|
throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
54
55
|
} else if (option === "--yes") {
|
|
55
56
|
yes = true;
|
|
56
|
-
} else if (option
|
|
57
|
+
} else if (isGlobalFlag(option, "--dry-run", "--json")) {
|
|
57
58
|
continue;
|
|
58
59
|
} else {
|
|
59
60
|
throw new Error(`unknown config init option: ${option}`);
|
|
@@ -95,7 +96,7 @@ export async function handleConfigCommand(
|
|
|
95
96
|
if (!ctx.isJson && isInteractive()) {
|
|
96
97
|
if (
|
|
97
98
|
!(await confirmAction(
|
|
98
|
-
`
|
|
99
|
+
`create ${plan.configPath} with vault_path ${plan.vaultPath}?`,
|
|
99
100
|
))
|
|
100
101
|
) {
|
|
101
102
|
console.log("config init cancelled; nothing written");
|
|
@@ -126,8 +127,8 @@ export async function handleConfigCommand(
|
|
|
126
127
|
if (sub === "show") {
|
|
127
128
|
const withSources = args.includes("--sources");
|
|
128
129
|
const data = await adapter.getConfigShow();
|
|
129
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
130
|
-
|
|
130
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, data, () => {
|
|
131
|
+
renderContextBanner(ctx.context);
|
|
131
132
|
if (withSources) {
|
|
132
133
|
const policy =
|
|
133
134
|
data.effective.config?.environment_overrides === false
|
|
@@ -150,7 +151,7 @@ export async function handleConfigCommand(
|
|
|
150
151
|
if (!key) throw new Error("usage: skillmux config get <key>");
|
|
151
152
|
const val = await adapter.getConfigGet(key);
|
|
152
153
|
emitSuccess(
|
|
153
|
-
{ isJson: ctx.isJson,
|
|
154
|
+
{ isJson: ctx.isJson, context: ctx.context },
|
|
154
155
|
{ key, value: val },
|
|
155
156
|
() => {
|
|
156
157
|
console.log(
|
|
@@ -163,18 +164,16 @@ export async function handleConfigCommand(
|
|
|
163
164
|
|
|
164
165
|
if (sub === "validate") {
|
|
165
166
|
const res = await adapter.configValidate();
|
|
166
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
167
|
-
console.log(
|
|
168
|
-
res.valid ? "Configuration is valid." : "Configuration is invalid.",
|
|
169
|
-
);
|
|
167
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
168
|
+
console.log(res.valid ? "configuration is valid" : "configuration is invalid");
|
|
170
169
|
});
|
|
171
170
|
return;
|
|
172
171
|
}
|
|
173
172
|
|
|
174
173
|
if (sub === "diff") {
|
|
175
174
|
const res = await adapter.configDiff();
|
|
176
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
177
|
-
|
|
175
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
176
|
+
renderContextBanner(ctx.context);
|
|
178
177
|
console.log(JSON.stringify(res.diff, null, 2));
|
|
179
178
|
});
|
|
180
179
|
return;
|
|
@@ -187,8 +186,8 @@ export async function handleConfigCommand(
|
|
|
187
186
|
throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
|
|
188
187
|
}
|
|
189
188
|
const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
|
|
190
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
191
|
-
|
|
189
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
190
|
+
renderContextBanner(ctx.context);
|
|
192
191
|
const prefix = ctx.dryRun ? "[dry-run] " : "";
|
|
193
192
|
console.log(
|
|
194
193
|
`${prefix}${key}: ${JSON.stringify(res.prior_val)} -> ${JSON.stringify(res.resulting_val)}`,
|
|
@@ -202,16 +201,24 @@ export async function handleConfigCommand(
|
|
|
202
201
|
|
|
203
202
|
if (sub === "status") {
|
|
204
203
|
const res = await adapter.configStatus();
|
|
205
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
206
|
-
|
|
207
|
-
console.log(`
|
|
208
|
-
console.log(`
|
|
209
|
-
console.log(`
|
|
210
|
-
console.log(`
|
|
211
|
-
console.log(`
|
|
204
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
205
|
+
renderContextBanner(ctx.context);
|
|
206
|
+
console.log(`runtime: ${res.runtime}`);
|
|
207
|
+
console.log(`deployment runtime: ${res.deployment_runtime}`);
|
|
208
|
+
console.log(`image variant: ${res.image_variant ?? "none"}`);
|
|
209
|
+
console.log(`active revision: ${res.active_revision}`);
|
|
210
|
+
console.log(`readiness: ${res.readiness.status}`);
|
|
212
211
|
});
|
|
213
212
|
return;
|
|
214
213
|
}
|
|
215
214
|
|
|
216
|
-
throw
|
|
215
|
+
throw unknownSubcommandError("config", sub, [
|
|
216
|
+
"init",
|
|
217
|
+
"show",
|
|
218
|
+
"get",
|
|
219
|
+
"set",
|
|
220
|
+
"validate",
|
|
221
|
+
"diff",
|
|
222
|
+
"status",
|
|
223
|
+
]);
|
|
217
224
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addContext,
|
|
3
|
+
getCurrentContext,
|
|
4
|
+
listContexts,
|
|
5
|
+
removeContext,
|
|
6
|
+
useContext,
|
|
7
|
+
type ResolvedContext,
|
|
8
|
+
} from "../context";
|
|
9
|
+
import {
|
|
10
|
+
emitSuccess,
|
|
11
|
+
renderTable,
|
|
12
|
+
renderContextBanner,
|
|
13
|
+
unknownSubcommandError,
|
|
14
|
+
} from "../output";
|
|
15
|
+
|
|
16
|
+
export async function handleContextCommand(
|
|
17
|
+
sub: string,
|
|
18
|
+
args: string[],
|
|
19
|
+
ctx: { context: ResolvedContext; isJson: boolean },
|
|
20
|
+
) {
|
|
21
|
+
if (sub === "list") {
|
|
22
|
+
const contexts = await listContexts();
|
|
23
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, contexts, () => {
|
|
24
|
+
renderContextBanner(ctx.context);
|
|
25
|
+
renderTable(
|
|
26
|
+
[
|
|
27
|
+
{ key: "name", header: "NAME" },
|
|
28
|
+
{ key: "server", header: "SERVER" },
|
|
29
|
+
{ key: "token_env", header: "TOKEN_ENV" },
|
|
30
|
+
{ key: "isDefault", header: "DEFAULT" },
|
|
31
|
+
],
|
|
32
|
+
contexts.map((c) => ({
|
|
33
|
+
...c,
|
|
34
|
+
token_env: c.token_env ?? "-",
|
|
35
|
+
isDefault: c.isDefault ? "*" : "",
|
|
36
|
+
})),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (sub === "current") {
|
|
43
|
+
const current = await getCurrentContext();
|
|
44
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, current, () => {
|
|
45
|
+
renderContextBanner(ctx.context);
|
|
46
|
+
console.log(`Current context: ${current.name} (${current.server})`);
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (sub === "add") {
|
|
52
|
+
const name = args[0];
|
|
53
|
+
let server: string | undefined;
|
|
54
|
+
let tokenEnv: string | undefined;
|
|
55
|
+
for (let i = 1; i < args.length; i++) {
|
|
56
|
+
if (args[i] === "--server") server = args[++i];
|
|
57
|
+
else if (args[i] === "--token-env") tokenEnv = args[++i];
|
|
58
|
+
}
|
|
59
|
+
if (!name || !server) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"usage: skillmux context add <name> --server <url> [--token-env <env_name>]",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
await addContext(name, { server, token_env: tokenEnv });
|
|
65
|
+
emitSuccess(
|
|
66
|
+
{ isJson: ctx.isJson, context: ctx.context },
|
|
67
|
+
{ name, server, token_env: tokenEnv },
|
|
68
|
+
() => {
|
|
69
|
+
console.log(`Added context "${name}" -> ${server}`);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (sub === "use") {
|
|
76
|
+
const name = args[0];
|
|
77
|
+
if (!name) throw new Error("usage: skillmux context use <name>");
|
|
78
|
+
await useContext(name);
|
|
79
|
+
emitSuccess(
|
|
80
|
+
{ isJson: ctx.isJson, context: ctx.context },
|
|
81
|
+
{ default_context: name },
|
|
82
|
+
() => {
|
|
83
|
+
console.log(`Switched default context to "${name}"`);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (sub === "remove") {
|
|
90
|
+
const name = args[0];
|
|
91
|
+
if (!name) throw new Error("usage: skillmux context remove <name>");
|
|
92
|
+
await removeContext(name);
|
|
93
|
+
emitSuccess(
|
|
94
|
+
{ isJson: ctx.isJson, context: ctx.context },
|
|
95
|
+
{ removed: name },
|
|
96
|
+
() => {
|
|
97
|
+
console.log(`Removed context "${name}"`);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw unknownSubcommandError("context", sub, ["add", "list", "current", "use", "remove"]);
|
|
104
|
+
}
|
package/src/commands/core.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expandHome } from "../config";
|
|
2
2
|
import { pinCore, unpinCore, validateManifest, writeManifestAtomic } from "../manifest";
|
|
3
|
-
import { emitSuccess } from "../output";
|
|
3
|
+
import { emitSuccess, unknownSubcommandError } from "../output";
|
|
4
4
|
import { confirmIfNeeded, loadManifestContext } from "./shared";
|
|
5
5
|
export async function runCore(
|
|
6
6
|
subCommand: string,
|
|
@@ -8,7 +8,7 @@ export async function runCore(
|
|
|
8
8
|
options: { isJson: boolean; dryRun: boolean },
|
|
9
9
|
): Promise<void> {
|
|
10
10
|
if (subCommand !== "pin" && subCommand !== "unpin") {
|
|
11
|
-
throw
|
|
11
|
+
throw unknownSubcommandError("core", subCommand, ["pin", "unpin"]);
|
|
12
12
|
}
|
|
13
13
|
const skillIds = args.filter((arg) => !arg.startsWith("-"));
|
|
14
14
|
if (skillIds.length === 0) {
|
|
@@ -48,5 +48,9 @@ export async function runCore(
|
|
|
48
48
|
)
|
|
49
49
|
return;
|
|
50
50
|
writeManifestAtomic(manifestPath, updated);
|
|
51
|
-
|
|
51
|
+
emitSuccess(
|
|
52
|
+
{ isJson: options.isJson },
|
|
53
|
+
{ subcommand: subCommand, skill_ids: skillIds },
|
|
54
|
+
() => console.log(`${subCommand}: [core] ${skillIds.join(", ")}`),
|
|
55
|
+
);
|
|
52
56
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { resolveConfigPath } from "../config";
|
|
2
|
+
import { diagnose } from "../doctor";
|
|
3
|
+
import { getEffectiveConfig } from "../config-service";
|
|
4
|
+
import type { ContextAdapter } from "../adapters";
|
|
5
|
+
import type { ResolvedContext } from "../context";
|
|
6
|
+
import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
|
|
7
|
+
import {
|
|
8
|
+
emitSuccess,
|
|
9
|
+
green,
|
|
10
|
+
red,
|
|
11
|
+
renderContextBanner,
|
|
12
|
+
} from "../output";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* doctor takes no options of its own, but it still has to reject unknown ones
|
|
16
|
+
* rather than silently ignoring them the way every other command does.
|
|
17
|
+
*/
|
|
18
|
+
export function parseDoctorArgs(args: readonly string[]): void {
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const option = args[i];
|
|
21
|
+
if (isGlobalFlag(option, "--json", "--allow-insecure", "--verbose")) {
|
|
22
|
+
// handled globally by main(); recognized here so it isn't rejected
|
|
23
|
+
} else if (isGlobalFlagWithValue(option)) {
|
|
24
|
+
// handled globally by main()'s resolveContext(); skip its value too
|
|
25
|
+
i++;
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error(`unknown doctor option: ${option}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runDoctor(options: {
|
|
33
|
+
isJson: boolean;
|
|
34
|
+
context: ResolvedContext;
|
|
35
|
+
adapter: ContextAdapter;
|
|
36
|
+
args?: readonly string[];
|
|
37
|
+
}): Promise<void> {
|
|
38
|
+
parseDoctorArgs(options.args ?? []);
|
|
39
|
+
if (options.context.type === "remote") {
|
|
40
|
+
const context = options.context;
|
|
41
|
+
const [status, caps] = await Promise.all([
|
|
42
|
+
options.adapter.configStatus(),
|
|
43
|
+
options.adapter.getCapabilities(),
|
|
44
|
+
]);
|
|
45
|
+
const remoteReport = {
|
|
46
|
+
target: context.name || context.server,
|
|
47
|
+
server: context.server,
|
|
48
|
+
version: status.version,
|
|
49
|
+
deployment_runtime: status.deployment_runtime,
|
|
50
|
+
image_variant: status.image_variant ?? null,
|
|
51
|
+
runtime: status.runtime,
|
|
52
|
+
readiness: status.readiness,
|
|
53
|
+
active_revision: status.active_revision,
|
|
54
|
+
capabilities: caps,
|
|
55
|
+
restart_required_keys: status.restart_required_keys,
|
|
56
|
+
last_reload_error: status.last_reload_error,
|
|
57
|
+
};
|
|
58
|
+
emitSuccess({ isJson: options.isJson, target: options.context }, remoteReport, () => {
|
|
59
|
+
renderContextBanner(options.context);
|
|
60
|
+
console.log(`server: ${remoteReport.server}`);
|
|
61
|
+
console.log(`version: ${remoteReport.version}`);
|
|
62
|
+
console.log(`deployment runtime: ${remoteReport.deployment_runtime}`);
|
|
63
|
+
console.log(`image variant: ${remoteReport.image_variant ?? "none"}`);
|
|
64
|
+
console.log(`runtime: ${remoteReport.runtime}`);
|
|
65
|
+
console.log(`readiness: ${remoteReport.readiness.status} (${remoteReport.readiness.capability})`);
|
|
66
|
+
console.log(`active revision: ${remoteReport.active_revision}`);
|
|
67
|
+
console.log(`persistence: ${caps.persistence}`);
|
|
68
|
+
console.log(`config read: ${caps.config_read}`);
|
|
69
|
+
console.log(`config write: ${caps.config_write}`);
|
|
70
|
+
if (status.last_reload_error) {
|
|
71
|
+
console.log(`last reload error: ${status.last_reload_error}`);
|
|
72
|
+
}
|
|
73
|
+
if (status.restart_required_keys.length > 0) {
|
|
74
|
+
console.log(`restart required for: ${status.restart_required_keys.join(", ")}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const effective = await getEffectiveConfig(resolveConfigPath());
|
|
81
|
+
const report = await diagnose(effective.effective, process.env, effective.sources);
|
|
82
|
+
emitSuccess({ isJson: options.isJson }, report, () => {
|
|
83
|
+
console.log(`version: ${report.version}`);
|
|
84
|
+
console.log(`runtime: ${report.runtime}`);
|
|
85
|
+
console.log(`image variant: ${report.image_variant ?? "none"}`);
|
|
86
|
+
console.log(`vault path: ${report.vault_path}`);
|
|
87
|
+
console.log(`state directory: ${report.state_dir}`);
|
|
88
|
+
console.log(`inference mode: ${report.mode}`);
|
|
89
|
+
console.log(`routing capability: ${report.capability}`);
|
|
90
|
+
console.log(`retrieval capability: ${report.retrieval_capability}`);
|
|
91
|
+
for (const check of report.checks)
|
|
92
|
+
console.log(
|
|
93
|
+
`${check.ok ? green("ok") : red("fail")}: ${check.name} - ${check.detail}`,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
if (report.checks.some((check) => !check.ok)) process.exitCode = 1;
|
|
97
|
+
}
|
package/src/commands/eval.ts
CHANGED
|
@@ -1,54 +1,61 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { expandHome, loadConfig } from "../config";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { emitSuccess } from "../output";
|
|
4
|
+
import { excludeExistingCases, parseEvalCases } from "../eval";
|
|
5
|
+
import { emitSuccess, warn } from "../output";
|
|
7
6
|
import { parseSince } from "../stats";
|
|
8
7
|
import { confirmIfNeeded } from "./shared";
|
|
8
|
+
import type { ContextAdapter } from "../adapters";
|
|
9
|
+
import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
|
|
9
10
|
|
|
10
11
|
export async function runEvalPromote(
|
|
11
12
|
args: string[],
|
|
12
|
-
options: { isJson: boolean; dryRun: boolean },
|
|
13
|
+
options: { isJson: boolean; dryRun: boolean; adapter: ContextAdapter },
|
|
13
14
|
): Promise<void> {
|
|
14
15
|
let since: string | undefined;
|
|
16
|
+
let out: string | undefined;
|
|
15
17
|
let target: string | undefined;
|
|
16
18
|
let dryRun = options.dryRun;
|
|
17
19
|
let yes = false;
|
|
18
20
|
for (let i = 0; i < args.length; i++) {
|
|
19
21
|
const arg = args[i];
|
|
20
22
|
if (arg === "--since") since = args[++i];
|
|
23
|
+
else if (arg === "--out") out = args[++i];
|
|
21
24
|
else if (arg === "--target") target = args[++i];
|
|
22
25
|
else if (arg === "--dry-run") dryRun = true;
|
|
23
26
|
else if (arg === "--yes") yes = true;
|
|
24
|
-
else if (arg
|
|
27
|
+
else if (isGlobalFlag(arg, "--json", "--allow-insecure", "--verbose")) {
|
|
25
28
|
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
29
|
+
} else if (isGlobalFlagWithValue(arg)) {
|
|
30
|
+
i++; // skip flag value
|
|
26
31
|
} else if (arg?.startsWith("--")) {
|
|
27
32
|
throw new Error(`unknown eval promote option: ${arg}`);
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
if (!since) {
|
|
31
|
-
throw new Error("usage: skillmux eval promote --since <window> [--
|
|
36
|
+
throw new Error("usage: skillmux eval promote --since <window> [--out <path>] [--dry-run] [--yes] [--json]");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (out !== undefined && target !== undefined && out !== target) {
|
|
40
|
+
throw new Error("cannot specify conflicting --out and --target");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (target !== undefined) {
|
|
44
|
+
warn("--target is deprecated, use --out instead");
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
const config = await loadConfig();
|
|
35
48
|
const stateDir = expandHome(config.state_dir);
|
|
36
|
-
const targetPath = target ?? join(stateDir, "eval-observed.json");
|
|
49
|
+
const targetPath = out ?? target ?? join(stateDir, "eval-observed.json");
|
|
37
50
|
const sinceDate = parseSince(since);
|
|
38
51
|
const sinceIso = sinceDate.toISOString();
|
|
39
52
|
|
|
40
|
-
const
|
|
41
|
-
let candidates: ReturnType<typeof buildPromotedCases>;
|
|
42
|
-
try {
|
|
43
|
-
candidates = buildPromotedCases(queryPromotableFetches(db, sinceIso));
|
|
44
|
-
} finally {
|
|
45
|
-
db.close();
|
|
46
|
-
}
|
|
53
|
+
const candidates = await options.adapter.evalPromote(since);
|
|
47
54
|
|
|
48
55
|
const existing = existsSync(targetPath) ? parseEvalCases(JSON.parse(readFileSync(targetPath, "utf-8"))) : [];
|
|
49
56
|
const { cases: newCases, skipped } = excludeExistingCases(candidates, existing);
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
warn("promoted eval cases contain raw user queries");
|
|
52
59
|
|
|
53
60
|
if (dryRun) {
|
|
54
61
|
emitSuccess(
|