@klhapp/skillmux 0.6.0 → 1.0.1
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 +51 -0
- package/README.md +9 -3
- package/docs/configuration.md +13 -10
- package/package.json +1 -1
- package/src/adapters.ts +11 -5
- package/src/calibrate.ts +7 -59
- package/src/cli.ts +521 -869
- package/src/commands/config.ts +202 -0
- package/src/commands/core.ts +52 -0
- package/src/commands/project.ts +412 -0
- package/src/commands/shared.ts +45 -0
- package/src/commands/target.ts +110 -0
- package/src/completions.ts +69 -55
- package/src/config-mutation.ts +65 -0
- package/src/config-watcher.ts +63 -2
- package/src/doctor.ts +19 -1
- package/src/output.ts +21 -23
- package/src/server.ts +228 -63
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,57 @@ 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.0.1](https://github.com/klhq/skillmux/compare/v1.0.0...v1.0.1) (2026-07-24)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
* **cli:** centralize output envelopes ([#77](https://github.com/klhq/skillmux/issues/77)) ([d457b6b](https://github.com/klhq/skillmux/commit/d457b6b24322efe0eda896ebb76cfd8dc9985819))
|
|
14
|
+
* **cli:** split command modules ([#75](https://github.com/klhq/skillmux/issues/75)) ([b1c2e8b](https://github.com/klhq/skillmux/commit/b1c2e8bf69e0a12c95960729eda581468c9c0646))
|
|
15
|
+
* **config:** extract TOML config-mutation module and share watcher test-utils ([#80](https://github.com/klhq/skillmux/issues/80)) ([d084bd6](https://github.com/klhq/skillmux/commit/d084bd6a56a5985dc92b7362171a0e62e9875926))
|
|
16
|
+
|
|
17
|
+
## [1.0.0](https://github.com/klhq/skillmux/compare/v0.6.0...v1.0.0) (2026-07-24)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### ⚠ BREAKING CHANGES
|
|
21
|
+
|
|
22
|
+
* **cli:** init's and target add's custom-directory flag is now --dir instead of --path (matches the manifest schema's own "dir" field on [targets.*] entries) - old --path is removed, no alias.
|
|
23
|
+
* **cli:** bare "skillmux which <skill_id>" is removed. Every other command follows a noun-verb shape (target list, project pin); "which" was the one bare-verb holdout. It's now "skillmux skill which <skill_id>", giving it a noun to sit under (room for future skill-level introspection subcommands). Running the bare form fails with a specific error naming the replacement, echoing back the argument the user typed so the suggested command is directly copy-pasteable.
|
|
24
|
+
* **cli:** skillmux manifest pin/unpin no longer exists. [core] pinning is now skillmux core pin/unpin (added in the prior commit); [project.*] pinning was already fully covered by skillmux project pin/unpin. Running skillmux manifest now fails with a specific error naming both replacements instead of falling through to a generic "unknown command" message.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
* **cli:** add --json to scan, report, install, eval, models download ([456ab33](https://github.com/klhq/skillmux/commit/456ab330ed8b11d08a74bddf08a1827da4b18c19))
|
|
29
|
+
* **cli:** add core pin/unpin command ([c8b2387](https://github.com/klhq/skillmux/commit/c8b2387e01ac99a9e349cdf4ef2b57bb6f241820))
|
|
30
|
+
* **cli:** bring doctor and local-vault init onto the shared options contract ([2517571](https://github.com/klhq/skillmux/commit/25175719ae175bc8aa22d667a27a462b6286b33b))
|
|
31
|
+
* **cli:** remove manifest command in favor of core/project ([1a38b22](https://github.com/klhq/skillmux/commit/1a38b22e51c54e764c11ad5c8400a03537f01929))
|
|
32
|
+
* **cli:** rename --path to --dir on init/target add; disambiguate target vs context ([0aa704b](https://github.com/klhq/skillmux/commit/0aa704b584c377dbb4c67d87690aae6af9a78320))
|
|
33
|
+
* **cli:** rename which to skill which for noun-verb consistency ([dc5b75e](https://github.com/klhq/skillmux/commit/dc5b75ea3dddf9623e7b1a11bc3650fcf4804162))
|
|
34
|
+
* **completions:** generate bash/zsh/fish from one shared command list ([1431c36](https://github.com/klhq/skillmux/commit/1431c365a3e10071405f4c7d3e072da435018cf7))
|
|
35
|
+
* **doctor:** validate the manifest as part of skillmux doctor ([#67](https://github.com/klhq/skillmux/issues/67)) ([8749245](https://github.com/klhq/skillmux/commit/87492450b9af595c10e0f77c21f2371189b5b09f))
|
|
36
|
+
* **manifest:** bulk pin/unpin for manifest --core ([#66](https://github.com/klhq/skillmux/issues/66)) ([0ded6ea](https://github.com/klhq/skillmux/commit/0ded6ea7e58dfccd236582d433a32606f8ec9d36))
|
|
37
|
+
* **output:** add emitSuccess helper for consistent JSON-envelope output ([7e06551](https://github.com/klhq/skillmux/commit/7e06551a154fdb41452d047f470a89e74e81c570))
|
|
38
|
+
* **output:** add typed CliError for exit-code classification ([a88e4f9](https://github.com/klhq/skillmux/commit/a88e4f9e67b64717ab28a82bc829a84a615459d8))
|
|
39
|
+
* **server:** activate config watcher runtime ([#74](https://github.com/klhq/skillmux/issues/74)) ([ba575ae](https://github.com/klhq/skillmux/commit/ba575ae855514a0a816c39dd95e00a763fd7db9d))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
* **cli:** core pin --dry-run --json and which-removal message edge cases ([082ca53](https://github.com/klhq/skillmux/commit/082ca53a811df680287b142da97de702a8a1d764))
|
|
45
|
+
* **config:** enforce live reload policy ([#73](https://github.com/klhq/skillmux/issues/73)) ([4382356](https://github.com/klhq/skillmux/commit/43823564ce24c19460be5f7e9bd4bb3535594d65))
|
|
46
|
+
* **output:** stop misclassifying exit codes by message-substring matching ([acd958f](https://github.com/klhq/skillmux/commit/acd958f127a307c3af1bc20964fce5d906f92485))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
* **cli:** consolidate command plumbing ([#72](https://github.com/klhq/skillmux/issues/72)) ([9fb3e5d](https://github.com/klhq/skillmux/commit/9fb3e5de1aa45790fbb3e1fb3f760e82fd28b027))
|
|
52
|
+
* **cli:** use emitSuccess instead of hand-rolled envelope printing ([ea4c3a5](https://github.com/klhq/skillmux/commit/ea4c3a5ebcf85fa041b7a279ae8ee27d9da8a1fa))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
### Docs
|
|
56
|
+
|
|
57
|
+
* rewrite manifest pin, which, and --path references for the CLI consolidation ([0a81070](https://github.com/klhq/skillmux/commit/0a810705ba04e4bfef96a8098fb0b6c8def86230))
|
|
58
|
+
|
|
8
59
|
## [0.6.0](https://github.com/klhq/skillmux/compare/v0.5.0...v0.6.0) (2026-07-24)
|
|
9
60
|
|
|
10
61
|
|
package/README.md
CHANGED
|
@@ -232,7 +232,7 @@ Use direct targets when you want paths rather than clients:
|
|
|
232
232
|
skillmux init --target agent-skills --yes
|
|
233
233
|
skillmux init --target claude-code --yes
|
|
234
234
|
skillmux init --target codex --yes
|
|
235
|
-
skillmux init --target custom --
|
|
235
|
+
skillmux init --target custom --dir /srv/my-agent/skills --yes
|
|
236
236
|
```
|
|
237
237
|
|
|
238
238
|
The old `agents` and `claude` target names still work and print a deprecation
|
|
@@ -280,10 +280,16 @@ without duplicating existing entries. Project setup runs `sync` unless
|
|
|
280
280
|
existing core pins and makes no guesses. Add another pin with:
|
|
281
281
|
|
|
282
282
|
```sh
|
|
283
|
-
skillmux
|
|
283
|
+
skillmux core pin csv-formatter --yes
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
-
|
|
286
|
+
Pin several skills to `[core]` in one atomic call by passing more than one `skill_id`:
|
|
287
|
+
|
|
288
|
+
```sh
|
|
289
|
+
skillmux core pin csv-formatter pdf-extractor terraform-plans --yes
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Unpin the same way: `skillmux core unpin csv-formatter --yes` (or `skillmux core unpin csv-formatter pdf-extractor --yes` for several at once). `skillmux project pin <group> <skill_id>...` pins into an existing `[project.<group>]` tier instead — create the group first with `skillmux project add-path <group> <path> --yes` if it doesn't exist yet. Every pin is validated before writing — the skill must actually resolve from the vault, and `[core]` stays under its 25-skill cap. When multiple `skill_id`s are given, they're applied to a single in-memory manifest and written once: if any one of them is already pinned elsewhere (or already unpinned, for `unpin`), the whole call fails and nothing is written.
|
|
287
293
|
|
|
288
294
|
Hand-editing `skillmux.toml` still works if you prefer it:
|
|
289
295
|
|
package/docs/configuration.md
CHANGED
|
@@ -118,23 +118,26 @@ project_groups = ["repo1"] # which [project.*] groups materialize into
|
|
|
118
118
|
- `[project.<group>].paths` can list the same project's checkout on more than one machine (e.g. `["/home/alice/code/repo1", "/Users/alice/code/repo1"]`) — `sync` silently skips any entry that doesn't exist on the machine it's running on (see below), so one shared manifest can span machines with different checkout locations without needing per-machine manifests.
|
|
119
119
|
- `[targets.<name>]` — one entry per adopted surface. `skillmux init --target <name> --yes` writes these and scopes newly added targets to the current hostname. Hand-editing is fine as long as `sync` is still allowed to own the directory (see below). An optional `host` limits the target to an exact machine-hostname match; omit it for a global, backward-compatible target. A host mismatch is reported and skipped before any target filesystem operation. `project_groups` is an explicit list, not a boolean — a target only receives the specific groups it names, never every group in the manifest.
|
|
120
120
|
|
|
121
|
-
**Pin/unpin without hand-editing.** `skillmux
|
|
121
|
+
**Pin/unpin without hand-editing.** `skillmux core pin`/`unpin` mutate `[core]` for you, and `skillmux project pin`/`unpin` mutate `[project.*]`, validating with the same rules `sync` enforces (skill must resolve from `vault_path`, no duplicate pins, `[core]` stays under the 25-skill cap) before writing anything:
|
|
122
122
|
|
|
123
123
|
```sh
|
|
124
|
-
skillmux
|
|
125
|
-
skillmux
|
|
126
|
-
skillmux
|
|
127
|
-
skillmux
|
|
128
|
-
skillmux
|
|
124
|
+
skillmux core pin csv-formatter --yes # add to [core]
|
|
125
|
+
skillmux core pin csv-formatter pdf-extractor terraform-plans --yes # pin several skills to [core] in one atomic call
|
|
126
|
+
skillmux project pin repo1 pdf-extractor --yes # add to an existing group
|
|
127
|
+
skillmux core unpin csv-formatter --yes # remove from [core]
|
|
128
|
+
skillmux core unpin csv-formatter pdf-extractor --yes # unpin several skills from [core] in one atomic call
|
|
129
|
+
skillmux project unpin repo1 pdf-extractor --yes # remove from a group (group stays, even if empty)
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
Both commands accept one or more `skill_id` arguments per call; all of them are validated and applied against a single in-memory manifest before anything is written, so if any one of them is already pinned elsewhere (or, for unpin, not currently pinned), the whole call fails and the manifest file is left untouched — no partial pins. To pin into a `[project.<group>]` tier that doesn't exist yet, create it first with `skillmux project add-path <group> <path> --yes`. Hand-editing `skillmux.toml` directly is still fully supported; these commands are a convenience layer over the same file, not a replacement for it.
|
|
132
133
|
|
|
134
|
+
> **Breaking change:** `skillmux manifest pin`/`unpin` is removed. `[core]` pinning is now `skillmux core pin`/`unpin`; `[project.*]` pinning was already available as `skillmux project pin`/`unpin` and is now the only way to do it — there's no more `--path`-based inline group creation from a pin call, use `project add-path` to create the group first.
|
|
135
|
+
>
|
|
133
136
|
> **Breaking change:** `[targets.<name>].project` (a boolean) has been replaced by `project_groups` (an array of `[project.*]` names). A manifest still using the old field fails to parse with an error pointing at the new one. To migrate, replace `project = true` with `project_groups = [...]` listing every group that target previously received (previously *all* groups, unconditionally); replace `project = false` with `project_groups = []`.
|
|
134
137
|
>
|
|
135
138
|
> **Breaking change:** `[project.<group>].repos` has been renamed to `paths` — it was never required to be a git repository, just a local directory, and the old name collided in meaning with `skillmux install <repo>`'s unrelated git-source `repo` concept. A manifest still using `repos` fails to parse with an error pointing at `paths`; migrate by renaming the key (values are unchanged).
|
|
136
139
|
|
|
137
|
-
Every `[core]`/`[project.*]` skill_id must resolve from the canonical `vault_path` — pinning a skill that only exists in a `local_vault_paths` entry (see below) fails `sync` with a distinct error, since the manifest is meant to be portable across machines and a machine-local override wouldn't exist elsewhere. `doctor`
|
|
140
|
+
Every `[core]`/`[project.*]` skill_id must resolve from the canonical `vault_path` — pinning a skill that only exists in a `local_vault_paths` entry (see below) fails `sync` with a distinct error, since the manifest is meant to be portable across machines and a machine-local override wouldn't exist elsewhere. `doctor` validates the manifest as part of its checks, surfacing any violation without writing anything back.
|
|
138
141
|
|
|
139
142
|
### Ownership marker
|
|
140
143
|
|
|
@@ -176,9 +179,9 @@ local_vault_paths = ["~/skills-local"] # optional, default []: override-only,
|
|
|
176
179
|
|
|
177
180
|
**Visibility.** A `skill_id` present in more than one root is silently resolved via the precedence above with no output during normal use — two commands make that resolution visible on demand:
|
|
178
181
|
|
|
179
|
-
- `skillmux which <skill_id>` — prints which root actually serves that skill, and names every root it shadows:
|
|
182
|
+
- `skillmux skill which <skill_id>` — prints which root actually serves that skill, and names every root it shadows:
|
|
180
183
|
```
|
|
181
|
-
$ skillmux which my-skill
|
|
184
|
+
$ skillmux skill which my-skill
|
|
182
185
|
my-skill: serving from /home/user/skills-local
|
|
183
186
|
shadows: /home/user/skills
|
|
184
187
|
```
|
package/package.json
CHANGED
package/src/adapters.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { applyCalibrationRun, getCalibrationRun, listCalibrationRuns, loadDecisionCasesFromFile, openCalibrateDb, runCalibration, type CalibrationResult } from "./calibrate";
|
|
4
4
|
import { createClients } from "./clients";
|
|
5
5
|
import { DEFAULT_CONFIG_PATH, expandHome, loadConfig } from "./config";
|
|
6
|
+
import { CliError } from "./output";
|
|
6
7
|
import {
|
|
7
8
|
computeHash,
|
|
8
9
|
getDottedKey,
|
|
@@ -109,7 +110,7 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
109
110
|
async configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult> {
|
|
110
111
|
const caps = await this.getCapabilities();
|
|
111
112
|
if (caps.persistence === "externally_managed") {
|
|
112
|
-
throw new
|
|
113
|
+
throw new CliError("Configuration is externally managed and cannot be modified", 4);
|
|
113
114
|
}
|
|
114
115
|
return setDottedKey(key, rawValStr, {
|
|
115
116
|
configPath: this.configPath,
|
|
@@ -232,9 +233,14 @@ export class RemoteAdapter implements TargetAdapter {
|
|
|
232
233
|
} catch {
|
|
233
234
|
// text
|
|
234
235
|
}
|
|
236
|
+
if (res.status === 401 || res.status === 403) {
|
|
237
|
+
const message = typeof data === "object" && data ? data.message || data.error || data : data;
|
|
238
|
+
throw new CliError(`Remote server rejected the request (${res.status}): ${message}`, 3);
|
|
239
|
+
}
|
|
235
240
|
return { status: res.status, headers: res.headers, data };
|
|
236
241
|
} catch (err: any) {
|
|
237
|
-
|
|
242
|
+
if (err instanceof CliError) throw err;
|
|
243
|
+
throw new CliError(`Failed to reach remote server "${this.serverUrl}": ${err.message}`, 3);
|
|
238
244
|
}
|
|
239
245
|
}
|
|
240
246
|
|
|
@@ -279,7 +285,7 @@ export class RemoteAdapter implements TargetAdapter {
|
|
|
279
285
|
async configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult> {
|
|
280
286
|
const caps = await this.getCapabilities();
|
|
281
287
|
if (!caps.config_write || caps.persistence === "externally_managed") {
|
|
282
|
-
throw new
|
|
288
|
+
throw new CliError("Remote server configuration is externally managed or read-only", 4);
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
const { status: showStatus, headers: showHeaders, data: showData } = await this.fetchJson("/admin/v1/config");
|
|
@@ -317,10 +323,10 @@ export class RemoteAdapter implements TargetAdapter {
|
|
|
317
323
|
|
|
318
324
|
if (status === 409) {
|
|
319
325
|
if (data?.error === "CONFIG_REVISION_CONFLICT") {
|
|
320
|
-
throw new
|
|
326
|
+
throw new CliError(`Revision conflict: ${data.message || "Remote configuration was modified concurrently"}`, 4);
|
|
321
327
|
}
|
|
322
328
|
if (data?.error === "CONFIG_EXTERNALLY_MANAGED") {
|
|
323
|
-
throw new
|
|
329
|
+
throw new CliError("Configuration is externally managed on the remote server", 4);
|
|
324
330
|
}
|
|
325
331
|
}
|
|
326
332
|
|
package/src/calibrate.ts
CHANGED
|
@@ -732,66 +732,14 @@ export async function applyCalibrationRun(
|
|
|
732
732
|
// --- Atomic TOML write ---
|
|
733
733
|
const { match_score, match_margin, candidate_floor } = run.selected_thresholds;
|
|
734
734
|
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
renameSync(tmpPath, tomlPath);
|
|
735
|
+
const { patchTomlFile } = await import("./config-mutation");
|
|
736
|
+
await patchTomlFile(tomlPath, {
|
|
737
|
+
matchScore: match_score,
|
|
738
|
+
matchMargin: match_margin,
|
|
739
|
+
candidateFloor: candidate_floor,
|
|
740
|
+
runId,
|
|
741
|
+
});
|
|
743
742
|
}
|
|
744
743
|
|
|
745
|
-
// ---------------------------------------------------------------------------
|
|
746
|
-
// TOML patch helpers (surgical text manipulation)
|
|
747
|
-
// ---------------------------------------------------------------------------
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* Patch an existing TOML string to set [inference.thresholds] values and
|
|
751
|
-
* add/update [inference.calibration] with run_id.
|
|
752
|
-
*
|
|
753
|
-
* Strategy:
|
|
754
|
-
* 1. Replace any existing [inference.thresholds] section content
|
|
755
|
-
* 2. Add/replace [inference.calibration] section
|
|
756
|
-
*/
|
|
757
|
-
function patchToml(
|
|
758
|
-
source: string,
|
|
759
|
-
runId: string,
|
|
760
|
-
matchScore: number,
|
|
761
|
-
matchMargin: number,
|
|
762
|
-
candidateFloor: number,
|
|
763
|
-
): string {
|
|
764
|
-
const thresholdsBlock = `[inference.thresholds]\nmatch_score = ${matchScore}\nmatch_margin = ${matchMargin}\ncandidate_floor = ${candidateFloor}\n`;
|
|
765
|
-
const calibrationBlock = `[inference.calibration]\nrun_id = "${runId}"\n`;
|
|
766
|
-
|
|
767
|
-
// Remove existing [inference.thresholds] section
|
|
768
|
-
let result = removeSectionBlock(source, "[inference.thresholds]");
|
|
769
|
-
// Remove existing [inference.calibration] section
|
|
770
|
-
result = removeSectionBlock(result, "[inference.calibration]");
|
|
771
|
-
// Append both sections
|
|
772
|
-
result = result.trimEnd() + "\n\n" + thresholdsBlock + "\n" + calibrationBlock;
|
|
773
|
-
return result;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* Remove a TOML section header and all lines until the next section header
|
|
778
|
-
* (or end of file). Matches exact header string at start of line.
|
|
779
|
-
*/
|
|
780
|
-
function removeSectionBlock(source: string, header: string): string {
|
|
781
|
-
const lines = source.split("\n");
|
|
782
|
-
const out: string[] = [];
|
|
783
|
-
let skipping = false;
|
|
784
|
-
for (const line of lines) {
|
|
785
|
-
if (line.trimEnd() === header) {
|
|
786
|
-
skipping = true;
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
if (skipping && line.startsWith("[")) {
|
|
790
|
-
skipping = false;
|
|
791
|
-
}
|
|
792
|
-
if (!skipping) out.push(line);
|
|
793
|
-
}
|
|
794
|
-
return out.join("\n");
|
|
795
|
-
}
|
|
796
744
|
|
|
797
745
|
|