@klhapp/skillmux 0.6.0 → 1.0.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 CHANGED
@@ -5,6 +5,48 @@ 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.0](https://github.com/klhq/skillmux/compare/v0.6.0...v1.0.0) (2026-07-24)
9
+
10
+
11
+ ### ⚠ BREAKING CHANGES
12
+
13
+ * **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.
14
+ * **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.
15
+ * **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.
16
+
17
+ ### Added
18
+
19
+ * **cli:** add --json to scan, report, install, eval, models download ([456ab33](https://github.com/klhq/skillmux/commit/456ab330ed8b11d08a74bddf08a1827da4b18c19))
20
+ * **cli:** add core pin/unpin command ([c8b2387](https://github.com/klhq/skillmux/commit/c8b2387e01ac99a9e349cdf4ef2b57bb6f241820))
21
+ * **cli:** bring doctor and local-vault init onto the shared options contract ([2517571](https://github.com/klhq/skillmux/commit/25175719ae175bc8aa22d667a27a462b6286b33b))
22
+ * **cli:** remove manifest command in favor of core/project ([1a38b22](https://github.com/klhq/skillmux/commit/1a38b22e51c54e764c11ad5c8400a03537f01929))
23
+ * **cli:** rename --path to --dir on init/target add; disambiguate target vs context ([0aa704b](https://github.com/klhq/skillmux/commit/0aa704b584c377dbb4c67d87690aae6af9a78320))
24
+ * **cli:** rename which to skill which for noun-verb consistency ([dc5b75e](https://github.com/klhq/skillmux/commit/dc5b75ea3dddf9623e7b1a11bc3650fcf4804162))
25
+ * **completions:** generate bash/zsh/fish from one shared command list ([1431c36](https://github.com/klhq/skillmux/commit/1431c365a3e10071405f4c7d3e072da435018cf7))
26
+ * **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))
27
+ * **manifest:** bulk pin/unpin for manifest --core ([#66](https://github.com/klhq/skillmux/issues/66)) ([0ded6ea](https://github.com/klhq/skillmux/commit/0ded6ea7e58dfccd236582d433a32606f8ec9d36))
28
+ * **output:** add emitSuccess helper for consistent JSON-envelope output ([7e06551](https://github.com/klhq/skillmux/commit/7e06551a154fdb41452d047f470a89e74e81c570))
29
+ * **output:** add typed CliError for exit-code classification ([a88e4f9](https://github.com/klhq/skillmux/commit/a88e4f9e67b64717ab28a82bc829a84a615459d8))
30
+ * **server:** activate config watcher runtime ([#74](https://github.com/klhq/skillmux/issues/74)) ([ba575ae](https://github.com/klhq/skillmux/commit/ba575ae855514a0a816c39dd95e00a763fd7db9d))
31
+
32
+
33
+ ### Fixed
34
+
35
+ * **cli:** core pin --dry-run --json and which-removal message edge cases ([082ca53](https://github.com/klhq/skillmux/commit/082ca53a811df680287b142da97de702a8a1d764))
36
+ * **config:** enforce live reload policy ([#73](https://github.com/klhq/skillmux/issues/73)) ([4382356](https://github.com/klhq/skillmux/commit/43823564ce24c19460be5f7e9bd4bb3535594d65))
37
+ * **output:** stop misclassifying exit codes by message-substring matching ([acd958f](https://github.com/klhq/skillmux/commit/acd958f127a307c3af1bc20964fce5d906f92485))
38
+
39
+
40
+ ### Changed
41
+
42
+ * **cli:** consolidate command plumbing ([#72](https://github.com/klhq/skillmux/issues/72)) ([9fb3e5d](https://github.com/klhq/skillmux/commit/9fb3e5de1aa45790fbb3e1fb3f760e82fd28b027))
43
+ * **cli:** use emitSuccess instead of hand-rolled envelope printing ([ea4c3a5](https://github.com/klhq/skillmux/commit/ea4c3a5ebcf85fa041b7a279ae8ee27d9da8a1fa))
44
+
45
+
46
+ ### Docs
47
+
48
+ * rewrite manifest pin, which, and --path references for the CLI consolidation ([0a81070](https://github.com/klhq/skillmux/commit/0a810705ba04e4bfef96a8098fb0b6c8def86230))
49
+
8
50
  ## [0.6.0](https://github.com/klhq/skillmux/compare/v0.5.0...v0.6.0) (2026-07-24)
9
51
 
10
52
 
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 --path /srv/my-agent/skills --yes
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 manifest pin csv-formatter --core
283
+ skillmux core pin csv-formatter --yes
284
284
  ```
285
285
 
286
- Unpin the same way: `skillmux manifest unpin csv-formatter --core`. `--project <group>` pins into a `[project.<group>]` tier instead (add `--path <path>` the first time, to create the group). Every pin is validated before writing — the skill must actually resolve from the vault, and `[core]` stays under its 25-skill cap.
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
 
@@ -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 manifest pin`/`unpin` mutate `[core]`/`[project.*]` for you, 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:
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 manifest pin csv-formatter --core # add to [core]
125
- skillmux manifest pin pdf-extractor --project repo1 # add to an existing group
126
- skillmux manifest pin pdf-extractor --project repo2 --path ~/code/repo2 # create a new group
127
- skillmux manifest unpin csv-formatter --core # remove from [core]
128
- skillmux manifest unpin pdf-extractor --project repo1 # remove from a group (group stays, even if empty)
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
- `--path` is only accepted when `--project <group>` names a group that doesn't exist yet passing it for an existing group is rejected, since changing an existing group's `paths` isn't this command's job (append another entry by hand-editing `skillmux.toml` instead). 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
+ 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` does not validate the manifest yet.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klhapp/skillmux",
3
- "version": "0.6.0",
3
+ "version": "1.0.0",
4
4
  "description": "Local read-only MCP server routing natural-language task queries to skills in a SKILL.md vault, with zero-loss delivery",
5
5
  "type": "module",
6
6
  "private": false,
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 Error("Configuration is externally managed and cannot be modified");
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
- throw new Error(`Failed to reach remote server "${this.serverUrl}": ${err.message}`);
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 Error("Remote server configuration is externally managed or read-only");
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 Error(`Revision conflict: ${data.message || "Remote configuration was modified concurrently"}`);
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 Error("Configuration is externally managed on the remote server");
329
+ throw new CliError("Configuration is externally managed on the remote server", 4);
324
330
  }
325
331
  }
326
332