@klhapp/skillmux 1.9.3 → 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 +15 -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 +1 -1
- package/docs/deployment.md +10 -6
- package/docs/getting-started.md +1 -1
- package/docs/skill-management.md +6 -0
- package/package.json +1 -1
- package/src/adapters.ts +148 -2
- package/src/cli.ts +266 -1299
- package/src/commands/audit.ts +53 -56
- 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 +2 -1
- 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 +2 -1
- package/src/config-service.ts +1 -51
- 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/logger.ts +26 -0
- package/src/output.ts +30 -5
- package/src/router-core.ts +8 -27
- package/src/server.ts +160 -13
- package/src/toml-writer.ts +51 -0
package/src/router-core.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
import { buildAuditRow } from "./audit";
|
|
5
5
|
import { embeddingDimension, embeddingFingerprint, expandHome, loadConfig } from "./config";
|
|
6
6
|
import { RemoteInferenceError } from "./clients";
|
|
7
|
+
import { log } from "./logger";
|
|
8
|
+
import { warn } from "./output";
|
|
7
9
|
import {
|
|
8
10
|
deleteSkill,
|
|
9
11
|
findExactMatch,
|
|
@@ -274,7 +276,7 @@ export async function syncVaultIfNeeded(): Promise<void> {
|
|
|
274
276
|
const invalidIds: string[] = [];
|
|
275
277
|
const skills = await scanVaults(vaultPath, localVaultPaths, (skillId, error) => {
|
|
276
278
|
invalidIds.push(skillId);
|
|
277
|
-
|
|
279
|
+
warn(`keeping previous index entry for ${skillId}: ${error}`);
|
|
278
280
|
});
|
|
279
281
|
const rows = skills.map(toSkillRow);
|
|
280
282
|
// See rebuildIndex: a skill_id invalid in one root can still be valid in
|
|
@@ -361,7 +363,7 @@ async function reindexOneSkill(db: Database, vaultPath: string, skillId: string)
|
|
|
361
363
|
upsertSkill(db, await readSkill(vaultPath, skillId));
|
|
362
364
|
backfillEmbeddings().catch(() => {});
|
|
363
365
|
} catch (error) {
|
|
364
|
-
|
|
366
|
+
warn(`keeping previous index entry for ${skillId}: ${error}`);
|
|
365
367
|
}
|
|
366
368
|
}
|
|
367
369
|
|
|
@@ -395,7 +397,7 @@ export async function startVaultWatcher(): Promise<() => void> {
|
|
|
395
397
|
// A watcher error (e.g. the vault root disappearing) must degrade the index,
|
|
396
398
|
// not crash the server — an unhandled 'error' event would throw.
|
|
397
399
|
watcher.on("error", (error) => {
|
|
398
|
-
|
|
400
|
+
warn(`vault watcher error, live updates paused: ${error}`);
|
|
399
401
|
});
|
|
400
402
|
|
|
401
403
|
return () => {
|
|
@@ -554,14 +556,7 @@ export async function retrieveAndRerank(
|
|
|
554
556
|
retrieval = "lexical";
|
|
555
557
|
degraded_from = clients.rerank ? "reranked" : "hybrid";
|
|
556
558
|
degradation_reason = classifyInferenceError("embedding", embedRes.error);
|
|
557
|
-
|
|
558
|
-
JSON.stringify({
|
|
559
|
-
level: "warn",
|
|
560
|
-
stage: "embedding",
|
|
561
|
-
degraded_from,
|
|
562
|
-
reason: degradation_reason,
|
|
563
|
-
}),
|
|
564
|
-
);
|
|
559
|
+
log.warn("embedding", { degraded_from, reason: degradation_reason });
|
|
565
560
|
} else {
|
|
566
561
|
try {
|
|
567
562
|
const queryVec = (embedRes as Float32Array[])[0];
|
|
@@ -574,14 +569,7 @@ export async function retrieveAndRerank(
|
|
|
574
569
|
retrieval = "lexical";
|
|
575
570
|
degraded_from = clients.rerank ? "reranked" : "hybrid";
|
|
576
571
|
degradation_reason = classifyInferenceError("embedding", embedError);
|
|
577
|
-
|
|
578
|
-
JSON.stringify({
|
|
579
|
-
level: "warn",
|
|
580
|
-
stage: "embedding",
|
|
581
|
-
degraded_from,
|
|
582
|
-
reason: degradation_reason,
|
|
583
|
-
}),
|
|
584
|
-
);
|
|
572
|
+
log.warn("embedding", { degraded_from, reason: degradation_reason });
|
|
585
573
|
}
|
|
586
574
|
}
|
|
587
575
|
}
|
|
@@ -601,14 +589,7 @@ export async function retrieveAndRerank(
|
|
|
601
589
|
scores = null;
|
|
602
590
|
degraded_from = "reranked";
|
|
603
591
|
degradation_reason = classifyInferenceError("reranker", rerankError);
|
|
604
|
-
|
|
605
|
-
JSON.stringify({
|
|
606
|
-
level: "warn",
|
|
607
|
-
stage: "reranker",
|
|
608
|
-
degraded_from,
|
|
609
|
-
reason: degradation_reason,
|
|
610
|
-
}),
|
|
611
|
-
);
|
|
592
|
+
log.warn("reranker", { degraded_from, reason: degradation_reason });
|
|
612
593
|
}
|
|
613
594
|
}
|
|
614
595
|
|
package/src/server.ts
CHANGED
|
@@ -16,13 +16,15 @@ import {
|
|
|
16
16
|
resolveSkill,
|
|
17
17
|
} from "./router-core";
|
|
18
18
|
import { closeRuntime, getRuntime, startVaultWatcher } from "./router-core";
|
|
19
|
-
import { getStats, SINCE_PATTERN } from "./stats";
|
|
19
|
+
import { getStats, parseSince, SINCE_PATTERN } from "./stats";
|
|
20
|
+
import { countPrunable, insertAdminAuditRow, pruneAuditBefore, type AdminAuditChange } from "./db";
|
|
21
|
+
import { buildPromotedCases, evalVault, queryPromotableFetches } from "./eval";
|
|
20
22
|
import { SKILL_ID_PATTERN } from "./vault";
|
|
21
23
|
import { MetricsRegistry } from "./metrics";
|
|
22
24
|
import { ReadinessState } from "./readiness";
|
|
23
25
|
import { initializeRuntime } from "./lifecycle";
|
|
24
26
|
import { buildRedactor } from "./redact";
|
|
25
|
-
import {
|
|
27
|
+
import { redactedErrorLog } from "./logger";
|
|
26
28
|
import type { Clients, Config } from "./types";
|
|
27
29
|
import {
|
|
28
30
|
computeHash,
|
|
@@ -41,16 +43,6 @@ export const readinessState = new ReadinessState();
|
|
|
41
43
|
const DEFAULT_MAX_BODY_BYTES = 1_048_576; // 1 MiB
|
|
42
44
|
const DEFAULT_MAX_CONCURRENT_REQUESTS = 100;
|
|
43
45
|
|
|
44
|
-
/** Pairs a fixed log prefix with a redacted error message, for console.error. */
|
|
45
|
-
export function redactedErrorLog(
|
|
46
|
-
prefix: string,
|
|
47
|
-
err: unknown,
|
|
48
|
-
redact: (text: string) => string,
|
|
49
|
-
): [string, string] {
|
|
50
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
51
|
-
return [prefix, redact(msg)];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
46
|
export interface ServerHandle {
|
|
55
47
|
port?: number;
|
|
56
48
|
statsPort?: number;
|
|
@@ -409,7 +401,7 @@ export async function startServer(opts?: {
|
|
|
409
401
|
allowed_origins: [],
|
|
410
402
|
};
|
|
411
403
|
const origin = req.headers.get("origin") || "";
|
|
412
|
-
const allowedOrigins = serverConfig.allowed_origins;
|
|
404
|
+
const allowedOrigins = serverConfig.allowed_origins || [];
|
|
413
405
|
const isAllowed =
|
|
414
406
|
allowedOrigins.includes("*") || allowedOrigins.includes(origin);
|
|
415
407
|
const allowOriginHeader = isAllowed
|
|
@@ -700,6 +692,161 @@ export async function startServer(opts?: {
|
|
|
700
692
|
});
|
|
701
693
|
}
|
|
702
694
|
|
|
695
|
+
if (
|
|
696
|
+
req.method === "POST" &&
|
|
697
|
+
url.pathname === "/admin/v1/audit/prune"
|
|
698
|
+
) {
|
|
699
|
+
let body: {
|
|
700
|
+
older_than?: string;
|
|
701
|
+
dry_run?: boolean;
|
|
702
|
+
confirm?: boolean;
|
|
703
|
+
} = {};
|
|
704
|
+
try {
|
|
705
|
+
const text = await req.text();
|
|
706
|
+
if (text.trim()) {
|
|
707
|
+
body = JSON.parse(text);
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
return new Response(
|
|
711
|
+
JSON.stringify({
|
|
712
|
+
error: "INVALID_JSON",
|
|
713
|
+
message: "Request body must be valid JSON",
|
|
714
|
+
}),
|
|
715
|
+
{ status: 400, headers },
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const dryRun = body.dry_run ?? false;
|
|
720
|
+
const confirm = body.confirm ?? false;
|
|
721
|
+
if (!dryRun && !confirm) {
|
|
722
|
+
return new Response(
|
|
723
|
+
JSON.stringify({
|
|
724
|
+
error: "CONFIRMATION_REQUIRED",
|
|
725
|
+
message:
|
|
726
|
+
"Non-dry-run audit prune requires confirm: true",
|
|
727
|
+
}),
|
|
728
|
+
{ status: 400, headers },
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const { effective } = await getEffectiveConfig(configPath);
|
|
733
|
+
let cutoff: Date;
|
|
734
|
+
if (body.older_than) {
|
|
735
|
+
try {
|
|
736
|
+
cutoff = parseSince(body.older_than);
|
|
737
|
+
} catch (err: any) {
|
|
738
|
+
return new Response(
|
|
739
|
+
JSON.stringify({
|
|
740
|
+
error: "INVALID_CUTOFF",
|
|
741
|
+
message: err.message,
|
|
742
|
+
}),
|
|
743
|
+
{ status: 400, headers },
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
const retentionDays = effective.audit?.retention_days ?? 90;
|
|
748
|
+
if (retentionDays <= 0) {
|
|
749
|
+
return new Response(
|
|
750
|
+
JSON.stringify({
|
|
751
|
+
audit_deleted: 0,
|
|
752
|
+
fetch_deleted: 0,
|
|
753
|
+
admin_audit_deleted: 0,
|
|
754
|
+
dry_run: dryRun,
|
|
755
|
+
cutoff: null,
|
|
756
|
+
}),
|
|
757
|
+
{ status: 200, headers },
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
cutoff = new Date(Date.now() - retentionDays * 86_400_000);
|
|
761
|
+
}
|
|
762
|
+
const cutoffIso = cutoff.toISOString();
|
|
763
|
+
|
|
764
|
+
const { auditDb } = await getRuntime();
|
|
765
|
+
if (dryRun) {
|
|
766
|
+
const counts = countPrunable(auditDb, cutoffIso);
|
|
767
|
+
return new Response(
|
|
768
|
+
JSON.stringify({
|
|
769
|
+
...counts,
|
|
770
|
+
dry_run: true,
|
|
771
|
+
cutoff: cutoffIso,
|
|
772
|
+
}),
|
|
773
|
+
{ status: 200, headers },
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const counts = pruneAuditBefore(auditDb, cutoffIso);
|
|
778
|
+
return new Response(
|
|
779
|
+
JSON.stringify({
|
|
780
|
+
...counts,
|
|
781
|
+
dry_run: false,
|
|
782
|
+
cutoff: cutoffIso,
|
|
783
|
+
}),
|
|
784
|
+
{ status: 200, headers },
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (req.method === "POST" && url.pathname === "/admin/v1/eval") {
|
|
789
|
+
const report = await evalVault();
|
|
790
|
+
return new Response(JSON.stringify(report), {
|
|
791
|
+
status: 200,
|
|
792
|
+
headers,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (
|
|
797
|
+
req.method === "POST" &&
|
|
798
|
+
url.pathname === "/admin/v1/eval/promote"
|
|
799
|
+
) {
|
|
800
|
+
let body: { since?: string } = {};
|
|
801
|
+
try {
|
|
802
|
+
const text = await req.text();
|
|
803
|
+
if (text.trim()) {
|
|
804
|
+
body = JSON.parse(text);
|
|
805
|
+
}
|
|
806
|
+
} catch {
|
|
807
|
+
return new Response(
|
|
808
|
+
JSON.stringify({
|
|
809
|
+
error: "INVALID_JSON",
|
|
810
|
+
message: "Request body must be valid JSON",
|
|
811
|
+
}),
|
|
812
|
+
{ status: 400, headers },
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (!body.since || typeof body.since !== "string") {
|
|
817
|
+
return new Response(
|
|
818
|
+
JSON.stringify({
|
|
819
|
+
error: "MISSING_SINCE",
|
|
820
|
+
message: "Field 'since' is required",
|
|
821
|
+
}),
|
|
822
|
+
{ status: 400, headers },
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
let sinceDate: Date;
|
|
827
|
+
try {
|
|
828
|
+
sinceDate = parseSince(body.since);
|
|
829
|
+
} catch (err: any) {
|
|
830
|
+
return new Response(
|
|
831
|
+
JSON.stringify({
|
|
832
|
+
error: "INVALID_SINCE",
|
|
833
|
+
message: err.message,
|
|
834
|
+
}),
|
|
835
|
+
{ status: 400, headers },
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
const sinceIso = sinceDate.toISOString();
|
|
839
|
+
|
|
840
|
+
const { auditDb } = await getRuntime();
|
|
841
|
+
const candidates = buildPromotedCases(
|
|
842
|
+
queryPromotableFetches(auditDb, sinceIso),
|
|
843
|
+
);
|
|
844
|
+
return new Response(JSON.stringify({ candidates }), {
|
|
845
|
+
status: 200,
|
|
846
|
+
headers,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
703
850
|
return new Response("Not Found", { status: 404, headers });
|
|
704
851
|
}
|
|
705
852
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function stringifyToml(obj: Record<string, any>): string {
|
|
2
|
+
let out = "";
|
|
3
|
+
const topLevel: Record<string, any> = {};
|
|
4
|
+
const sections: Record<string, any> = {};
|
|
5
|
+
|
|
6
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
7
|
+
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
8
|
+
sections[k] = v;
|
|
9
|
+
} else {
|
|
10
|
+
topLevel[k] = v;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const [k, v] of Object.entries(topLevel)) {
|
|
15
|
+
out += `${k} = ${formatTomlVal(v)}\n`;
|
|
16
|
+
}
|
|
17
|
+
if (Object.keys(topLevel).length > 0) out += "\n";
|
|
18
|
+
|
|
19
|
+
for (const [secName, secObj] of Object.entries(sections)) {
|
|
20
|
+
out += stringifyTomlSection([secName], secObj);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function stringifyTomlSection(path: string[], obj: Record<string, any>): string {
|
|
27
|
+
let out = `[${path.join(".")}]\n`;
|
|
28
|
+
const subSections: Record<string, any> = {};
|
|
29
|
+
|
|
30
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
31
|
+
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
32
|
+
subSections[k] = v;
|
|
33
|
+
} else {
|
|
34
|
+
out += `${k} = ${formatTomlVal(v)}\n`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
out += "\n";
|
|
38
|
+
|
|
39
|
+
for (const [subName, subObj] of Object.entries(subSections)) {
|
|
40
|
+
out += stringifyTomlSection([...path, subName], subObj);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatTomlVal(v: unknown): string {
|
|
47
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
48
|
+
if (typeof v === "boolean" || typeof v === "number") return String(v);
|
|
49
|
+
if (Array.isArray(v)) return JSON.stringify(v);
|
|
50
|
+
return JSON.stringify(v);
|
|
51
|
+
}
|