@klhapp/skillmux 1.3.0 → 1.3.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 +19 -0
- package/README.md +16 -9
- package/package.json +1 -1
- package/src/adapters.ts +3 -4
- package/src/calibrate.ts +13 -0
- package/src/cli.ts +7 -1
- package/src/doctor.ts +79 -1
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.2](https://github.com/klhq/skillmux/compare/v1.3.1...v1.3.2) (2026-07-31)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* **doctor:** flag uncalibrated or stale inference thresholds ([#99](https://github.com/klhq/skillmux/issues/99)) ([c8f684b](https://github.com/klhq/skillmux/commit/c8f684b6aedf6788def4cdc0fe49fff233569844))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Chores
|
|
17
|
+
|
|
18
|
+
* pin next release to 1.3.2 instead of 1.4.0 (take 2) ([#102](https://github.com/klhq/skillmux/issues/102)) ([bbbb7dd](https://github.com/klhq/skillmux/commit/bbbb7dd838d691735e797f39de7d4277b95f5187))
|
|
19
|
+
|
|
20
|
+
## [1.3.1](https://github.com/klhq/skillmux/compare/v1.3.0...v1.3.1) (2026-07-31)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
* **cli:** parse calibrate run flags correctly ([#97](https://github.com/klhq/skillmux/issues/97)) ([95d6b19](https://github.com/klhq/skillmux/commit/95d6b19272c34f7c8269cb354b7751f0fdd9ad75))
|
|
26
|
+
|
|
8
27
|
## [1.3.0](https://github.com/klhq/skillmux/compare/v1.2.0...v1.3.0) (2026-07-29)
|
|
9
28
|
|
|
10
29
|
|
package/README.md
CHANGED
|
@@ -10,26 +10,33 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
Skillmux manages [`SKILL.md`](https://agentskills.io) collections across AI
|
|
13
|
-
coding clients. Keep one
|
|
14
|
-
directories, and retrieve the rest through MCP.
|
|
13
|
+
coding clients. Keep one vault source of truth, pin a small set into native
|
|
14
|
+
skill directories, and retrieve the rest through MCP. On one machine, the
|
|
15
|
+
source and checkout can be the same directory. In a shared deployment, each
|
|
16
|
+
machine that manages native skills and the server use their own checkout; Git
|
|
17
|
+
or your deployment process keeps those checkouts current.
|
|
15
18
|
|
|
16
19
|
The same Skillmux CLI manages native skills and can serve local stdio MCP.
|
|
20
|
+
Most individual users need only the CLI. Add Docker when you need a shared or
|
|
21
|
+
always-on HTTP service.
|
|
22
|
+
|
|
17
23
|
Choose a setup by the job:
|
|
18
24
|
|
|
19
25
|
1. Need native skills or local MCP for one client? Install the **Skillmux CLI**.
|
|
20
26
|
2. On Linux without Bun? Install the standalone Linux executable instead; it is
|
|
21
27
|
the same CLI.
|
|
22
|
-
3. Need one shared HTTP MCP service? Deploy the **full Docker image
|
|
28
|
+
3. Need one shared HTTP MCP service? Deploy the **full Docker image**, the
|
|
23
29
|
self-contained default with GTE-small.
|
|
24
30
|
4. Already have remote embeddings, or intentionally want lexical-only
|
|
25
31
|
retrieval? Use the **slim** image; see [Deployment](docs/deployment.md).
|
|
26
32
|
5. Need native pins and shared retrieval? Run the CLI on the machines that own
|
|
27
|
-
client directories and one shared server for routed retrieval.
|
|
33
|
+
client directories and one shared server for routed retrieval. MCP-only
|
|
34
|
+
clients connect over HTTP and do not need the CLI.
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
Manage the server's vault checkout outside the container. Use the CLI for
|
|
37
|
+
Skillmux operations and Git or your deployment process for replication.
|
|
31
38
|
|
|
32
|
-
## One vault, three ways to use it
|
|
39
|
+
## One vault source of truth, three ways to use it
|
|
33
40
|
|
|
34
41
|
<p align="center">
|
|
35
42
|
<img src="docs/assets/architecture.svg" alt="Three ways to use Skillmux: manage native skills, add local MCP retrieval, or run a shared MCP service" width="100%">
|
|
@@ -51,7 +58,7 @@ skillmux --help
|
|
|
51
58
|
|
|
52
59
|
Native target sync needs permission to create directory symlinks on Windows.
|
|
53
60
|
|
|
54
|
-
On Linux, you can install a
|
|
61
|
+
On Linux, you can install a standalone AMD64 or ARM64 executable instead:
|
|
55
62
|
|
|
56
63
|
```sh
|
|
57
64
|
gh release download --repo klhq/skillmux --pattern 'skillmux-linux-*'
|
|
@@ -211,7 +218,7 @@ Skillmux preserves existing instruction files and unmanaged target content. Run
|
|
|
211
218
|
|
|
212
219
|
## Guarantees
|
|
213
220
|
|
|
214
|
-
- **Controlled sources:** pins come from the
|
|
221
|
+
- **Controlled sources:** pins come from the configured vault checkout, while routed delivery follows the configured overlay order.
|
|
215
222
|
- **Scoped writes:** management commands write only to documented config, vault, state, and adopted target paths.
|
|
216
223
|
- **Managed ownership:** sync removes only entries recorded in the target's `.skillmux` marker.
|
|
217
224
|
- **Current bytes:** MCP delivery hashes the file on disk and never serves a stale indexed body.
|
package/package.json
CHANGED
package/src/adapters.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { applyCalibrationRun, getCalibrationRun, insertCalibrationRun, listCalibrationRuns, loadDecisionCasesFromFile, openCalibrateDb, runCalibration, summarizeDatasetProvenance, type CalibrationResult } from "./calibrate";
|
|
3
|
+
import { applyCalibrationRun, computeCorpusFingerprint, getCalibrationRun, insertCalibrationRun, listCalibrationRuns, loadDecisionCasesFromFile, openCalibrateDb, runCalibration, summarizeDatasetProvenance, type CalibrationResult } from "./calibrate";
|
|
4
4
|
import { createClients } from "./clients";
|
|
5
5
|
import { DEFAULT_CONFIG_PATH, embeddingFingerprint, expandHome, loadConfig, rerankerFingerprint } from "./config";
|
|
6
6
|
import { openIndex } from "./db";
|
|
@@ -141,10 +141,12 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
141
141
|
const datasetFile = opts?.datasetPath ?? join(expandHome(config.state_dir), "queries.json");
|
|
142
142
|
const indexDb = openIndex(expandHome(config.state_dir));
|
|
143
143
|
let indexedSkills: Array<{ skill_id: string; content_sha256: string }>;
|
|
144
|
+
let corpusFingerprint: string;
|
|
144
145
|
try {
|
|
145
146
|
indexedSkills = indexDb
|
|
146
147
|
.query("SELECT skill_id, content_sha256 FROM skills ORDER BY skill_id")
|
|
147
148
|
.all() as Array<{ skill_id: string; content_sha256: string }>;
|
|
149
|
+
corpusFingerprint = computeCorpusFingerprint(indexDb);
|
|
148
150
|
} finally {
|
|
149
151
|
indexDb.close();
|
|
150
152
|
}
|
|
@@ -180,9 +182,6 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
180
182
|
throw new Error("A configured remote reranker is required to record calibration.");
|
|
181
183
|
}
|
|
182
184
|
const datasetText = await Bun.file(datasetFile).text();
|
|
183
|
-
const corpusFingerprint =
|
|
184
|
-
"vault:" +
|
|
185
|
-
createHash("sha256").update(JSON.stringify(indexedSkills)).digest("hex");
|
|
186
185
|
const runId = `run_${crypto.randomUUID()}`;
|
|
187
186
|
const db = openCalibrateDb(expandHome(config.state_dir));
|
|
188
187
|
try {
|
package/src/calibrate.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { Database } from "bun:sqlite";
|
|
@@ -900,6 +901,18 @@ export interface CalibrationRunSummary {
|
|
|
900
901
|
failed_reason?: CalibrationFailureReason;
|
|
901
902
|
}
|
|
902
903
|
|
|
904
|
+
/**
|
|
905
|
+
* Fingerprint the indexed vault content so calibration runs can detect
|
|
906
|
+
* corpus drift. Must match how `insertCalibrationRun` computes it at
|
|
907
|
+
* `calibrate run` time.
|
|
908
|
+
*/
|
|
909
|
+
export function computeCorpusFingerprint(indexDb: Database): string {
|
|
910
|
+
const indexedSkills = indexDb
|
|
911
|
+
.query("SELECT skill_id, content_sha256 FROM skills ORDER BY skill_id")
|
|
912
|
+
.all() as Array<{ skill_id: string; content_sha256: string }>;
|
|
913
|
+
return "vault:" + createHash("sha256").update(JSON.stringify(indexedSkills)).digest("hex");
|
|
914
|
+
}
|
|
915
|
+
|
|
903
916
|
/**
|
|
904
917
|
* Open (or create) the calibration evidence database in `stateDir`.
|
|
905
918
|
* Uses a separate `calibrate.sqlite3` file — never the index.sqlite3 used
|
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
2
3
|
import { Database } from "bun:sqlite";
|
|
3
4
|
import { existsSync, lstatSync, mkdirSync, rmSync } from "node:fs";
|
|
4
5
|
import { hostname } from "node:os";
|
|
@@ -170,6 +171,11 @@ async function main() {
|
|
|
170
171
|
const commandArgs = rawArgv.slice(2);
|
|
171
172
|
|
|
172
173
|
const command = rawArgv[0];
|
|
174
|
+
if (command === "--version" || command === "-V") {
|
|
175
|
+
console.log(packageJson.version);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
173
179
|
if (!command || command === "--help" || command === "-h") {
|
|
174
180
|
printHelp();
|
|
175
181
|
return;
|
|
@@ -446,7 +452,7 @@ async function handleCalibrateCommand(
|
|
|
446
452
|
if (!Number.isFinite(value)) throw new Error(`${flag} must be a number`);
|
|
447
453
|
return value;
|
|
448
454
|
};
|
|
449
|
-
for (let i =
|
|
455
|
+
for (let i = 1; i < args.length; i++) {
|
|
450
456
|
const option = args[i];
|
|
451
457
|
if (option === "--dataset") {
|
|
452
458
|
datasetPath = args[++i];
|
package/src/doctor.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { computeCorpusFingerprint, getCalibrationRun, openCalibrateDb } from "./calibrate";
|
|
2
3
|
import { createClients, RemoteInferenceError } from "./clients";
|
|
3
|
-
import { embeddingDimension, expandHome } from "./config";
|
|
4
|
+
import { embeddingDimension, embeddingFingerprint, expandHome, rerankerFingerprint } from "./config";
|
|
5
|
+
import { openIndex } from "./db";
|
|
4
6
|
import { parseManifest, resolveManifestPath, validateManifest } from "./manifest";
|
|
5
7
|
import { readSkillmuxMarker } from "./sync";
|
|
6
8
|
import type { Config } from "./types";
|
|
@@ -19,6 +21,78 @@ export interface DoctorReport {
|
|
|
19
21
|
checks: DoctorCheck[];
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Warn when live `inference.thresholds` didn't come from an applied
|
|
26
|
+
* `skillmux calibrate` run — e.g. hand-copied from an example config.
|
|
27
|
+
* Reranker score scales are not portable across models/adapters/corpora,
|
|
28
|
+
* so uncalibrated thresholds routinely make automatic matching unreachable
|
|
29
|
+
* without any visible error.
|
|
30
|
+
*/
|
|
31
|
+
function checkCalibration(config: Config): DoctorCheck {
|
|
32
|
+
const inference = config.inference;
|
|
33
|
+
if (inference.mode !== "remote") throw new Error("checkCalibration requires remote inference mode");
|
|
34
|
+
const runId = inference.calibration?.run_id;
|
|
35
|
+
if (!runId) {
|
|
36
|
+
return {
|
|
37
|
+
name: "calibration",
|
|
38
|
+
ok: false,
|
|
39
|
+
detail: "inference.thresholds are set but were never produced by `skillmux calibrate apply` — " +
|
|
40
|
+
"likely copied from an example config. Reranker scores are not portable across models, " +
|
|
41
|
+
"adapters, or corpora, so automatic matching may never trigger (or may trigger incorrectly). " +
|
|
42
|
+
"Run `skillmux calibrate`.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const calibrateDb = openCalibrateDb(expandHome(config.state_dir));
|
|
47
|
+
let run;
|
|
48
|
+
try {
|
|
49
|
+
run = getCalibrationRun(calibrateDb, runId);
|
|
50
|
+
} finally {
|
|
51
|
+
calibrateDb.close();
|
|
52
|
+
}
|
|
53
|
+
if (!run) {
|
|
54
|
+
return {
|
|
55
|
+
name: "calibration",
|
|
56
|
+
ok: false,
|
|
57
|
+
detail: `inference.calibration.run_id "${runId}" was not found in the local calibration ` +
|
|
58
|
+
"evidence store (state_dir may differ from where it was calibrated). Recalibrate.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (run.status !== "completed") {
|
|
62
|
+
return {
|
|
63
|
+
name: "calibration",
|
|
64
|
+
ok: false,
|
|
65
|
+
detail: `inference.calibration.run_id "${runId}" has status "${run.status}" and should never ` +
|
|
66
|
+
"have been applied. Recalibrate.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const indexDb = openIndex(expandHome(config.state_dir));
|
|
71
|
+
let currentCorpusFingerprint: string;
|
|
72
|
+
try {
|
|
73
|
+
currentCorpusFingerprint = computeCorpusFingerprint(indexDb);
|
|
74
|
+
} finally {
|
|
75
|
+
indexDb.close();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stale = [
|
|
79
|
+
rerankerFingerprint(config) !== run.reranker_fingerprint ? "reranker" : null,
|
|
80
|
+
embeddingFingerprint(config) !== run.embedding_fingerprint ? "embedding" : null,
|
|
81
|
+
currentCorpusFingerprint !== run.corpus_fingerprint ? "vault contents" : null,
|
|
82
|
+
].filter((part): part is string => part !== null);
|
|
83
|
+
|
|
84
|
+
if (stale.length > 0) {
|
|
85
|
+
return {
|
|
86
|
+
name: "calibration",
|
|
87
|
+
ok: false,
|
|
88
|
+
detail: `applied calibration run "${runId}" is stale — ${stale.join(", ")} changed since it was ` +
|
|
89
|
+
"calibrated. Recalibrate.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { name: "calibration", ok: true, detail: `thresholds from applied calibration run "${runId}"` };
|
|
94
|
+
}
|
|
95
|
+
|
|
22
96
|
export async function diagnose(config: Config): Promise<DoctorReport> {
|
|
23
97
|
const checks: DoctorCheck[] = [];
|
|
24
98
|
checks.push({ name: "vault", ok: existsSync(expandHome(config.vault_path)), detail: expandHome(config.vault_path) });
|
|
@@ -129,6 +203,10 @@ export async function diagnose(config: Config): Promise<DoctorReport> {
|
|
|
129
203
|
}
|
|
130
204
|
}
|
|
131
205
|
|
|
206
|
+
if (config.inference.mode === "remote" && config.inference.reranker && config.inference.thresholds) {
|
|
207
|
+
checks.push(checkCalibration(config));
|
|
208
|
+
}
|
|
209
|
+
|
|
132
210
|
const inferenceReady = checks.some((check) => check.name === "embedding" && check.ok);
|
|
133
211
|
const coreReady = checks.some((check) => check.name === "vault" && check.ok)
|
|
134
212
|
&& checks.some((check) => check.name === "state" && check.ok);
|
package/src/types.ts
CHANGED
|
@@ -73,6 +73,7 @@ export interface RemoteInferenceConfig {
|
|
|
73
73
|
embedding: RemoteEmbeddingConfig;
|
|
74
74
|
reranker?: RemoteRerankerConfig;
|
|
75
75
|
thresholds?: Required<Omit<Thresholds, "candidate_limit">>;
|
|
76
|
+
calibration?: { run_id: string };
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
export type InferenceConfig = LocalInferenceConfig | RemoteInferenceConfig;
|