@klhapp/skillmux 1.9.2 → 1.10.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 +30 -0
- package/README.md +1 -1
- package/docs/README.md +1 -1
- package/docs/cli.md +74 -3
- package/docs/concepts.md +1 -1
- package/docs/configuration.md +52 -1
- package/docs/deployment.md +10 -6
- package/docs/getting-started.md +1 -1
- package/docs/skill-management.md +49 -0
- package/package.json +1 -1
- package/src/adapters.ts +148 -2
- package/src/cli.ts +302 -1291
- package/src/clients.ts +17 -0
- package/src/commands/audit.ts +54 -51
- package/src/commands/config.ts +11 -12
- package/src/commands/context.ts +103 -0
- package/src/commands/core.ts +5 -1
- package/src/commands/doctor.ts +76 -0
- package/src/commands/eval.ts +10 -13
- package/src/commands/init.ts +621 -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 +8 -5
- package/src/commands/project.ts +37 -11
- package/src/commands/report.ts +66 -0
- package/src/commands/scan.ts +61 -0
- package/src/commands/skill.ts +32 -0
- package/src/commands/sync.ts +232 -0
- package/src/commands/target.ts +18 -6
- package/src/commands/update.ts +11 -5
- package/src/concurrency-limiter.ts +61 -0
- package/src/config-service.ts +1 -51
- package/src/config.ts +5 -0
- 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 -413
- package/src/global-flags.ts +46 -0
- package/src/install.ts +15 -0
- package/src/logger.ts +26 -0
- package/src/output.ts +30 -5
- package/src/redact.ts +52 -0
- package/src/router-core.ts +8 -27
- package/src/server.ts +594 -267
- package/src/toml-writer.ts +51 -0
- package/src/types.ts +7 -0
package/src/clients.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Clients, Config, RemoteRerankerConfig } from "./types";
|
|
2
2
|
import { expandHome } from "./config";
|
|
3
|
+
import { assertHostAllowed } from "./install";
|
|
3
4
|
import type { pipeline as createPipeline } from "@huggingface/transformers";
|
|
4
5
|
|
|
5
6
|
export type RemoteErrorKind = "configuration" | "availability" | "protocol";
|
|
@@ -34,6 +35,17 @@ function authorizationHeaders(
|
|
|
34
35
|
return { authorization: `Bearer ${apiKey}` };
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
function assertInferenceHostAllowed(url: string, allowedHosts: string[] | undefined): void {
|
|
39
|
+
try {
|
|
40
|
+
assertHostAllowed(url, allowedHosts);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new RemoteInferenceError(
|
|
43
|
+
"configuration",
|
|
44
|
+
error instanceof Error ? error.message : String(error),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
function httpFailure(surface: string, status: number): RemoteInferenceError {
|
|
38
50
|
const kind: RemoteErrorKind =
|
|
39
51
|
status === 401 || status === 403
|
|
@@ -196,9 +208,12 @@ async function fetchRerankerScores(
|
|
|
196
208
|
timeoutMs: number,
|
|
197
209
|
query: string,
|
|
198
210
|
docs: { skill_id: string; text: string }[],
|
|
211
|
+
allowedHosts: string[] | undefined,
|
|
199
212
|
): Promise<number[]> {
|
|
200
213
|
if (docs.length === 0) return [];
|
|
201
214
|
|
|
215
|
+
assertInferenceHostAllowed(reranker.endpoint, allowedHosts);
|
|
216
|
+
|
|
202
217
|
let response: Response;
|
|
203
218
|
try {
|
|
204
219
|
response = await fetch(reranker.endpoint, {
|
|
@@ -300,6 +315,7 @@ export function createClients(config: Config): Clients {
|
|
|
300
315
|
}
|
|
301
316
|
|
|
302
317
|
const embedding = config.inference.embedding;
|
|
318
|
+
assertInferenceHostAllowed(embedding.endpoint, config.egress?.allowed_hosts);
|
|
303
319
|
let response: Response;
|
|
304
320
|
try {
|
|
305
321
|
response = await fetch(embedding.endpoint, {
|
|
@@ -344,6 +360,7 @@ export function createClients(config: Config): Clients {
|
|
|
344
360
|
inference.timeout_ms,
|
|
345
361
|
query,
|
|
346
362
|
docs,
|
|
363
|
+
config.egress?.allowed_hosts,
|
|
347
364
|
);
|
|
348
365
|
};
|
|
349
366
|
}
|
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 { TargetAdapter } 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; target: ResolvedContext; adapter: TargetAdapter },
|
|
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,62 +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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
cutoff = new Date(Date.now() - retentionDays * 86_400_000);
|
|
33
|
+
if (dryRun) {
|
|
34
|
+
const counts = await options.adapter.auditCount(olderThan);
|
|
35
|
+
emitSuccess(
|
|
36
|
+
{ isJson: options.isJson },
|
|
37
|
+
counts,
|
|
38
|
+
() =>
|
|
39
|
+
console.log(
|
|
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)`,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
return;
|
|
48
46
|
}
|
|
49
|
-
const cutoffIso = cutoff.toISOString();
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const counts = countPrunable(db, cutoffIso);
|
|
55
|
-
emitSuccess(
|
|
56
|
-
{ isJson: options.isJson },
|
|
57
|
-
{ ...counts, dry_run: true, cutoff: cutoffIso },
|
|
58
|
-
() => console.log(`prune: audit=${counts.audit_deleted} fetch=${counts.fetch_deleted} (dry-run)`),
|
|
59
|
-
);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
!(await confirmIfNeeded({
|
|
65
|
-
confirmed: yes,
|
|
66
|
-
isJson: options.isJson,
|
|
67
|
-
prompt: `prune audit rows older than ${cutoffIso}?`,
|
|
68
|
-
nonInteractiveError: "skillmux audit prune requires --yes when run non-interactively",
|
|
69
|
-
}))
|
|
70
|
-
)
|
|
71
|
-
return;
|
|
72
|
-
|
|
73
|
-
const counts = pruneAuditBefore(db, cutoffIso);
|
|
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) {
|
|
74
51
|
emitSuccess(
|
|
75
52
|
{ isJson: options.isJson },
|
|
76
|
-
{
|
|
77
|
-
() => console.log(
|
|
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"),
|
|
78
55
|
);
|
|
79
|
-
|
|
80
|
-
db.close();
|
|
56
|
+
return;
|
|
81
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
|
+
);
|
|
82
85
|
}
|
package/src/commands/config.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { expandHome, migrateLegacyPaths, resolveConfigPath } from "../config";
|
|
2
2
|
import { type TargetAdapter } from "../adapters";
|
|
3
|
-
import { type
|
|
3
|
+
import { type ResolvedContext } from "../context";
|
|
4
4
|
import { applyConfigInit, planConfigInit, type ConfigInitPlan } from "../setup";
|
|
5
5
|
import { emitSuccess, isInteractive, renderTargetBanner } 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: {
|
|
@@ -40,7 +41,7 @@ export async function handleConfigCommand(
|
|
|
40
41
|
adapter: TargetAdapter,
|
|
41
42
|
sub: string,
|
|
42
43
|
args: string[],
|
|
43
|
-
ctx: { target:
|
|
44
|
+
ctx: { target: 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");
|
|
@@ -164,9 +165,7 @@ export async function handleConfigCommand(
|
|
|
164
165
|
if (sub === "validate") {
|
|
165
166
|
const res = await adapter.configValidate();
|
|
166
167
|
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
167
|
-
console.log(
|
|
168
|
-
res.valid ? "Configuration is valid." : "Configuration is invalid.",
|
|
169
|
-
);
|
|
168
|
+
console.log(res.valid ? "configuration is valid" : "configuration is invalid");
|
|
170
169
|
});
|
|
171
170
|
return;
|
|
172
171
|
}
|
|
@@ -204,11 +203,11 @@ export async function handleConfigCommand(
|
|
|
204
203
|
const res = await adapter.configStatus();
|
|
205
204
|
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
206
205
|
renderTargetBanner(ctx.target);
|
|
207
|
-
console.log(`
|
|
208
|
-
console.log(`
|
|
209
|
-
console.log(`
|
|
210
|
-
console.log(`
|
|
211
|
-
console.log(`
|
|
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
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
renderTargetBanner,
|
|
13
|
+
} from "../output";
|
|
14
|
+
|
|
15
|
+
export async function handleContextCommand(
|
|
16
|
+
sub: string,
|
|
17
|
+
args: string[],
|
|
18
|
+
ctx: { target: ResolvedContext; isJson: boolean },
|
|
19
|
+
) {
|
|
20
|
+
if (sub === "list") {
|
|
21
|
+
const contexts = await listContexts();
|
|
22
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, contexts, () => {
|
|
23
|
+
renderTargetBanner(ctx.target);
|
|
24
|
+
renderTable(
|
|
25
|
+
[
|
|
26
|
+
{ key: "name", header: "NAME" },
|
|
27
|
+
{ key: "server", header: "SERVER" },
|
|
28
|
+
{ key: "token_env", header: "TOKEN_ENV" },
|
|
29
|
+
{ key: "isDefault", header: "DEFAULT" },
|
|
30
|
+
],
|
|
31
|
+
contexts.map((c) => ({
|
|
32
|
+
...c,
|
|
33
|
+
token_env: c.token_env ?? "-",
|
|
34
|
+
isDefault: c.isDefault ? "*" : "",
|
|
35
|
+
})),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (sub === "current") {
|
|
42
|
+
const current = await getCurrentContext();
|
|
43
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, current, () => {
|
|
44
|
+
renderTargetBanner(ctx.target);
|
|
45
|
+
console.log(`Current context: ${current.name} (${current.server})`);
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (sub === "add") {
|
|
51
|
+
const name = args[0];
|
|
52
|
+
let server: string | undefined;
|
|
53
|
+
let tokenEnv: string | undefined;
|
|
54
|
+
for (let i = 1; i < args.length; i++) {
|
|
55
|
+
if (args[i] === "--server") server = args[++i];
|
|
56
|
+
else if (args[i] === "--token-env") tokenEnv = args[++i];
|
|
57
|
+
}
|
|
58
|
+
if (!name || !server) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"usage: skillmux context add <name> --server <url> [--token-env <env_name>]",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
await addContext(name, { server, token_env: tokenEnv });
|
|
64
|
+
emitSuccess(
|
|
65
|
+
{ isJson: ctx.isJson, target: ctx.target },
|
|
66
|
+
{ name, server, token_env: tokenEnv },
|
|
67
|
+
() => {
|
|
68
|
+
console.log(`Added context "${name}" -> ${server}`);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (sub === "use") {
|
|
75
|
+
const name = args[0];
|
|
76
|
+
if (!name) throw new Error("usage: skillmux context use <name>");
|
|
77
|
+
await useContext(name);
|
|
78
|
+
emitSuccess(
|
|
79
|
+
{ isJson: ctx.isJson, target: ctx.target },
|
|
80
|
+
{ default_context: name },
|
|
81
|
+
() => {
|
|
82
|
+
console.log(`Switched default context to "${name}"`);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (sub === "remove") {
|
|
89
|
+
const name = args[0];
|
|
90
|
+
if (!name) throw new Error("usage: skillmux context remove <name>");
|
|
91
|
+
await removeContext(name);
|
|
92
|
+
emitSuccess(
|
|
93
|
+
{ isJson: ctx.isJson, target: ctx.target },
|
|
94
|
+
{ removed: name },
|
|
95
|
+
() => {
|
|
96
|
+
console.log(`Removed context "${name}"`);
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error("usage: skillmux context <add|list|current|use|remove>");
|
|
103
|
+
}
|
package/src/commands/core.ts
CHANGED
|
@@ -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,76 @@
|
|
|
1
|
+
import { resolveConfigPath } from "../config";
|
|
2
|
+
import { diagnose } from "../doctor";
|
|
3
|
+
import { getEffectiveConfig } from "../config-service";
|
|
4
|
+
import type { TargetAdapter } from "../adapters";
|
|
5
|
+
import type { ResolvedContext } from "../context";
|
|
6
|
+
import {
|
|
7
|
+
emitSuccess,
|
|
8
|
+
green,
|
|
9
|
+
red,
|
|
10
|
+
renderTargetBanner,
|
|
11
|
+
} from "../output";
|
|
12
|
+
|
|
13
|
+
export async function runDoctor(options: {
|
|
14
|
+
isJson: boolean;
|
|
15
|
+
target: ResolvedContext;
|
|
16
|
+
adapter: TargetAdapter;
|
|
17
|
+
}): Promise<void> {
|
|
18
|
+
if (options.target.type === "remote") {
|
|
19
|
+
const target = options.target;
|
|
20
|
+
const [status, caps] = await Promise.all([
|
|
21
|
+
options.adapter.configStatus(),
|
|
22
|
+
options.adapter.getCapabilities(),
|
|
23
|
+
]);
|
|
24
|
+
const remoteReport = {
|
|
25
|
+
target: target.name || target.server,
|
|
26
|
+
server: target.server,
|
|
27
|
+
version: status.version,
|
|
28
|
+
deployment_runtime: status.deployment_runtime,
|
|
29
|
+
image_variant: status.image_variant ?? null,
|
|
30
|
+
runtime: status.runtime,
|
|
31
|
+
readiness: status.readiness,
|
|
32
|
+
active_revision: status.active_revision,
|
|
33
|
+
capabilities: caps,
|
|
34
|
+
restart_required_keys: status.restart_required_keys,
|
|
35
|
+
last_reload_error: status.last_reload_error,
|
|
36
|
+
};
|
|
37
|
+
emitSuccess({ isJson: options.isJson, target: options.target }, remoteReport, () => {
|
|
38
|
+
renderTargetBanner(options.target);
|
|
39
|
+
console.log(`server: ${remoteReport.server}`);
|
|
40
|
+
console.log(`version: ${remoteReport.version}`);
|
|
41
|
+
console.log(`deployment runtime: ${remoteReport.deployment_runtime}`);
|
|
42
|
+
console.log(`image variant: ${remoteReport.image_variant ?? "none"}`);
|
|
43
|
+
console.log(`runtime: ${remoteReport.runtime}`);
|
|
44
|
+
console.log(`readiness: ${remoteReport.readiness.status} (${remoteReport.readiness.capability})`);
|
|
45
|
+
console.log(`active revision: ${remoteReport.active_revision}`);
|
|
46
|
+
console.log(`persistence: ${caps.persistence}`);
|
|
47
|
+
console.log(`config read: ${caps.config_read}`);
|
|
48
|
+
console.log(`config write: ${caps.config_write}`);
|
|
49
|
+
if (status.last_reload_error) {
|
|
50
|
+
console.log(`last reload error: ${status.last_reload_error}`);
|
|
51
|
+
}
|
|
52
|
+
if (status.restart_required_keys.length > 0) {
|
|
53
|
+
console.log(`restart required for: ${status.restart_required_keys.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const effective = await getEffectiveConfig(resolveConfigPath());
|
|
60
|
+
const report = await diagnose(effective.effective, process.env, effective.sources);
|
|
61
|
+
emitSuccess({ isJson: options.isJson }, report, () => {
|
|
62
|
+
console.log(`version: ${report.version}`);
|
|
63
|
+
console.log(`runtime: ${report.runtime}`);
|
|
64
|
+
console.log(`image variant: ${report.image_variant ?? "none"}`);
|
|
65
|
+
console.log(`vault path: ${report.vault_path}`);
|
|
66
|
+
console.log(`state directory: ${report.state_dir}`);
|
|
67
|
+
console.log(`inference mode: ${report.mode}`);
|
|
68
|
+
console.log(`routing capability: ${report.capability}`);
|
|
69
|
+
console.log(`retrieval capability: ${report.retrieval_capability}`);
|
|
70
|
+
for (const check of report.checks)
|
|
71
|
+
console.log(
|
|
72
|
+
`${check.ok ? green("ok") : red("fail")}: ${check.name} - ${check.detail}`,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
if (report.checks.some((check) => !check.ok)) process.exitCode = 1;
|
|
76
|
+
}
|
package/src/commands/eval.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
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 { TargetAdapter } 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: TargetAdapter },
|
|
13
14
|
): Promise<void> {
|
|
14
15
|
let since: string | undefined;
|
|
15
16
|
let target: string | undefined;
|
|
@@ -21,8 +22,10 @@ export async function runEvalPromote(
|
|
|
21
22
|
else if (arg === "--target") target = args[++i];
|
|
22
23
|
else if (arg === "--dry-run") dryRun = true;
|
|
23
24
|
else if (arg === "--yes") yes = true;
|
|
24
|
-
else if (arg
|
|
25
|
+
else if (isGlobalFlag(arg, "--json", "--allow-insecure", "--verbose")) {
|
|
25
26
|
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
27
|
+
} else if (isGlobalFlagWithValue(arg)) {
|
|
28
|
+
i++; // skip flag value
|
|
26
29
|
} else if (arg?.startsWith("--")) {
|
|
27
30
|
throw new Error(`unknown eval promote option: ${arg}`);
|
|
28
31
|
}
|
|
@@ -37,18 +40,12 @@ export async function runEvalPromote(
|
|
|
37
40
|
const sinceDate = parseSince(since);
|
|
38
41
|
const sinceIso = sinceDate.toISOString();
|
|
39
42
|
|
|
40
|
-
const
|
|
41
|
-
let candidates: ReturnType<typeof buildPromotedCases>;
|
|
42
|
-
try {
|
|
43
|
-
candidates = buildPromotedCases(queryPromotableFetches(db, sinceIso));
|
|
44
|
-
} finally {
|
|
45
|
-
db.close();
|
|
46
|
-
}
|
|
43
|
+
const candidates = await options.adapter.evalPromote(since);
|
|
47
44
|
|
|
48
45
|
const existing = existsSync(targetPath) ? parseEvalCases(JSON.parse(readFileSync(targetPath, "utf-8"))) : [];
|
|
49
46
|
const { cases: newCases, skipped } = excludeExistingCases(candidates, existing);
|
|
50
47
|
|
|
51
|
-
|
|
48
|
+
warn("promoted eval cases contain raw user queries");
|
|
52
49
|
|
|
53
50
|
if (dryRun) {
|
|
54
51
|
emitSuccess(
|