@klhapp/skillmux 1.1.0 → 1.3.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.
@@ -0,0 +1,209 @@
1
+ # Managing skills
2
+
3
+ Skillmux keeps skill content in a canonical vault and materializes selected
4
+ skills into client directories. This guide covers the commands that change or
5
+ inspect that state.
6
+
7
+ Run these commands on the machine that owns the vault and client directories.
8
+ For a retrieval-only Docker service, manage the mounted vault on the host and
9
+ keep the container mount read-only.
10
+
11
+ ## Install from Git
12
+
13
+ `skillmux install` accepts a GitHub shorthand or a full Git URL:
14
+
15
+ ```sh
16
+ skillmux install owner/repo
17
+ skillmux install owner/repo/path/to/skill
18
+ skillmux install https://git.example.com/team/skill.git
19
+ ```
20
+
21
+ The repository root must contain `SKILL.md`. If a repository contains several
22
+ skill directories, add the path for the one you want.
23
+
24
+ Skillmux clones into a temporary directory, validates the selected skill,
25
+ scans its text files, and copies it to `vault_path`. Existing skill IDs require
26
+ `--force`.
27
+
28
+ Preview the destination without copying:
29
+
30
+ ```sh
31
+ skillmux install owner/repo --dry-run
32
+ ```
33
+
34
+ Set a scan gate when you want findings to block installation:
35
+
36
+ ```sh
37
+ skillmux install owner/repo --fail-on high
38
+ ```
39
+
40
+ The scanner detects suspicious prompt-injection patterns, secrets, and risky
41
+ instructions. Findings remain advisory unless you pass `--fail-on`.
42
+
43
+ ## Scan a vault or candidate
44
+
45
+ ```sh
46
+ skillmux scan
47
+ skillmux scan ~/skills/candidate
48
+ skillmux scan --format json
49
+ skillmux scan --fail-on medium
50
+ ```
51
+
52
+ With no path, `scan` checks the configured vault. `--json` wraps the result in
53
+ the standard CLI automation envelope, while `--format json` selects the
54
+ scanner's raw JSON rendering.
55
+
56
+ ## Plan client delivery
57
+
58
+ Use product names for common clients:
59
+
60
+ ```sh
61
+ skillmux init --client claude-code --client codex --dry-run
62
+ ```
63
+
64
+ Use a direct target when you need a known path:
65
+
66
+ ```sh
67
+ skillmux init --target agent-skills --yes
68
+ skillmux init --target custom --dir /srv/my-agent/skills --yes
69
+ ```
70
+
71
+ Skillmux refuses to adopt a target that points to the whole vault because sync
72
+ would reduce its visible skills. Review that migration first:
73
+
74
+ ```sh
75
+ skillmux init --client claude-code --migrate-full-vault \
76
+ --core csv-formatter --dry-run
77
+ skillmux init --client claude-code --migrate-full-vault \
78
+ --core csv-formatter --yes
79
+ ```
80
+
81
+ ## Manage core pins
82
+
83
+ Core skills go to each configured target:
84
+
85
+ ```sh
86
+ skillmux core pin csv-formatter --yes
87
+ skillmux core pin code-context systematic-debugging --yes
88
+ skillmux core unpin csv-formatter --yes
89
+ ```
90
+
91
+ One command can change several skill IDs. Skillmux validates the complete
92
+ change before writing, so a conflict prevents the whole operation. Core stays
93
+ capped at 25 skills.
94
+
95
+ Run `skillmux sync` after a direct pin or unpin command.
96
+
97
+ ## Manage project groups
98
+
99
+ Create a group with the guided command:
100
+
101
+ ```sh
102
+ skillmux project init
103
+ ```
104
+
105
+ Maintain it with explicit commands:
106
+
107
+ ```sh
108
+ skillmux project list
109
+ skillmux project show my-project
110
+ skillmux project add-path my-project ~/code/my-project --yes
111
+ skillmux project pin my-project code-context --yes
112
+ skillmux project attach my-project --client claude-code --client codex --yes
113
+ skillmux project unpin my-project code-context --yes
114
+ skillmux project detach my-project --target codex --yes
115
+ skillmux project remove-path my-project ~/code/my-project --yes
116
+ ```
117
+
118
+ Create the group with `project init` or `project add-path` before pinning.
119
+ Project setup syncs by default. Direct maintenance commands update the
120
+ manifest but leave materialization to the next `skillmux sync`.
121
+
122
+ ## Synchronize targets
123
+
124
+ ```sh
125
+ skillmux sync --dry-run
126
+ skillmux sync
127
+ ```
128
+
129
+ Sync compares the manifest with entries recorded in each target's `.skillmux`
130
+ marker. It creates missing symlinks and removes stale managed links.
131
+
132
+ Install a vault Git hook when merges can change `skillmux.toml`:
133
+
134
+ ```sh
135
+ skillmux sync --install-hook
136
+ ```
137
+
138
+ The hook lives in the canonical vault and runs `skillmux sync` after a merge.
139
+
140
+ ## Inspect active state
141
+
142
+ Find which vault root serves a skill:
143
+
144
+ ```sh
145
+ skillmux skill which code-context
146
+ ```
147
+
148
+ If a local overlay shadows the canonical copy, the output lists both paths.
149
+
150
+ Inspect configuration and readiness:
151
+
152
+ ```sh
153
+ skillmux config show
154
+ skillmux config diff
155
+ skillmux config status
156
+ skillmux doctor
157
+ ```
158
+
159
+ ## Use routing data to tune tiers
160
+
161
+ `resolve_skill` writes an audit row for each request. Summarize recent usage:
162
+
163
+ ```sh
164
+ skillmux report --since 7d
165
+ skillmux report --server http://host:3000 --since 7d
166
+ ```
167
+
168
+ Repeatedly matched skills may belong in core or a project group. Repeated
169
+ `no_match` queries point to missing skills or weak skill descriptions.
170
+
171
+ `--since` accepts windows such as `1h`, `7d`, and `1m`, plus absolute dates and
172
+ timestamps.
173
+
174
+ ## Target ownership and recovery
175
+
176
+ `skillmux target remove <name> --yes` removes the manifest record and preserves
177
+ the target directory, marker, and files. Cleanup stays under your control.
178
+
179
+ Restore a managed target to one symlink that exposes the full vault:
180
+
181
+ ```sh
182
+ skillmux sync --restore-monolith
183
+ ```
184
+
185
+ This operation removes the target marker and per-skill links. It refuses to
186
+ run when unmanaged content makes the replacement unsafe. Re-adopt the target
187
+ with `skillmux init` before running managed sync again.
188
+
189
+ Do not delete `.skillmux` markers by hand. The marker gives sync the ownership
190
+ record it needs to preserve unrelated content.
191
+
192
+ ## Local overlays
193
+
194
+ Configure machine-specific override roots:
195
+
196
+ ```toml
197
+ vault_path = "~/skills"
198
+ local_vault_paths = ["~/skills-local"]
199
+ ```
200
+
201
+ Then record the relationship:
202
+
203
+ ```sh
204
+ skillmux local-vault init ~/skills-local --yes
205
+ skillmux skill which my-skill
206
+ ```
207
+
208
+ Read [Configuration](configuration.md#local-vault-overlays) for precedence,
209
+ pinning restrictions, and watcher behavior.
@@ -0,0 +1,199 @@
1
+ # Troubleshooting
2
+
3
+ Start with:
4
+
5
+ ```sh
6
+ skillmux doctor
7
+ skillmux config show
8
+ skillmux config validate
9
+ ```
10
+
11
+ Add `--json` when you need machine-readable diagnostics.
12
+
13
+ ## Vault failures
14
+
15
+ ### Vault path does not exist
16
+
17
+ Check the effective path and its source:
18
+
19
+ ```sh
20
+ skillmux config show
21
+ ```
22
+
23
+ Create the directory or initialize config against a populated vault:
24
+
25
+ ```sh
26
+ skillmux config init --vault ~/skills --yes
27
+ ```
28
+
29
+ `config init` requires at least one valid `SKILL.md`.
30
+
31
+ ### A skill does not appear
32
+
33
+ Each skill must sit one directory below the vault:
34
+
35
+ ```text
36
+ ~/skills/<skill-id>/SKILL.md
37
+ ```
38
+
39
+ Run:
40
+
41
+ ```sh
42
+ skillmux scan ~/skills/<skill-id>
43
+ skillmux index
44
+ skillmux skill which <skill-id>
45
+ ```
46
+
47
+ The scanner reports invalid frontmatter and unreadable content. `skill which`
48
+ also reveals local-overlay shadowing.
49
+
50
+ ## Sync failures
51
+
52
+ ### Target has no ownership marker
53
+
54
+ Skillmux will not change an existing unmarked directory. Adopt it first:
55
+
56
+ ```sh
57
+ skillmux init --client claude-code --dry-run
58
+ skillmux init --client claude-code --yes
59
+ ```
60
+
61
+ ### Target points to the full vault
62
+
63
+ Review the smaller pinned set before converting:
64
+
65
+ ```sh
66
+ skillmux init --client claude-code \
67
+ --migrate-full-vault \
68
+ --core code-context \
69
+ --dry-run
70
+ ```
71
+
72
+ Apply the same command with `--yes` after checking the plan.
73
+
74
+ ### Unmanaged file collides with a pin
75
+
76
+ Skillmux preserves unmanaged target content. Rename or remove the conflicting
77
+ entry yourself, then rerun `skillmux sync`.
78
+
79
+ ### Target belongs to another host
80
+
81
+ New targets include the current hostname. `sync` skips a target when its
82
+ manifest `host` differs. Run `skillmux target show <name>` and add a separate
83
+ target for the current machine instead of reusing the other machine's path.
84
+
85
+ ### A local-overlay skill cannot be pinned
86
+
87
+ Core and project pins must exist in the canonical `vault_path`. Copy or commit
88
+ the skill there before pinning it. Routed lookup can still serve the overlay.
89
+
90
+ ## Retrieval failures
91
+
92
+ ### `resolve_skill` returns `ambiguous`
93
+
94
+ Ambiguity is the expected result without calibrated reranker thresholds. The
95
+ calling model should select a candidate and call `fetch_skill`.
96
+
97
+ Improve a weak shortlist by:
98
+
99
+ - writing a concrete skill description with task vocabulary;
100
+ - enabling embeddings;
101
+ - increasing recall depth when the relevant skill falls outside the fused
102
+ candidate set.
103
+
104
+ Use a labelled dataset and [Policy calibration](calibration.md) before enabling
105
+ automatic matches.
106
+
107
+ ### Server reports lexical mode
108
+
109
+ Lexical mode means Skillmux can query FTS5 but cannot use embeddings. The next
110
+ step depends on the installation:
111
+
112
+ | Installation | Expected action |
113
+ | --- | --- |
114
+ | Skillmux CLI with local inference | Download the local model and rebuild the index |
115
+ | Full Docker image | Confirm the running tag is the full image and inspect `doctor` output |
116
+ | Slim Docker image | Configure remote embeddings or keep lexical fallback |
117
+
118
+ For a Skillmux CLI installation, run:
119
+
120
+ ```sh
121
+ skillmux doctor
122
+ skillmux models download
123
+ skillmux index
124
+ ```
125
+
126
+ For remote inference, verify the endpoint, model, dimension, API-key
127
+ environment variable, and network path. The slim image does not contain
128
+ GTE-small, so `models download` is not its recovery path.
129
+
130
+ ### Reranker is unavailable
131
+
132
+ Skillmux keeps the hybrid shortlist when a reranker probe or request fails.
133
+ Check `inference.reranker.endpoint`, `adapter`, `model`, and the environment
134
+ variable named by `api_key_env`.
135
+
136
+ Use the complete request URL. Skillmux does not append `/rerank` or infer an
137
+ adapter from the endpoint.
138
+
139
+ ### Delivered content changed after indexing
140
+
141
+ Skillmux checks the file before delivery and refreshes stale metadata. If a
142
+ skill cannot be parsed during a live edit, the index keeps the previous good
143
+ metadata but does not serve stale body bytes. Finish the write with valid
144
+ frontmatter and retry.
145
+
146
+ ## HTTP failures
147
+
148
+ ### Another machine cannot connect
149
+
150
+ The Skillmux CLI binds HTTP to `127.0.0.1`. Set `server.hostname` or
151
+ `HTTP_HOSTNAME` to a reachable interface, then restart the server. Docker
152
+ binds `0.0.0.0` inside the container, but the host still needs a published
153
+ port. Enable authentication before exposing either deployment.
154
+
155
+ ### Browser receives `403`
156
+
157
+ Add the browser origin to `server.allowed_origins`. The value must match the
158
+ request's `Origin` header. Curl and server-to-server clients omit this header
159
+ and do not use the CORS list.
160
+
161
+ ### Client receives `401`
162
+
163
+ Confirm `server.auth_enabled = true`, export the environment variable named by
164
+ `auth_token_env`, and send `Authorization: Bearer <token>`.
165
+
166
+ An enabled server with an empty token environment variable returns a server
167
+ configuration error rather than accepting an empty token.
168
+
169
+ ### Client receives `429`
170
+
171
+ The rate limiter rejected the request. Read `Retry-After` and the
172
+ `X-RateLimit-*` response headers. Increase `requests_per_minute` only after
173
+ checking for a retry loop or shared-token traffic.
174
+
175
+ ## Configuration migration errors
176
+
177
+ Skillmux rejects removed fields with migration guidance:
178
+
179
+ - replace `[targets.<name>].project` with `project_groups = [...]`;
180
+ - rename `[project.<group>].repos` to `paths`;
181
+ - use `skillmux core pin|unpin` instead of removed `manifest pin|unpin`;
182
+ - replace legacy reranker base-URL variables with
183
+ `SKILLMUX_RERANK_ENDPOINT` and `SKILLMUX_RERANK_ADAPTER`.
184
+
185
+ Run `skillmux config validate` after editing TOML.
186
+
187
+ ## Collect diagnostics
188
+
189
+ For a bug report, include:
190
+
191
+ ```sh
192
+ skillmux --help
193
+ skillmux config show
194
+ skillmux doctor --json
195
+ ```
196
+
197
+ Remove tokens, private endpoint credentials, local usernames, and raw audit
198
+ queries before posting output. Open an issue at
199
+ <https://github.com/klhq/skillmux/issues>.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@klhapp/skillmux",
3
- "version": "1.1.0",
4
- "description": "Local read-only MCP server routing natural-language task queries to skills in a SKILL.md vault, with zero-loss delivery",
3
+ "version": "1.3.0",
4
+ "description": "Skill management and retrieval for AI agents: sync native skills across clients and route the long tail over MCP",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "author": "Lance Hsu <lance@klh.app>",
@@ -22,9 +22,7 @@
22
22
  },
23
23
  "files": [
24
24
  "src",
25
- "docs/schema.json",
26
- "docs/configuration.md",
27
- "docs/releasing.md",
25
+ "docs",
28
26
  "README.md",
29
27
  "LICENSE",
30
28
  "CHANGELOG.md",
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, type CalibrationResult } from "./calibrate";
3
+ import { applyCalibrationRun, 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";
@@ -139,7 +139,19 @@ export class LocalAdapter implements TargetAdapter {
139
139
  }): Promise<{ run_id?: string; result?: CalibrationResult }> {
140
140
  const config = await loadConfig(this.configPath);
141
141
  const datasetFile = opts?.datasetPath ?? join(expandHome(config.state_dir), "queries.json");
142
- const cases = loadDecisionCasesFromFile(datasetFile);
142
+ const indexDb = openIndex(expandHome(config.state_dir));
143
+ let indexedSkills: Array<{ skill_id: string; content_sha256: string }>;
144
+ try {
145
+ indexedSkills = indexDb
146
+ .query("SELECT skill_id, content_sha256 FROM skills ORDER BY skill_id")
147
+ .all() as Array<{ skill_id: string; content_sha256: string }>;
148
+ } finally {
149
+ indexDb.close();
150
+ }
151
+ const cases = loadDecisionCasesFromFile(
152
+ datasetFile,
153
+ indexedSkills.map((skill) => skill.skill_id),
154
+ );
143
155
  const clients = createClients(config);
144
156
  configure({ config, clients });
145
157
  const result = await runCalibration({
@@ -168,18 +180,9 @@ export class LocalAdapter implements TargetAdapter {
168
180
  throw new Error("A configured remote reranker is required to record calibration.");
169
181
  }
170
182
  const datasetText = await Bun.file(datasetFile).text();
171
- const indexDb = openIndex(expandHome(config.state_dir));
172
- let corpusFingerprint: string;
173
- try {
174
- const rows = indexDb
175
- .query("SELECT skill_id, content_sha256 FROM skills ORDER BY skill_id")
176
- .all();
177
- corpusFingerprint =
178
- "vault:" +
179
- createHash("sha256").update(JSON.stringify(rows)).digest("hex");
180
- } finally {
181
- indexDb.close();
182
- }
183
+ const corpusFingerprint =
184
+ "vault:" +
185
+ createHash("sha256").update(JSON.stringify(indexedSkills)).digest("hex");
183
186
  const runId = `run_${crypto.randomUUID()}`;
184
187
  const db = openCalibrateDb(expandHome(config.state_dir));
185
188
  try {
@@ -191,6 +194,7 @@ export class LocalAdapter implements TargetAdapter {
191
194
  embedding_fingerprint: embeddingFingerprint(config),
192
195
  corpus_fingerprint: corpusFingerprint,
193
196
  dataset_hash: createHash("sha256").update(datasetText).digest("hex"),
197
+ dataset_provenance: summarizeDatasetProvenance(cases),
194
198
  candidate_limit: config.thresholds.candidate_limit,
195
199
  min_auto_match_precision: opts?.minAutoMatchPrecision ?? 0.99,
196
200
  min_auto_match_count: opts?.minAutoMatchCount ?? 30,
@@ -420,41 +424,30 @@ export class RemoteAdapter implements TargetAdapter {
420
424
  minDeliveredShortlistRecallAtK?: number;
421
425
  minAutoMatchCount?: number;
422
426
  }): Promise<{ run_id?: string; result?: CalibrationResult }> {
423
- const { status, data } = await this.fetchJson("/admin/v1/calibrations", {
424
- method: "POST",
425
- headers: { "Content-Type": "application/json" },
426
- body: JSON.stringify({
427
- dataset_path: opts?.datasetPath,
428
- min_auto_match_precision: opts?.minAutoMatchPrecision,
429
- min_retrieval_recall_at_k: opts?.minRetrievalRecallAtK,
430
- min_delivered_shortlist_recall_at_k: opts?.minDeliveredShortlistRecallAtK,
431
- min_auto_match_count: opts?.minAutoMatchCount,
432
- }),
433
- });
434
- if (status !== 202) {
435
- throw new Error(`Remote calibration start failed (${status}): ${data?.message || data}`);
436
- }
437
- return data;
427
+ void opts;
428
+ throw this.remoteCalibrationNotImplemented();
438
429
  }
439
430
 
440
431
  async calibrateList(): Promise<any[]> {
441
- const { status, data } = await this.fetchJson("/admin/v1/calibrations");
442
- if (status !== 200) throw new Error(`Remote calibration list failed (${status}): ${data?.message || data}`);
443
- return data;
432
+ throw this.remoteCalibrationNotImplemented();
444
433
  }
445
434
 
446
435
  async calibrateShow(runId: string): Promise<any> {
447
- const { status, data } = await this.fetchJson(`/admin/v1/calibrations/${runId}`);
448
- if (status !== 200) throw new Error(`Remote calibration show failed (${status}): ${data?.message || data}`);
449
- return data;
436
+ void runId;
437
+ throw this.remoteCalibrationNotImplemented();
450
438
  }
451
439
 
452
440
  async calibrateApply(runId: string): Promise<any> {
453
- const { status, data } = await this.fetchJson(`/admin/v1/calibrations/${runId}/apply`, {
454
- method: "POST",
455
- });
456
- if (status !== 200) throw new Error(`Remote calibration apply failed (${status}): ${data?.message || data}`);
457
- return data;
441
+ void runId;
442
+ throw this.remoteCalibrationNotImplemented();
443
+ }
444
+
445
+ private remoteCalibrationNotImplemented(): CliError {
446
+ return new CliError(
447
+ `Remote calibration is not implemented for target ${this.serverUrl}; ` +
448
+ "run `skillmux calibrate` against a local target.",
449
+ 2,
450
+ );
458
451
  }
459
452
  }
460
453