@klhapp/skillmux 1.6.0 → 1.7.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 +14 -0
- package/README.md +7 -7
- package/config.example.toml +5 -0
- package/config.remote.example.toml +4 -7
- package/docs/README.md +4 -4
- package/docs/assets/architecture.svg +1 -1
- package/docs/cli.md +4 -57
- package/docs/concepts.md +12 -14
- package/docs/configuration.md +10 -11
- package/docs/deployment.md +11 -8
- package/docs/getting-started.md +1 -1
- package/docs/mcp-routing.md +28 -23
- package/docs/ranked-shortlist-migration.md +160 -0
- package/docs/schema.json +37 -139
- package/docs/skill-management.md +7 -2
- package/docs/troubleshooting.md +12 -14
- package/package.json +1 -1
- package/src/adapters.ts +1 -536
- package/src/audit.ts +1 -3
- package/src/cli.ts +19 -254
- package/src/completions.ts +0 -4
- package/src/config-service.ts +19 -32
- package/src/config-watcher.ts +2 -6
- package/src/config.ts +61 -60
- package/src/db.ts +32 -18
- package/src/doctor.ts +1 -84
- package/src/eval.ts +143 -60
- package/src/init.ts +4 -5
- package/src/metrics.ts +1 -13
- package/src/router-core.ts +42 -149
- package/src/server.ts +13 -27
- package/src/stats.ts +126 -57
- package/src/types.ts +8 -39
- package/docs/calibration.md +0 -198
- package/src/calibrate.ts +0 -1775
- package/src/config-mutation.ts +0 -65
- package/src/dataset-generator.ts +0 -119
- package/src/decision.ts +0 -45
package/src/init.ts
CHANGED
|
@@ -357,15 +357,14 @@ export function applyInit(
|
|
|
357
357
|
return manifest;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
/** Verbatim from docs/sdd/skr-cli/think.md §3.3 — the shared instruction-stack paragraph. */
|
|
361
360
|
export const DISCOVERY_PARAGRAPH =
|
|
362
361
|
"Skills: only a curated core is loaded statically. Before improvising a " +
|
|
363
362
|
"multi-step workflow, or when a task smells like a domain you have no loaded " +
|
|
364
363
|
"skill for (career/resume, trading, SEO, i18n, design, one-off tooling), " +
|
|
365
|
-
"call `resolve_skill` with a one-line task description. `
|
|
366
|
-
"
|
|
367
|
-
"`fetch_skill
|
|
368
|
-
"
|
|
364
|
+
"call `resolve_skill` with a one-line task description. `resolve_skill` " +
|
|
365
|
+
"returns a ranked shortlist; inspect candidates, fetch one or more " +
|
|
366
|
+
"relevant skills with `fetch_skill`, or ignore all and proceed normally " +
|
|
367
|
+
"when none are useful.";
|
|
369
368
|
|
|
370
369
|
export const MCP_REGISTRATION_SNIPPET = JSON.stringify(
|
|
371
370
|
{ mcpServers: { "skillmux": { command: "skillmux", args: ["serve"] } } },
|
package/src/metrics.ts
CHANGED
|
@@ -8,8 +8,7 @@ type MetricsDeploymentIdentity = Pick<
|
|
|
8
8
|
|
|
9
9
|
export class MetricsRegistry {
|
|
10
10
|
private requests = new Map<string, number>();
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
|
|
13
12
|
private buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0];
|
|
14
13
|
private latencyBuckets = new Array(this.buckets.length).fill(0);
|
|
15
14
|
private latencyInf = 0;
|
|
@@ -34,10 +33,6 @@ export class MetricsRegistry {
|
|
|
34
33
|
this.requests.set(method, (this.requests.get(method) || 0) + 1);
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
recordResolveOutcome(outcome: string) {
|
|
38
|
-
this.outcomes.set(outcome, (this.outcomes.get(outcome) || 0) + 1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
36
|
recordDegradation(stage: "embedding" | "reranker", reason: string) {
|
|
42
37
|
const key = `${stage}:${reason}`;
|
|
43
38
|
this.degradations.set(key, (this.degradations.get(key) || 0) + 1);
|
|
@@ -72,13 +67,6 @@ export class MetricsRegistry {
|
|
|
72
67
|
lines.push(`skill_router_requests_total{method="${method}"} ${count}`);
|
|
73
68
|
}
|
|
74
69
|
|
|
75
|
-
// Resolve outcomes total
|
|
76
|
-
lines.push("# HELP skill_router_resolve_outcomes_total Total count of resolve_skill query outcomes.");
|
|
77
|
-
lines.push("# TYPE skill_router_resolve_outcomes_total counter");
|
|
78
|
-
for (const [outcome, count] of this.outcomes) {
|
|
79
|
-
lines.push(`skill_router_resolve_outcomes_total{outcome="${outcome}"} ${count}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
70
|
// Latency histogram
|
|
83
71
|
lines.push("# HELP skill_router_resolve_latency_seconds Latency histogram of resolve_skill executions.");
|
|
84
72
|
lines.push("# TYPE skill_router_resolve_latency_seconds histogram");
|
package/src/router-core.ts
CHANGED
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
vectorTopK,
|
|
24
24
|
} from "./db";
|
|
25
25
|
import type { SkillRow } from "./db";
|
|
26
|
-
import { decideResolveOutcome, type Decision } from "./decision";
|
|
27
26
|
import type {
|
|
28
27
|
RankedCandidate,
|
|
29
28
|
RetrievalCapability,
|
|
@@ -54,7 +53,6 @@ function maxVaultMtime(vaultPath: string, localVaultPaths: string[]): number {
|
|
|
54
53
|
|
|
55
54
|
export { buildAuditRow } from "./audit";
|
|
56
55
|
export { loadConfig } from "./config";
|
|
57
|
-
export { decideResolveOutcome } from "./decision";
|
|
58
56
|
export type * from "./types";
|
|
59
57
|
|
|
60
58
|
const NO_MATCH_MESSAGE =
|
|
@@ -370,87 +368,40 @@ export async function resolveSkill(input: ResolveSkillInput): Promise<ResolveRes
|
|
|
370
368
|
const { config, db } = await getEnv();
|
|
371
369
|
await syncVaultIfNeeded();
|
|
372
370
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
content_sha256: delivery.content_sha256,
|
|
383
|
-
score: 1.0,
|
|
384
|
-
margin: 1.0,
|
|
385
|
-
body: delivery.body,
|
|
386
|
-
files: delivery.files,
|
|
387
|
-
};
|
|
388
|
-
insertAudit(
|
|
389
|
-
db,
|
|
390
|
-
buildAuditRow({
|
|
391
|
-
id: 0,
|
|
392
|
-
ts: new Date().toISOString(),
|
|
393
|
-
query: input.query,
|
|
394
|
-
outcome: "matched",
|
|
395
|
-
retrieval: "exact",
|
|
396
|
-
candidates: [{ skill_id: exactMatch.skill_id, score: 1.0 }],
|
|
397
|
-
selected_skill_id: exactMatch.skill_id,
|
|
398
|
-
latency_ms: Math.round(performance.now() - t0),
|
|
399
|
-
}),
|
|
400
|
-
);
|
|
401
|
-
return result;
|
|
371
|
+
if (input.top_k !== undefined) {
|
|
372
|
+
if (!Number.isInteger(input.top_k) || input.top_k < 1) {
|
|
373
|
+
throw new Error(`Invalid top_k: ${input.top_k} must be a positive integer`);
|
|
374
|
+
}
|
|
375
|
+
if (input.top_k > config.output.max_top_k) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Invalid top_k: ${input.top_k} exceeds max_top_k of ${config.output.max_top_k}`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
402
380
|
}
|
|
403
381
|
|
|
382
|
+
const effectiveTopK = input.top_k ?? config.output.top_k;
|
|
404
383
|
const retrievalResult = await retrieveAndRerank(input);
|
|
405
384
|
const { retrieval, candidates: rankedCandidates } = retrievalResult;
|
|
406
385
|
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
body: delivery.body,
|
|
427
|
-
files: delivery.files,
|
|
428
|
-
};
|
|
429
|
-
} else if (decision.outcome === "ambiguous") {
|
|
430
|
-
result = {
|
|
431
|
-
outcome: "ambiguous",
|
|
432
|
-
retrieval,
|
|
433
|
-
...(retrievalResult.degraded_from
|
|
434
|
-
? {
|
|
435
|
-
degraded_from: retrievalResult.degraded_from,
|
|
436
|
-
degradation_reason: retrievalResult.degradation_reason,
|
|
437
|
-
}
|
|
438
|
-
: {}),
|
|
439
|
-
candidates: decision.candidates.map(({ score: _score, ...candidate }) => candidate),
|
|
440
|
-
};
|
|
441
|
-
} else {
|
|
442
|
-
result = {
|
|
443
|
-
outcome: "no_match",
|
|
444
|
-
retrieval,
|
|
445
|
-
...(retrievalResult.degraded_from
|
|
446
|
-
? {
|
|
447
|
-
degraded_from: retrievalResult.degraded_from,
|
|
448
|
-
degradation_reason: retrievalResult.degradation_reason,
|
|
449
|
-
}
|
|
450
|
-
: {}),
|
|
451
|
-
message: NO_MATCH_MESSAGE,
|
|
452
|
-
};
|
|
453
|
-
}
|
|
386
|
+
const candidates: RankedCandidate[] = rankedCandidates
|
|
387
|
+
.slice(0, effectiveTopK)
|
|
388
|
+
.map((c, index) => ({
|
|
389
|
+
rank: index + 1,
|
|
390
|
+
skill_id: c.skill_id,
|
|
391
|
+
description: c.description,
|
|
392
|
+
score: c.score,
|
|
393
|
+
}));
|
|
394
|
+
|
|
395
|
+
const result: ResolveResult = {
|
|
396
|
+
retrieval,
|
|
397
|
+
...(retrievalResult.degraded_from
|
|
398
|
+
? {
|
|
399
|
+
degraded_from: retrievalResult.degraded_from,
|
|
400
|
+
degradation_reason: retrievalResult.degradation_reason,
|
|
401
|
+
}
|
|
402
|
+
: {}),
|
|
403
|
+
candidates,
|
|
404
|
+
};
|
|
454
405
|
|
|
455
406
|
insertAudit(
|
|
456
407
|
db,
|
|
@@ -458,12 +409,10 @@ export async function resolveSkill(input: ResolveSkillInput): Promise<ResolveRes
|
|
|
458
409
|
id: 0, // assigned by SQLite
|
|
459
410
|
ts: new Date().toISOString(),
|
|
460
411
|
query: input.query,
|
|
461
|
-
outcome: result.outcome,
|
|
462
412
|
retrieval,
|
|
463
413
|
degraded_from: retrievalResult.degraded_from ?? null,
|
|
464
414
|
degradation_reason: retrievalResult.degradation_reason ?? null,
|
|
465
|
-
candidates:
|
|
466
|
-
selected_skill_id: result.outcome === "matched" ? result.skill_id : null,
|
|
415
|
+
candidates: candidates.map((c) => ({ skill_id: c.skill_id, score: c.score })),
|
|
467
416
|
latency_ms: Math.round(performance.now() - t0),
|
|
468
417
|
}),
|
|
469
418
|
);
|
|
@@ -471,11 +420,18 @@ export async function resolveSkill(input: ResolveSkillInput): Promise<ResolveRes
|
|
|
471
420
|
return result;
|
|
472
421
|
}
|
|
473
422
|
|
|
423
|
+
export interface RawCandidate {
|
|
424
|
+
skill_id: string;
|
|
425
|
+
title: string;
|
|
426
|
+
description: string;
|
|
427
|
+
score: number | null;
|
|
428
|
+
}
|
|
429
|
+
|
|
474
430
|
export interface RetrievalResult {
|
|
475
431
|
retrieval: Exclude<RetrievalCapability, "exact">;
|
|
476
432
|
degraded_from?: "reranked" | "hybrid";
|
|
477
433
|
degradation_reason?: DegradationReason;
|
|
478
|
-
candidates:
|
|
434
|
+
candidates: RawCandidate[];
|
|
479
435
|
trace: Array<{
|
|
480
436
|
skill_id: string;
|
|
481
437
|
lexical_rank: number | null;
|
|
@@ -484,18 +440,6 @@ export interface RetrievalResult {
|
|
|
484
440
|
}>;
|
|
485
441
|
}
|
|
486
442
|
|
|
487
|
-
export function decideRetrievalResult(config: Config, result: RetrievalResult): Decision {
|
|
488
|
-
const candidateLimit = config.output?.ambiguous_candidate_limit ?? config.thresholds?.candidate_limit ?? 5;
|
|
489
|
-
const thresholds = result.retrieval === "reranked" && config.inference.mode === "remote"
|
|
490
|
-
? { candidate_limit: candidateLimit, ...config.inference.thresholds }
|
|
491
|
-
: { ...config.thresholds, candidate_limit: candidateLimit };
|
|
492
|
-
return decideResolveOutcome({
|
|
493
|
-
reranked: result.retrieval === "reranked",
|
|
494
|
-
candidates: result.candidates,
|
|
495
|
-
thresholds,
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
|
|
499
443
|
export function classifyInferenceError(
|
|
500
444
|
stage: "embedding" | "reranker",
|
|
501
445
|
error: unknown,
|
|
@@ -530,64 +474,26 @@ export function classifyInferenceError(
|
|
|
530
474
|
return stage === "embedding" ? "embedding_unavailable" : "reranker_unavailable";
|
|
531
475
|
}
|
|
532
476
|
|
|
533
|
-
export interface StageTiming {
|
|
534
|
-
embedding_ms: number;
|
|
535
|
-
lexical_ms: number;
|
|
536
|
-
vector_ms: number;
|
|
537
|
-
reranker_ms: number;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
export interface RetrieveSnapshotOptions {
|
|
541
|
-
onTiming?: (timing: StageTiming) => void;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
477
|
/**
|
|
545
|
-
* Retrieve the full fused candidate set
|
|
546
|
-
*
|
|
478
|
+
* Retrieve the full fused candidate set, optionally rerank it, and synchronize
|
|
479
|
+
* the vault before reading the index.
|
|
547
480
|
*/
|
|
548
481
|
export async function retrieveAndRerank(
|
|
549
482
|
input: ResolveSkillInput,
|
|
550
483
|
): Promise<RetrievalResult> {
|
|
551
484
|
await syncVaultIfNeeded();
|
|
552
|
-
return retrieveAndRerankSnapshot(input);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Retrieve candidates against an already-synchronized index snapshot without
|
|
557
|
-
* triggering syncVaultIfNeeded(). Calibration uses this to avoid observing the
|
|
558
|
-
* policy it is trying to replace while keeping one corpus for the whole run.
|
|
559
|
-
*/
|
|
560
|
-
export async function retrieveAndRerankSnapshot(
|
|
561
|
-
input: ResolveSkillInput,
|
|
562
|
-
options?: RetrieveSnapshotOptions,
|
|
563
|
-
): Promise<RetrievalResult> {
|
|
564
485
|
const { config, db } = await getEnv();
|
|
565
486
|
const clients = getClients();
|
|
566
|
-
const measureTiming = options?.onTiming !== undefined;
|
|
567
487
|
|
|
568
|
-
let embedding_ms = 0;
|
|
569
|
-
let lexical_ms = 0;
|
|
570
|
-
let vector_ms = 0;
|
|
571
|
-
let reranker_ms = 0;
|
|
572
|
-
|
|
573
|
-
const tEmbed0 = measureTiming ? performance.now() : 0;
|
|
574
488
|
const embedPromise =
|
|
575
489
|
!input.forceLexical && clients.embed
|
|
576
490
|
? clients.embed([input.query]).then(
|
|
577
|
-
(res) =>
|
|
578
|
-
|
|
579
|
-
return res;
|
|
580
|
-
},
|
|
581
|
-
(err) => {
|
|
582
|
-
if (measureTiming) embedding_ms = Math.max(0, performance.now() - tEmbed0);
|
|
583
|
-
return { error: err };
|
|
584
|
-
},
|
|
491
|
+
(res) => res,
|
|
492
|
+
(err) => ({ error: err }),
|
|
585
493
|
)
|
|
586
494
|
: null;
|
|
587
495
|
|
|
588
|
-
const tLex0 = measureTiming ? performance.now() : 0;
|
|
589
496
|
const lexical = ftsSearch(db, input.query, config.recall.k_lexical);
|
|
590
|
-
if (measureTiming) lexical_ms = Math.max(0, performance.now() - tLex0);
|
|
591
497
|
const lexicalRanks = new Map(lexical.map((row, index) => [row.skill_id, index + 1]));
|
|
592
498
|
|
|
593
499
|
let retrieval: RetrievalResult["retrieval"] = "lexical";
|
|
@@ -611,17 +517,14 @@ export async function retrieveAndRerankSnapshot(
|
|
|
611
517
|
}),
|
|
612
518
|
);
|
|
613
519
|
} else {
|
|
614
|
-
const tVec0 = measureTiming ? performance.now() : 0;
|
|
615
520
|
try {
|
|
616
521
|
const queryVec = (embedRes as Float32Array[])[0];
|
|
617
522
|
if (!queryVec) throw new Error("Embedding client returned no query vector.");
|
|
618
523
|
const nearest = vectorTopK(db, queryVec, config.recall.k_vector);
|
|
619
|
-
if (measureTiming) vector_ms = Math.max(0, performance.now() - tVec0);
|
|
620
524
|
rows = reciprocalRankFusion(lexical, nearest);
|
|
621
525
|
fusedRows = rows;
|
|
622
526
|
retrieval = "hybrid";
|
|
623
527
|
} catch (embedError) {
|
|
624
|
-
if (measureTiming) vector_ms = Math.max(0, performance.now() - tVec0);
|
|
625
528
|
retrieval = "lexical";
|
|
626
529
|
degraded_from = clients.rerank ? "reranked" : "hybrid";
|
|
627
530
|
degradation_reason = classifyInferenceError("embedding", embedError);
|
|
@@ -641,17 +544,14 @@ export async function retrieveAndRerankSnapshot(
|
|
|
641
544
|
if (clients.rerank && retrieval === "hybrid" && rows.length > 0) {
|
|
642
545
|
const kRerank = config.recall.k_rerank ?? 10;
|
|
643
546
|
const rerankCandidates = rows.slice(0, kRerank);
|
|
644
|
-
const tRerank0 = measureTiming ? performance.now() : 0;
|
|
645
547
|
try {
|
|
646
548
|
scores = await clients.rerank(
|
|
647
549
|
input.query,
|
|
648
550
|
rerankCandidates.map((r) => ({ skill_id: r.skill_id, text: rerankText(r) })),
|
|
649
551
|
);
|
|
650
|
-
if (measureTiming) reranker_ms = Math.max(0, performance.now() - tRerank0);
|
|
651
552
|
retrieval = "reranked";
|
|
652
553
|
rows = rerankCandidates;
|
|
653
554
|
} catch (rerankError) {
|
|
654
|
-
if (measureTiming) reranker_ms = Math.max(0, performance.now() - tRerank0);
|
|
655
555
|
scores = null;
|
|
656
556
|
degraded_from = "reranked";
|
|
657
557
|
degradation_reason = classifyInferenceError("reranker", rerankError);
|
|
@@ -679,13 +579,6 @@ export async function retrieveAndRerankSnapshot(
|
|
|
679
579
|
: new Map<string, number>();
|
|
680
580
|
const traceRows = fusedRows ?? rows;
|
|
681
581
|
|
|
682
|
-
options?.onTiming?.({
|
|
683
|
-
embedding_ms,
|
|
684
|
-
lexical_ms,
|
|
685
|
-
vector_ms,
|
|
686
|
-
reranker_ms,
|
|
687
|
-
});
|
|
688
|
-
|
|
689
582
|
return {
|
|
690
583
|
retrieval,
|
|
691
584
|
...(degraded_from ? { degraded_from, degradation_reason } : {}),
|
package/src/server.ts
CHANGED
|
@@ -74,30 +74,29 @@ export function createMcpServer(): McpServer {
|
|
|
74
74
|
"resolve_skill",
|
|
75
75
|
{
|
|
76
76
|
description:
|
|
77
|
-
"Route a natural-language task description to
|
|
78
|
-
"Returns
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
"Route a natural-language task description to candidate skills in the vault. " +
|
|
78
|
+
"Returns a ranked shortlist of candidates. Use fetch_skill to retrieve a candidate's complete instructions.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
query: z.string().min(1).describe("Natural-language task or prompt description to route."),
|
|
81
|
+
top_k: z
|
|
82
|
+
.number()
|
|
83
|
+
.int()
|
|
84
|
+
.min(1)
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Optional maximum number of ranked candidates to return (subject to server max_top_k limit)."),
|
|
87
|
+
},
|
|
81
88
|
},
|
|
82
|
-
async ({ query }) => {
|
|
89
|
+
async ({ query, top_k }) => {
|
|
83
90
|
const startTime = performance.now();
|
|
84
91
|
try {
|
|
85
|
-
const result = await resolveSkill({ query });
|
|
92
|
+
const result = await resolveSkill({ query, top_k });
|
|
86
93
|
const duration = (performance.now() - startTime) / 1000;
|
|
87
94
|
metricsRegistry.recordResolveLatencySeconds(duration);
|
|
88
|
-
metricsRegistry.recordResolveOutcome(result.outcome);
|
|
89
95
|
if (result.degradation_reason) {
|
|
90
96
|
const stage = result.degradation_reason.startsWith("embedding_") ? "embedding" : "reranker";
|
|
91
97
|
metricsRegistry.recordDegradation(stage, result.degradation_reason);
|
|
92
98
|
}
|
|
93
99
|
|
|
94
|
-
if (result.outcome === "matched") {
|
|
95
|
-
const { body, ...meta } = result;
|
|
96
|
-
return {
|
|
97
|
-
content: [{ type: "text" as const, text: body }],
|
|
98
|
-
structuredContent: { ...meta },
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
100
|
return {
|
|
102
101
|
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
|
103
102
|
structuredContent: { ...result },
|
|
@@ -394,7 +393,6 @@ export async function startServer(opts?: {
|
|
|
394
393
|
JSON.stringify({
|
|
395
394
|
config_read: true,
|
|
396
395
|
config_write: !isExternallyManaged,
|
|
397
|
-
calibration: false,
|
|
398
396
|
persistence: isExternallyManaged
|
|
399
397
|
? "externally_managed"
|
|
400
398
|
: "writable",
|
|
@@ -481,18 +479,6 @@ export async function startServer(opts?: {
|
|
|
481
479
|
});
|
|
482
480
|
}
|
|
483
481
|
|
|
484
|
-
if (url.pathname.startsWith("/admin/v1/calibrations")) {
|
|
485
|
-
return new Response(
|
|
486
|
-
JSON.stringify({
|
|
487
|
-
error: "not_implemented",
|
|
488
|
-
message:
|
|
489
|
-
"Remote calibration is not implemented in this release. " +
|
|
490
|
-
"Run `skillmux calibrate` against a local target.",
|
|
491
|
-
}),
|
|
492
|
-
{ status: 501, headers },
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
482
|
return new Response("Not Found", { status: 404, headers });
|
|
497
483
|
}
|
|
498
484
|
|