@klhapp/skillmux 1.9.3 → 1.11.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 (53) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +19 -19
  3. package/docs/README.md +4 -4
  4. package/docs/assets/architecture-dark.svg +39 -32
  5. package/docs/assets/architecture-light.svg +25 -18
  6. package/docs/cli.md +147 -36
  7. package/docs/concepts.md +11 -11
  8. package/docs/configuration.md +7 -5
  9. package/docs/deployment.md +10 -6
  10. package/docs/getting-started.md +18 -14
  11. package/docs/mcp-routing.md +1 -1
  12. package/docs/skill-management.md +17 -11
  13. package/docs/troubleshooting.md +4 -4
  14. package/package.json +1 -1
  15. package/src/adapters.ts +157 -11
  16. package/src/cli.ts +396 -1319
  17. package/src/commands/audit.ts +53 -56
  18. package/src/commands/config.ts +33 -26
  19. package/src/commands/context.ts +104 -0
  20. package/src/commands/core.ts +7 -3
  21. package/src/commands/doctor.ts +97 -0
  22. package/src/commands/eval.ts +22 -15
  23. package/src/commands/init.ts +672 -0
  24. package/src/commands/install.ts +132 -0
  25. package/src/commands/local-vault.ts +60 -0
  26. package/src/commands/models.ts +10 -0
  27. package/src/commands/outdated.ts +2 -1
  28. package/src/commands/project.ts +194 -51
  29. package/src/commands/report.ts +66 -0
  30. package/src/commands/scan.ts +61 -0
  31. package/src/commands/shared.ts +7 -14
  32. package/src/commands/skill.ts +33 -0
  33. package/src/commands/sync.ts +232 -0
  34. package/src/commands/target.ts +45 -15
  35. package/src/commands/update.ts +2 -1
  36. package/src/completions.ts +41 -15
  37. package/src/config-service.ts +4 -54
  38. package/src/context.ts +8 -3
  39. package/src/db-audit.ts +286 -0
  40. package/src/db-index.ts +238 -0
  41. package/src/db.ts +3 -521
  42. package/src/global-flags.ts +46 -0
  43. package/src/init-agents.ts +329 -0
  44. package/src/init-instructions.ts +47 -28
  45. package/src/logger.ts +26 -0
  46. package/src/mcp-registration.ts +89 -0
  47. package/src/output.ts +80 -18
  48. package/src/prompts.ts +75 -20
  49. package/src/router-core.ts +8 -27
  50. package/src/scan.ts +19 -19
  51. package/src/server.ts +161 -14
  52. package/src/toml-writer.ts +51 -0
  53. package/src/init-clients.ts +0 -220
@@ -1,11 +1,11 @@
1
1
  # Managing skills
2
2
 
3
3
  Skillmux keeps skill content in a vault checkout and materializes selected
4
- skills into client directories. The vault source of truth is the logical
4
+ skills into agent directories. The vault source of truth is the logical
5
5
  collection; a checkout is its physical copy. This guide covers the commands
6
6
  that change or inspect that state.
7
7
 
8
- Run these commands on the machine that owns the vault checkout and client
8
+ Run these commands on the machine that owns the vault checkout and agent
9
9
  directories. For a retrieval-only Docker service, manage the mounted checkout
10
10
  on the host and keep the container mount read-only; the server image does not
11
11
  manage host agent directories.
@@ -114,28 +114,28 @@ skill whose on-disk content has drifted from what was last installed —
114
114
  someone may have hand-edited it. Pass `--force` to overwrite anyway. Like
115
115
  `audit prune`, a non-interactive or `--json` run needs `--yes`.
116
116
 
117
- ## Plan client delivery
117
+ ## Plan agent delivery
118
118
 
119
- Use product names for common clients:
119
+ Use product names for common agents:
120
120
 
121
121
  ```sh
122
- skillmux init --client claude-code --client codex --dry-run
122
+ skillmux init --agent claude-code --agent codex --dry-run
123
123
  ```
124
124
 
125
- Use a direct target when you need a known path:
125
+ For a directory that isn't tied to any supported agent, use `target add`
126
+ directly instead of `init`:
126
127
 
127
128
  ```sh
128
- skillmux init --target agent-skills --yes
129
- skillmux init --target custom --dir /srv/my-agent/skills --yes
129
+ skillmux target add my-agent --dir /srv/my-agent/skills --yes
130
130
  ```
131
131
 
132
132
  Skillmux refuses to adopt a target that points to the whole vault because sync
133
133
  would reduce its visible skills. Review that migration first:
134
134
 
135
135
  ```sh
136
- skillmux init --client claude-code --migrate-full-vault \
136
+ skillmux init --agent claude-code --migrate-full-vault \
137
137
  --core csv-formatter --dry-run
138
- skillmux init --client claude-code --migrate-full-vault \
138
+ skillmux init --agent claude-code --migrate-full-vault \
139
139
  --core csv-formatter --yes
140
140
  ```
141
141
 
@@ -170,7 +170,7 @@ skillmux project list
170
170
  skillmux project show my-project
171
171
  skillmux project add-path my-project ~/code/my-project --yes
172
172
  skillmux project pin my-project code-context --yes
173
- skillmux project attach my-project --client claude-code --client codex --yes
173
+ skillmux project attach my-project --agent claude-code --agent codex --yes
174
174
  skillmux project unpin my-project code-context --yes
175
175
  skillmux project detach my-project --target codex --yes
176
176
  skillmux project remove-path my-project ~/code/my-project --yes
@@ -267,6 +267,12 @@ true` with a token, or it refuses to start. `--stats-port` is rejected
267
267
  alongside `--transport http`, since that transport already serves `/stats` on
268
268
  `--port`.
269
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
+
270
276
  ## Target ownership and recovery
271
277
 
272
278
  `skillmux target remove <name> --yes` removes the manifest record and preserves
@@ -113,8 +113,8 @@ also reveals local-overlay shadowing.
113
113
  Skillmux will not change an existing unmarked directory. Adopt it first:
114
114
 
115
115
  ```sh
116
- skillmux init --client claude-code --dry-run
117
- skillmux init --client claude-code --yes
116
+ skillmux init --agent claude-code --dry-run
117
+ skillmux init --agent claude-code --yes
118
118
  ```
119
119
 
120
120
  ### Target points to the full vault
@@ -122,7 +122,7 @@ skillmux init --client claude-code --yes
122
122
  Review the smaller pinned set before converting:
123
123
 
124
124
  ```sh
125
- skillmux init --client claude-code \
125
+ skillmux init --agent claude-code \
126
126
  --migrate-full-vault \
127
127
  --core code-context \
128
128
  --dry-run
@@ -239,7 +239,7 @@ skillmux --context prod config status
239
239
  An MCP token for `/mcp` cannot authenticate this request, and the administrative
240
240
  token cannot authenticate an MCP client. A named context administers the
241
241
  deployed server configuration only; use Skillmux CLI on the machine that owns
242
- client directories for `install`, pinning, or `sync`.
242
+ agent directories for `install`, pinning, or `sync`.
243
243
 
244
244
  ### Client receives `429`
245
245
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klhapp/skillmux",
3
- "version": "1.9.3",
3
+ "version": "1.11.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;
@@ -25,13 +28,28 @@ export interface Capabilities {
25
28
  restart_required_keys: string[];
26
29
  }
27
30
 
28
- export interface TargetAdapterOptions {
31
+ export interface ContextAdapterOptions {
29
32
  configPath?: string;
30
33
  allowInsecure?: boolean;
31
34
  clients?: Clients;
32
35
  }
33
36
 
34
- export interface TargetAdapter {
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
+ * Context 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
+ */
52
+ export interface ContextAdapter {
35
53
  getCapabilities(): Promise<Capabilities>;
36
54
  getConfigShow(): Promise<{ effective: Config; sources: Record<string, string>; active_revision: string }>;
37
55
  getConfigGet(key: string): Promise<unknown>;
@@ -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 {
@@ -51,11 +74,11 @@ export function isLoopbackHost(hostname: string): boolean {
51
74
  );
52
75
  }
53
76
 
54
- export class LocalAdapter implements TargetAdapter {
77
+ export class LocalAdapter implements ContextAdapter {
55
78
  private configPath: string;
56
79
  private clients?: Clients;
57
80
 
58
- constructor(opts?: TargetAdapterOptions) {
81
+ constructor(opts?: ContextAdapterOptions) {
59
82
  this.configPath = resolveConfigPath(opts?.configPath);
60
83
  this.clients = opts?.clients;
61
84
  }
@@ -109,21 +132,90 @@ export class LocalAdapter implements TargetAdapter {
109
132
  return setDottedKey(key, rawValStr, {
110
133
  configPath: this.configPath,
111
134
  dryRun: opts?.dryRun,
112
- targetName: "local",
135
+ contextName: "local",
113
136
  });
114
137
  }
115
138
 
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
- export class RemoteAdapter implements TargetAdapter {
213
+ export class RemoteAdapter implements ContextAdapter {
122
214
  private serverUrl: string;
123
215
  private tokenEnv?: string;
124
216
  private allowInsecure: boolean;
125
217
 
126
- constructor(target: { server: string; token_env?: string }, opts?: TargetAdapterOptions) {
218
+ constructor(target: { server: string; token_env?: string }, opts?: ContextAdapterOptions) {
127
219
  this.serverUrl = target.server.replace(/\/$/, "");
128
220
  this.tokenEnv = target.token_env;
129
221
  this.allowInsecure = opts?.allowInsecure ?? false;
@@ -281,12 +373,66 @@ 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 {
287
- if (target.type === "local") {
432
+ export function createContextAdapter(context: ResolvedContext, opts?: ContextAdapterOptions): ContextAdapter {
433
+ if (context.type === "local") {
288
434
  return new LocalAdapter(opts);
289
435
  } else {
290
- return new RemoteAdapter({ server: target.server, token_env: target.token_env }, opts);
436
+ return new RemoteAdapter({ server: context.server, token_env: context.token_env }, opts);
291
437
  }
292
438
  }