@klhapp/skillmux 1.9.2 → 1.10.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +1 -1
  3. package/docs/README.md +1 -1
  4. package/docs/cli.md +74 -3
  5. package/docs/concepts.md +1 -1
  6. package/docs/configuration.md +52 -1
  7. package/docs/deployment.md +10 -6
  8. package/docs/getting-started.md +1 -1
  9. package/docs/skill-management.md +49 -0
  10. package/package.json +1 -1
  11. package/src/adapters.ts +148 -2
  12. package/src/cli.ts +302 -1291
  13. package/src/clients.ts +17 -0
  14. package/src/commands/audit.ts +54 -51
  15. package/src/commands/config.ts +11 -12
  16. package/src/commands/context.ts +103 -0
  17. package/src/commands/core.ts +5 -1
  18. package/src/commands/doctor.ts +76 -0
  19. package/src/commands/eval.ts +10 -13
  20. package/src/commands/init.ts +621 -0
  21. package/src/commands/install.ts +132 -0
  22. package/src/commands/local-vault.ts +60 -0
  23. package/src/commands/models.ts +10 -0
  24. package/src/commands/outdated.ts +8 -5
  25. package/src/commands/project.ts +37 -11
  26. package/src/commands/report.ts +66 -0
  27. package/src/commands/scan.ts +61 -0
  28. package/src/commands/skill.ts +32 -0
  29. package/src/commands/sync.ts +232 -0
  30. package/src/commands/target.ts +18 -6
  31. package/src/commands/update.ts +11 -5
  32. package/src/concurrency-limiter.ts +61 -0
  33. package/src/config-service.ts +1 -51
  34. package/src/config.ts +5 -0
  35. package/src/context.ts +8 -3
  36. package/src/db-audit.ts +286 -0
  37. package/src/db-index.ts +238 -0
  38. package/src/db.ts +3 -413
  39. package/src/global-flags.ts +46 -0
  40. package/src/install.ts +15 -0
  41. package/src/logger.ts +26 -0
  42. package/src/output.ts +30 -5
  43. package/src/redact.ts +52 -0
  44. package/src/router-core.ts +8 -27
  45. package/src/server.ts +594 -267
  46. package/src/toml-writer.ts +51 -0
  47. package/src/types.ts +7 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ 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.10.0](https://github.com/klhq/skillmux/compare/v1.9.3...v1.10.0) (2026-09-01)
9
+
10
+
11
+ ### Added
12
+
13
+ * **cli:** reject remote context for local-only commands ([#165](https://github.com/klhq/skillmux/issues/165)) ([934bb18](https://github.com/klhq/skillmux/commit/934bb18bce2b86dca8ac44cd8431ebdb53d12024))
14
+ * **cli:** remote admin parity for report, audit prune, eval, doctor ([#166](https://github.com/klhq/skillmux/issues/166)) ([615a396](https://github.com/klhq/skillmux/commit/615a396212d5c0e2df07712bf588c25e91e99773))
15
+
16
+
17
+ ### Changed
18
+
19
+ * **cli:** align context terminology ([#163](https://github.com/klhq/skillmux/issues/163)) ([e942075](https://github.com/klhq/skillmux/commit/e94207584d10e16c17d6d2f5ca9c23bd8aa7846f))
20
+ * **server:** consolidate redactedErrorLog into logger.ts ([#169](https://github.com/klhq/skillmux/issues/169)) ([f7f29e4](https://github.com/klhq/skillmux/commit/f7f29e4ed6ea516e243d47d677e7ede207e47937))
21
+ * split db.ts, extract cli.ts commands, consolidate flag parsing ([#168](https://github.com/klhq/skillmux/issues/168)) ([28a798a](https://github.com/klhq/skillmux/commit/28a798abacfb830aff7a6510c6e892bb28009e5c))
22
+
23
+ ## [1.9.3](https://github.com/klhq/skillmux/compare/v1.9.2...v1.9.3) (2026-08-31)
24
+
25
+
26
+ ### Added
27
+
28
+ * **security:** add egress allowlist for install/update ([#160](https://github.com/klhq/skillmux/issues/160)) ([08779fd](https://github.com/klhq/skillmux/commit/08779fd67aacb941aa56025aecc05cf52e93e0c9))
29
+ * **security:** centralized secret redaction + tamper-evident admin audit trail ([#162](https://github.com/klhq/skillmux/issues/162)) ([f5335ec](https://github.com/klhq/skillmux/commit/f5335ecacf877d4e75821fe9d4be615718055814))
30
+ * **server:** remote report reporting via a stats-only port + authenticated --context ([#158](https://github.com/klhq/skillmux/issues/158)) ([aa19986](https://github.com/klhq/skillmux/commit/aa19986d43d5557d1ef5c6dd4ff53fca7fc9002a))
31
+ * **server:** runtime resource hardening — body/concurrency bounds and inference egress allowlist ([#161](https://github.com/klhq/skillmux/issues/161)) ([5e98765](https://github.com/klhq/skillmux/commit/5e9876581190aead64434c62f70e8d5e4010a9ad))
32
+
33
+
34
+ ### Chores
35
+
36
+ * force release version to 1.9.3 ([97887fe](https://github.com/klhq/skillmux/commit/97887fe4931b183951dec6859333fb1497f6df91))
37
+
8
38
  ## [1.9.2](https://github.com/klhq/skillmux/compare/v1.9.1...v1.9.2) (2026-08-30)
9
39
 
10
40
 
package/README.md CHANGED
@@ -189,7 +189,7 @@ for all three setups.
189
189
  | Surface | User | Purpose | CLI required |
190
190
  | --- | --- | --- | --- |
191
191
  | `/mcp` | AI clients | Resolve and fetch skills | No |
192
- | `/admin/v1/*` | Operators | Inspect or update server configuration | Yes, when using named CLI contexts |
192
+ | `/admin/v1/*` (and `GET /stats`) | Operators | Inspect/update config, stats, audit prune, evaluation, and remote diagnostics | Yes, when using named CLI contexts |
193
193
 
194
194
  MCP clients authenticate only to `/mcp` with the MCP bearer token. Operators
195
195
  use a separate administrative bearer token for `/admin/v1/*`; neither token
package/docs/README.md CHANGED
@@ -34,7 +34,7 @@ to customize behavior; see [Configuration](configuration.md#machine-config-boots
34
34
  | Surface | User | Purpose | CLI required |
35
35
  | --- | --- | --- | --- |
36
36
  | `/mcp` | AI clients | Resolve and fetch skills | No |
37
- | `/admin/v1/*` | Operators | Inspect or update server configuration | Yes, when using named CLI contexts |
37
+ | `/admin/v1/*` (and `GET /stats`) | Operators | Inspect/update config, stats, audit prune, evaluation, and remote diagnostics | Yes, when using named CLI contexts |
38
38
 
39
39
  The MCP and administrative surfaces use separate bearer tokens; possession of
40
40
  one does not grant access to the other. A named CLI context administers the
package/docs/cli.md CHANGED
@@ -14,6 +14,9 @@ In this guide, **local target** means the filesystem and process selected by
14
14
  the built-in CLI context. It does not describe local inference. A local target
15
15
  can call remote inference endpoints.
16
16
 
17
+ Run `skillmux --help` for the full command list, or `skillmux <command> --help`
18
+ (`-h` also works) for one command's usage and flags.
19
+
17
20
  For task-oriented workflows, start with [Getting started](getting-started.md)
18
21
  or [Managing skills](skill-management.md).
19
22
 
@@ -25,13 +28,31 @@ CLI. Its `skillmux --help` surface is intentionally limited to `serve`,
25
28
  `skill which`, and read-only `config` inspection (`show`, `get`, `validate`,
26
29
  `diff`, and `status`). Run `init`, `install`, pinning, `sync`, project or
27
30
  target management, model downloads, contexts, and bare `eval` (vault ranking
28
- evaluation, which needs local embeddings and the vault) on the host.
31
+ evaluation, which needs an embeddings client and the vault) on the host.
29
32
 
30
33
  When the image rejects one of those commands, it exits with code 2. JSON mode
31
34
  uses `CONTAINER_COMMAND_UNSUPPORTED` and includes `rejected_command`,
32
35
  `recommended_host_command`, and a deployment-guide URL. See [the container
33
36
  command contract](deployment.md#container-command-contract) for examples.
34
37
 
38
+ ## Local-only commands and remote context rejection
39
+
40
+ Commands that operate directly on the local vault checkout reject `--context` and
41
+ `--server` (or a configured remote default context) with exit code 2. These 14
42
+ commands are local-only: `install`, `update`, `outdated`, `sync`, `core`,
43
+ `project`, `target`, `local-vault`, `index`, `models`, `scan`, `init`, `serve`,
44
+ and `skill which`.
45
+
46
+ When one of these commands receives a remote context, it exits with code 2.
47
+ Human-mode output reports:
48
+
49
+ ```
50
+ error: `<command>` operates on the local vault only; --context/--server isn't supported here
51
+ ```
52
+
53
+ In `--json` mode, the CLI emits a structured error envelope with `code: "REMOTE_CONTEXT_UNSUPPORTED"`
54
+ and includes `rejected_command`.
55
+
35
56
  ## Global options and target resolution
36
57
 
37
58
  Every target-aware command resolves its execution target deterministically in this order:
@@ -52,6 +73,27 @@ Every target-aware command resolves its execution target deterministically in th
52
73
  | `--allow-insecure` | Allow plaintext HTTP admin requests to non-loopback addresses |
53
74
  | `--verbose` | Output diagnostic stack traces for errors |
54
75
 
76
+ `--context`/`--server` selects which Skillmux admin instance a command talks to
77
+ over `/admin/v1/*` — nothing more. That's one of three independent axes in this
78
+ system:
79
+
80
+ 1. Which vault checkout backs Core/Project pinning — always a local Git checkout
81
+ on the machine running the CLI. `install`/`update`/`sync`/`core`/`project`/
82
+ `target`/`local-vault`/`index`/`models`/`scan`/`init`/`outdated`/`serve`/
83
+ `skill which` operate on it. There is no remote version of this — Git and the
84
+ deployment process move content between checkouts, not Skillmux's own
85
+ commands.
86
+ 2. Which MCP server an agent queries for Routed retrieval — the agent's own MCP
87
+ client configuration (local stdio vs. remote HTTP), entirely separate from
88
+ `skillmux context`.
89
+ 3. Which Skillmux instance the CLI's admin commands act on — `config`, `report`,
90
+ `audit prune`, `eval`/`eval promote`, and `doctor`. This is the only thing
91
+ `--context`/`--server` actually selects.
92
+
93
+ The commands above belong entirely to axis 1. `--context` has no meaning for
94
+ them — not "risky," a category error, the same way `--context prod` wouldn't
95
+ mean anything on `ls`.
96
+
55
97
  ---
56
98
 
57
99
  ## Context management (`skillmux context`)
@@ -286,6 +328,11 @@ Show which root actually serves a skill_id, and every root it shadows:
286
328
  skillmux skill which csv-formatter
287
329
  ```
288
330
 
331
+ > [!NOTE]
332
+ > `skill which` performs local vault-checkout shadow resolution (which root
333
+ > wins on the local filesystem) and is unrelated to semantic MCP skill routing
334
+ > (`resolve_skill`/`fetch_skill`).
335
+
289
336
  ---
290
337
 
291
338
  ## Observability and evaluation (`skillmux report`, `audit`, `eval`)
@@ -352,10 +399,10 @@ The HTTP server has two separate surfaces:
352
399
  | Surface | User | Purpose | CLI required |
353
400
  | --- | --- | --- | --- |
354
401
  | `/mcp` | AI clients | Resolve and fetch skills | No |
355
- | `/admin/v1/*` | Operators | Inspect or update server configuration | Yes, when using named CLI contexts |
402
+ | `/admin/v1/*` (and `GET /stats`) | Operators | Inspect/update config, stats, audit prune, evaluation, and remote diagnostics | Yes, when using named CLI contexts |
356
403
 
357
404
  The MCP bearer token applies only to `/mcp`. The administrative bearer token
358
- below applies only to `/admin/v1/*`; neither credential grants access to the
405
+ below applies only to `/admin/v1/*` (and `GET /stats`); neither credential grants access to the
359
406
  other surface. Named contexts use the latter to administer the deployed server,
360
407
  not any remote client skill directory.
361
408
 
@@ -374,6 +421,30 @@ Requests require `Authorization: Bearer <token>` where `<token>` matches the env
374
421
  | `/admin/v1/capabilities` | `GET` | Advertises server features (`config_read`, `config_write`, `persistence`) |
375
422
  | `/admin/v1/config` | `GET` | Returns desired/effective config, sources, and `ETag` revision hash |
376
423
  | `/admin/v1/config` | `PATCH` | Applies dotted-key updates; requires matching `If-Match` header |
424
+ | `/admin/v1/audit/prune` | `POST` | Prunes audit/fetch/admin_audit rows older than cutoff; requires `confirm: true` unless `dry_run: true` |
425
+ | `/admin/v1/eval` | `POST` | Runs vault ranking evaluation server-side and returns `EvalReport` |
426
+ | `/admin/v1/eval/promote` | `POST` | Returns candidate promoted eval cases from the server's audit database; requires `since` |
427
+
428
+ ### Remote command capabilities
429
+
430
+ Named CLI contexts (`--context <name>` or `--server <url>`) support the following administrative and diagnostic operations:
431
+
432
+ - `skillmux config` (`show`, `get`, `set`, `validate`, `diff`, `status`): inspect and modify remote server configuration over `/admin/v1/*`.
433
+ - `skillmux report --since <window>`: fetches usage and retrieval metrics from the remote server's `GET /stats`.
434
+ - `skillmux audit prune [--older-than <window>] [--dry-run] [--yes]`: prunes or dry-run counts audit records on the remote server via `POST /admin/v1/audit/prune`.
435
+ - `skillmux eval`: executes ranking evaluation against the remote server's in-process runtime via `POST /admin/v1/eval`.
436
+ - `skillmux eval promote --since <window>`: fetches promotable candidates from the remote server's audit db via `POST /admin/v1/eval/promote`, dedups against the local fixture file, and writes locally.
437
+ - `skillmux doctor`: inspects remote server status, readiness, deployment runtime, and capabilities without requiring local vault access.
438
+
439
+ ---
440
+
441
+ ## Color
442
+
443
+ Interactive terminal output uses a small amount of color: red for error
444
+ messages, yellow for `warning:` lines, green/red for `doctor`'s `ok`/`fail`
445
+ checks, and bold table headers. Color is automatic — it's on only when
446
+ stdout is a TTY and off whenever `NO_COLOR` is set or output is piped or
447
+ redirected. `--json` output never includes color codes.
377
448
 
378
449
  ---
379
450
 
package/docs/concepts.md CHANGED
@@ -94,7 +94,7 @@ An HTTP server has two separate surfaces:
94
94
  | Surface | User | Purpose | CLI required |
95
95
  | --- | --- | --- | --- |
96
96
  | `/mcp` | AI clients | Resolve and fetch skills | No |
97
- | `/admin/v1/*` | Operators | Inspect or update server configuration | Yes, when using named CLI contexts |
97
+ | `/admin/v1/*` (and `GET /stats`) | Operators | Inspect/update config, stats, audit prune, evaluation, and remote diagnostics | Yes, when using named CLI contexts |
98
98
 
99
99
  MCP authentication protects `/mcp`; administrative authentication protects
100
100
  `/admin/v1/*`. Their bearer tokens are distinct and do not grant access across
@@ -184,7 +184,7 @@ The HTTP server provides separate MCP and administrative surfaces:
184
184
  | Surface | User | Purpose | CLI required |
185
185
  | --- | --- | --- | --- |
186
186
  | `/mcp` | AI clients | Resolve and fetch skills | No |
187
- | `/admin/v1/*` | Operators | Inspect or update server configuration | Yes, when using named CLI contexts |
187
+ | `/admin/v1/*` (and `GET /stats`) | Operators | Inspect/update config, stats, audit prune, evaluation, and remote diagnostics | Yes, when using named CLI contexts |
188
188
 
189
189
  ```toml
190
190
  [server]
@@ -192,6 +192,8 @@ hostname = "127.0.0.1"
192
192
  auth_enabled = false
193
193
  auth_token_env = "SKILLMUX_AUTH_TOKEN"
194
194
  allowed_origins = []
195
+ max_body_bytes = 1048576
196
+ max_concurrent_requests = 100
195
197
 
196
198
  [server.rate_limit]
197
199
  enabled = false
@@ -205,6 +207,8 @@ token_env = "SKILLMUX_ADMIN_TOKEN"
205
207
 
206
208
  Defaults are loopback-only (`hostname = "127.0.0.1"`) with CORS deny-by-default (`allowed_origins = []`), so a zero-config `skillmux serve --transport http` is not reachable from the network or from a browser tab on another origin. Docker sets `hostname` to `0.0.0.0` automatically (`RUNNING_IN_DOCKER=true`) since port-mapping needs the container to accept connections on all interfaces.
207
209
 
210
+ `max_body_bytes` (default 1 MiB) and `max_concurrent_requests` (default 100) are positive resource bounds on the `--transport http` listener: unlike `rate_limit`, which is opt-in and off by default, these apply out of the box so the transport is never unbounded by omission. A request whose body exceeds `max_body_bytes` is rejected with `413 Payload Too Large` before it's fully read; once `max_concurrent_requests` requests are in flight, an additional request is rejected with `503 Service Unavailable`. Both are unrelated to `rate_limit`, which bounds request *count* per client over time rather than body size or concurrency.
211
+
208
212
  Before exposing HTTP beyond localhost, set `hostname` to a reachable interface, `auth_enabled = true` with a token, and populate `allowed_origins` with the specific origins that need browser access. `rate_limit.trust_proxy` should stay `false` unless a trusted reverse proxy sets `X-Forwarded-For`: it's otherwise a client-controlled, spoofable header, and trusting it defeats per-client rate limiting.
209
213
 
210
214
  `server.auth_token_env` names the MCP token for AI clients calling `/mcp`.
@@ -219,6 +223,53 @@ Inside the server image, only read-only `config show`, `get`, `validate`,
219
223
  host CLI; the image returns `CONTAINER_COMMAND_UNSUPPORTED` with the exact host
220
224
  command to run. See [Deployment](deployment.md#container-command-contract).
221
225
 
226
+ ## Secret redaction and the admin audit trail
227
+
228
+ Skillmux redacts resolved credential values before they can reach CLI
229
+ output or server logs. Every `*_env`-suffixed config key (`api_key_env`,
230
+ `token_env`, `auth_token_env`) names an environment variable rather than
231
+ storing the secret itself; when an error message would otherwise embed that
232
+ variable's current value — or a credential typed directly into a URL, e.g.
233
+ `https://user:TOKEN@host/repo.git` for private-repo git auth — it is
234
+ replaced with `[REDACTED]` before the CLI's error handler or the server's
235
+ exception logging writes it out. This applies in both human and `--json`
236
+ output and requires no configuration; with no `*_env` keys set, it is a
237
+ no-op.
238
+
239
+ Every successful `PATCH /admin/v1/config` mutation appends one row to an
240
+ `admin_audit` table in the same `audit.sqlite3` used for fetch/resolve
241
+ telemetry, recording the timestamp, changed keys with their old and new
242
+ values, and the resulting config revision hash. A rejected request (stale
243
+ `If-Match`, read-only config) writes no row. Each row's hash is chained to
244
+ the previous row's hash, so any row deleted or edited outside the running
245
+ server breaks the chain — detectable by walking the table and recomputing
246
+ the chain, without a dedicated query endpoint. `admin_audit` rows are
247
+ pruned by the same `[audit] retention_days` setting as the rest of
248
+ `audit.sqlite3`; there is no separate retention config for admin history.
249
+
250
+ ## Egress allowlist
251
+
252
+ ```toml
253
+ [egress]
254
+ allowed_hosts = ["github.com"]
255
+ ```
256
+
257
+ Unset by default, matching Skillmux's opt-in security posture elsewhere.
258
+ When set, `skillmux install` and `skillmux update` refuse to fetch from any
259
+ git host not on the list, checked before the network call — see
260
+ [Managing skills](skill-management.md#restrict-which-hosts-install-and-update-can-reach).
261
+ `file://` sources are exempt (no network egress occurs; they're already
262
+ gated by `--allow-local-source`), and host matching is exact and
263
+ case-insensitive, with no glob support.
264
+
265
+ The same `allowed_hosts` list also gates remote-inference calls: when set,
266
+ a `[inference.embedding]` or `[inference.reranker]` `endpoint` host not on
267
+ the list is rejected before the HTTP request, surfaced the same way as any
268
+ other embedding/reranker configuration error (`resolve_skill` degrades to
269
+ the strongest available retrieval lane rather than failing outright). This
270
+ does not apply to `inference.mode = "local"`, which never makes a network
271
+ call.
272
+
222
273
  ## Tiers and the manifest
223
274
 
224
275
  `skillmux init` and `skillmux sync` manage native delivery by pinning selected
@@ -180,7 +180,7 @@ The shared server exposes two distinct HTTP surfaces:
180
180
  | Surface | User | Purpose | CLI required |
181
181
  | --- | --- | --- | --- |
182
182
  | `/mcp` | AI clients | Resolve and fetch skills | No |
183
- | `/admin/v1/*` | Operators | Inspect or update server configuration | Yes, when using named CLI contexts |
183
+ | `/admin/v1/*` (and `GET /stats`) | Operators | Inspect/update config, stats, audit prune, evaluation, and remote diagnostics | Yes, when using named CLI contexts |
184
184
 
185
185
  Configure and distribute separate bearer tokens. An MCP token authenticates an
186
186
  AI client to `/mcp` only; an administrative token authenticates an operator to
@@ -280,6 +280,9 @@ skillmux context add prod \
280
280
  --token-env SKILLMUX_PROD_ADMIN_TOKEN
281
281
  skillmux context use prod
282
282
  skillmux config status
283
+ skillmux --context prod report --since 7d
284
+ skillmux --context prod audit prune --dry-run
285
+ skillmux --context prod doctor
283
286
  ```
284
287
 
285
288
  The context stores the token environment variable name. Export its value in
@@ -291,11 +294,12 @@ Enable the admin API and use a separate admin token in server configuration.
291
294
  Read [CLI reference](cli.md#administrative-http-api-adminv1) for routes and
292
295
  [Configuration](configuration.md#http-server) for reload behavior.
293
296
 
294
- Named contexts administer the deployed server configuration only. They do not
295
- install, pin, synchronize, or otherwise manage skill directories on remote
296
- client machines. Run those filesystem-management commands through Skillmux CLI
297
- on the machine that owns the directories. The full and slim server images read
298
- their mounted vault checkout and do not manage host agent directories.
297
+ Named contexts administer the deployed server only configuration, stats,
298
+ audit prune, evaluation, and remote diagnostics. They do not install, pin,
299
+ synchronize, or otherwise manage skill directories on remote client machines.
300
+ Run those filesystem-management commands through Skillmux CLI on the machine
301
+ that owns the directories. The full and slim server images read their mounted
302
+ vault checkout and do not manage host agent directories.
299
303
 
300
304
  ## Persistent data and backups
301
305
 
@@ -261,7 +261,7 @@ The server keeps its two HTTP surfaces separate:
261
261
  | Surface | User | Purpose | CLI required |
262
262
  | --- | --- | --- | --- |
263
263
  | `/mcp` | AI clients | Resolve and fetch skills | No |
264
- | `/admin/v1/*` | Operators | Inspect or update server configuration | Yes, when using named CLI contexts |
264
+ | `/admin/v1/*` (and `GET /stats`) | Operators | Inspect/update config, stats, audit prune, evaluation, and remote diagnostics | Yes, when using named CLI contexts |
265
265
 
266
266
  Configure separate MCP and administrative bearer tokens; one never grants
267
267
  access to the other. Named CLI contexts can administer this deployed server,
@@ -53,6 +53,22 @@ from a local repo is a deliberate, interactive choice:
53
53
  skillmux install file:///path/to/local/repo --allow-local-source
54
54
  ```
55
55
 
56
+ ### Restrict which hosts install and update can reach
57
+
58
+ By default `install` and `update` will fetch from any git host. Set
59
+ `[egress] allowed_hosts` in your config to restrict both to an explicit list:
60
+
61
+ ```toml
62
+ [egress]
63
+ allowed_hosts = ["github.com", "git.example.com"]
64
+ ```
65
+
66
+ A host not on the list is rejected before any network fetch, for both a new
67
+ `skillmux install <source>` and a `skillmux update` re-fetching a skill's
68
+ recorded origin. This doesn't apply to `file://` sources, which never leave
69
+ the local filesystem and are already gated by `--allow-local-source` above.
70
+ Leaving `allowed_hosts` unset (the default) leaves both commands unrestricted.
71
+
56
72
  ## Scan a vault or candidate
57
73
 
58
74
  ```sh
@@ -211,6 +227,7 @@ skillmux doctor
211
227
  ```sh
212
228
  skillmux report --since 7d
213
229
  skillmux report --server http://host:3000 --since 7d
230
+ skillmux report --context prod --since 7d
214
231
  ```
215
232
 
216
233
  `skillmux report` aggregates total requests, empty shortlist count and rate,
@@ -224,6 +241,38 @@ Top empty shortlist queries point to missing skills or weak skill descriptions.
224
241
  `--since` accepts windows such as `1h`, `7d`, and `1m`, plus absolute dates and
225
242
  timestamps.
226
243
 
244
+ Register a remote deployment once with `skillmux context add`, then reuse it by
245
+ name instead of retyping `--server` (and, if the deployment requires
246
+ authentication, its token) on every call:
247
+
248
+ ```sh
249
+ skillmux context add prod --server http://host:3000 --token-env SKILLMUX_AUTH_TOKEN
250
+ skillmux report --context prod --since 7d
251
+ ```
252
+
253
+ `--context` and bare `--server` both hit `GET /stats` on the target and require
254
+ an HTTP transport listening there. A stdio-only deployment (the common case
255
+ for an MCP server spawned by a host over stdin/stdout) has no such listener by
256
+ default. Give it a narrow, read-only one — just `/health` and `/stats`, none
257
+ of the MCP tool surface — without switching its primary transport:
258
+
259
+ ```sh
260
+ skillmux serve --transport stdio --stats-port 4317
261
+ ```
262
+
263
+ The stats port inherits the same `[server]` bind-posture rule as the `http`
264
+ transport (see [Configuration](configuration.md#http-server)): binding it
265
+ to anything other than a loopback address requires `server.auth_enabled =
266
+ true` with a token, or it refuses to start. `--stats-port` is rejected
267
+ alongside `--transport http`, since that transport already serves `/stats` on
268
+ `--port`.
269
+
270
+ `--context`/`--server` work the same way for `audit prune`, `eval`, `eval
271
+ promote`, and `doctor` — each hits the matching `/admin/v1/*` route on the
272
+ named deployment instead of the local vault and audit db. See [CLI
273
+ reference](cli.md#administrative-http-api-adminv1) for the full remote
274
+ command surface and route table.
275
+
227
276
  ## Target ownership and recovery
228
277
 
229
278
  `skillmux target remove <name> --yes` removes the manifest record and preserves
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klhapp/skillmux",
3
- "version": "1.9.2",
3
+ "version": "1.10.0",
4
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,
package/src/adapters.ts CHANGED
@@ -14,8 +14,11 @@ import {
14
14
  type ConfigStatusResponse,
15
15
  type SetConfigResult,
16
16
  } from "./config-service";
17
- import type { ResolvedTarget } from "./context";
17
+ import type { ResolvedContext } from "./context";
18
18
  import type { Clients, Config } from "./types";
19
+ import { getStats, parseSince, type StatsResponse } from "./stats";
20
+ import { countPrunable, openAudit, pruneAuditBefore, type PruneResult } from "./db";
21
+ import { buildPromotedCases, evalVault, queryPromotableFetches, type EvalCase, type EvalReport } from "./eval";
19
22
 
20
23
  export interface Capabilities {
21
24
  config_read: boolean;
@@ -31,6 +34,21 @@ export interface TargetAdapterOptions {
31
34
  clients?: Clients;
32
35
  }
33
36
 
37
+ export interface AuditPruneOptions {
38
+ older_than?: string;
39
+ dry_run?: boolean;
40
+ confirm?: boolean;
41
+ }
42
+
43
+ export interface AuditPruneResult extends PruneResult {
44
+ dry_run: boolean;
45
+ cutoff: string | null;
46
+ }
47
+
48
+ /**
49
+ * Target adapter: `local` = this CLI process has the Skillmux runtime (vault, index,
50
+ * audit db, embeddings/reranker clients) in-process; `remote` = thin network client to an external process.
51
+ */
34
52
  export interface TargetAdapter {
35
53
  getCapabilities(): Promise<Capabilities>;
36
54
  getConfigShow(): Promise<{ effective: Config; sources: Record<string, string>; active_revision: string }>;
@@ -39,6 +57,11 @@ export interface TargetAdapter {
39
57
  configDiff(): Promise<{ diff: Record<string, { prior: unknown; resulting: unknown }> }>;
40
58
  configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult>;
41
59
  configStatus(): Promise<ConfigStatusResponse>;
60
+ getStats(since: string): Promise<StatsResponse>;
61
+ auditPrune(opts?: AuditPruneOptions): Promise<AuditPruneResult>;
62
+ auditCount(older_than?: string): Promise<AuditPruneResult>;
63
+ evalRun(): Promise<EvalReport>;
64
+ evalPromote(since: string): Promise<EvalCase[]>;
42
65
  }
43
66
 
44
67
  export function isLoopbackHost(hostname: string): boolean {
@@ -116,6 +139,75 @@ export class LocalAdapter implements TargetAdapter {
116
139
  async configStatus(): Promise<ConfigStatusResponse> {
117
140
  return getLocalConfigStatus(this.configPath);
118
141
  }
142
+
143
+ async getStats(since: string): Promise<StatsResponse> {
144
+ const config = await loadConfig(this.configPath);
145
+ const stateDir = expandHome(config.state_dir);
146
+ const db = openAudit(stateDir);
147
+ try {
148
+ return getStats(db, since);
149
+ } finally {
150
+ db.close();
151
+ }
152
+ }
153
+
154
+ async auditPrune(opts?: AuditPruneOptions): Promise<AuditPruneResult> {
155
+ const config = await loadConfig(this.configPath);
156
+ const stateDir = expandHome(config.state_dir);
157
+ const olderThan = opts?.older_than;
158
+ const dryRun = opts?.dry_run ?? false;
159
+
160
+ let cutoff: Date;
161
+ if (olderThan) {
162
+ cutoff = parseSince(olderThan);
163
+ } else {
164
+ const retentionDays = config.audit?.retention_days ?? 90;
165
+ if (retentionDays <= 0) {
166
+ return {
167
+ audit_deleted: 0,
168
+ fetch_deleted: 0,
169
+ admin_audit_deleted: 0,
170
+ dry_run: dryRun,
171
+ cutoff: null,
172
+ };
173
+ }
174
+ cutoff = new Date(Date.now() - retentionDays * 86_400_000);
175
+ }
176
+ const cutoffIso = cutoff.toISOString();
177
+
178
+ const db = openAudit(stateDir);
179
+ try {
180
+ if (dryRun) {
181
+ const counts = countPrunable(db, cutoffIso);
182
+ return { ...counts, dry_run: true, cutoff: cutoffIso };
183
+ }
184
+ const counts = pruneAuditBefore(db, cutoffIso);
185
+ return { ...counts, dry_run: false, cutoff: cutoffIso };
186
+ } finally {
187
+ db.close();
188
+ }
189
+ }
190
+
191
+ async auditCount(older_than?: string): Promise<AuditPruneResult> {
192
+ return this.auditPrune({ older_than, dry_run: true });
193
+ }
194
+
195
+ async evalRun(): Promise<EvalReport> {
196
+ return evalVault();
197
+ }
198
+
199
+ async evalPromote(since: string): Promise<EvalCase[]> {
200
+ const sinceDate = parseSince(since);
201
+ const sinceIso = sinceDate.toISOString();
202
+ const config = await loadConfig(this.configPath);
203
+ const stateDir = expandHome(config.state_dir);
204
+ const db = openAudit(stateDir);
205
+ try {
206
+ return buildPromotedCases(queryPromotableFetches(db, sinceIso));
207
+ } finally {
208
+ db.close();
209
+ }
210
+ }
119
211
  }
120
212
 
121
213
  export class RemoteAdapter implements TargetAdapter {
@@ -281,9 +373,63 @@ export class RemoteAdapter implements TargetAdapter {
281
373
  }
282
374
  return data.runtime;
283
375
  }
376
+
377
+ async getStats(since: string): Promise<StatsResponse> {
378
+ const searchParams = new URLSearchParams({ since });
379
+ const { status, data } = await this.fetchJson(`/stats?${searchParams.toString()}`);
380
+ if (status !== 200) {
381
+ throw new Error(`Remote stats fetch failed (${status}): ${typeof data === "object" ? data?.message || data?.error || JSON.stringify(data) : data}`);
382
+ }
383
+ return data as StatsResponse;
384
+ }
385
+
386
+ async auditPrune(opts?: AuditPruneOptions): Promise<AuditPruneResult> {
387
+ const { status, data } = await this.fetchJson("/admin/v1/audit/prune", {
388
+ method: "POST",
389
+ headers: { "Content-Type": "application/json" },
390
+ body: JSON.stringify({
391
+ ...(opts?.older_than ? { older_than: opts.older_than } : {}),
392
+ ...(opts?.dry_run !== undefined ? { dry_run: opts.dry_run } : {}),
393
+ ...(opts?.confirm !== undefined ? { confirm: opts.confirm } : {}),
394
+ }),
395
+ });
396
+ if (status !== 200) {
397
+ const message = typeof data === "object" ? data?.message || data?.error || JSON.stringify(data) : data;
398
+ throw new Error(`Remote audit prune failed (${status}): ${message}`);
399
+ }
400
+ return data as AuditPruneResult;
401
+ }
402
+
403
+ async auditCount(older_than?: string): Promise<AuditPruneResult> {
404
+ return this.auditPrune({ older_than, dry_run: true });
405
+ }
406
+
407
+ async evalRun(): Promise<EvalReport> {
408
+ const { status, data } = await this.fetchJson("/admin/v1/eval", {
409
+ method: "POST",
410
+ });
411
+ if (status !== 200) {
412
+ const message = typeof data === "object" ? data?.message || data?.error || JSON.stringify(data) : data;
413
+ throw new Error(`Remote eval failed (${status}): ${message}`);
414
+ }
415
+ return data as EvalReport;
416
+ }
417
+
418
+ async evalPromote(since: string): Promise<EvalCase[]> {
419
+ const { status, data } = await this.fetchJson("/admin/v1/eval/promote", {
420
+ method: "POST",
421
+ headers: { "Content-Type": "application/json" },
422
+ body: JSON.stringify({ since }),
423
+ });
424
+ if (status !== 200) {
425
+ const message = typeof data === "object" ? data?.message || data?.error || JSON.stringify(data) : data;
426
+ throw new Error(`Remote eval promote failed (${status}): ${message}`);
427
+ }
428
+ return data.candidates as EvalCase[];
429
+ }
284
430
  }
285
431
 
286
- export function createTargetAdapter(target: ResolvedTarget, opts?: TargetAdapterOptions): TargetAdapter {
432
+ export function createTargetAdapter(target: ResolvedContext, opts?: TargetAdapterOptions): TargetAdapter {
287
433
  if (target.type === "local") {
288
434
  return new LocalAdapter(opts);
289
435
  } else {