@klhapp/skillmux 1.10.0 → 1.11.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/src/adapters.ts CHANGED
@@ -28,7 +28,7 @@ export interface Capabilities {
28
28
  restart_required_keys: string[];
29
29
  }
30
30
 
31
- export interface TargetAdapterOptions {
31
+ export interface ContextAdapterOptions {
32
32
  configPath?: string;
33
33
  allowInsecure?: boolean;
34
34
  clients?: Clients;
@@ -46,10 +46,10 @@ export interface AuditPruneResult extends PruneResult {
46
46
  }
47
47
 
48
48
  /**
49
- * Target adapter: `local` = this CLI process has the Skillmux runtime (vault, index,
49
+ * Context adapter: `local` = this CLI process has the Skillmux runtime (vault, index,
50
50
  * audit db, embeddings/reranker clients) in-process; `remote` = thin network client to an external process.
51
51
  */
52
- export interface TargetAdapter {
52
+ export interface ContextAdapter {
53
53
  getCapabilities(): Promise<Capabilities>;
54
54
  getConfigShow(): Promise<{ effective: Config; sources: Record<string, string>; active_revision: string }>;
55
55
  getConfigGet(key: string): Promise<unknown>;
@@ -74,11 +74,11 @@ export function isLoopbackHost(hostname: string): boolean {
74
74
  );
75
75
  }
76
76
 
77
- export class LocalAdapter implements TargetAdapter {
77
+ export class LocalAdapter implements ContextAdapter {
78
78
  private configPath: string;
79
79
  private clients?: Clients;
80
80
 
81
- constructor(opts?: TargetAdapterOptions) {
81
+ constructor(opts?: ContextAdapterOptions) {
82
82
  this.configPath = resolveConfigPath(opts?.configPath);
83
83
  this.clients = opts?.clients;
84
84
  }
@@ -132,7 +132,7 @@ export class LocalAdapter implements TargetAdapter {
132
132
  return setDottedKey(key, rawValStr, {
133
133
  configPath: this.configPath,
134
134
  dryRun: opts?.dryRun,
135
- targetName: "local",
135
+ contextName: "local",
136
136
  });
137
137
  }
138
138
 
@@ -210,12 +210,12 @@ export class LocalAdapter implements TargetAdapter {
210
210
  }
211
211
  }
212
212
 
213
- export class RemoteAdapter implements TargetAdapter {
213
+ export class RemoteAdapter implements ContextAdapter {
214
214
  private serverUrl: string;
215
215
  private tokenEnv?: string;
216
216
  private allowInsecure: boolean;
217
217
 
218
- constructor(target: { server: string; token_env?: string }, opts?: TargetAdapterOptions) {
218
+ constructor(target: { server: string; token_env?: string }, opts?: ContextAdapterOptions) {
219
219
  this.serverUrl = target.server.replace(/\/$/, "");
220
220
  this.tokenEnv = target.token_env;
221
221
  this.allowInsecure = opts?.allowInsecure ?? false;
@@ -429,10 +429,10 @@ export class RemoteAdapter implements TargetAdapter {
429
429
  }
430
430
  }
431
431
 
432
- export function createTargetAdapter(target: ResolvedContext, opts?: TargetAdapterOptions): TargetAdapter {
433
- if (target.type === "local") {
432
+ export function createContextAdapter(context: ResolvedContext, opts?: ContextAdapterOptions): ContextAdapter {
433
+ if (context.type === "local") {
434
434
  return new LocalAdapter(opts);
435
435
  } else {
436
- 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);
437
437
  }
438
438
  }
package/src/cli.ts CHANGED
@@ -17,7 +17,7 @@ import { type StatsResponse } from "./stats";
17
17
  import { scanVault } from "./vault";
18
18
 
19
19
  import { resolveContext, type ResolvedContext } from "./context";
20
- import { createTargetAdapter, isLoopbackHost, type TargetAdapter } from "./adapters";
20
+ import { createContextAdapter, isLoopbackHost, type ContextAdapter } from "./adapters";
21
21
  import {
22
22
  emitSuccess,
23
23
  CliError,
@@ -28,6 +28,7 @@ import {
28
28
  warn,
29
29
  } from "./output";
30
30
  import { generateCompletions, type ShellType } from "./completions";
31
+ import { SUPPORTED_AGENT_IDS } from "./init-agents";
31
32
  import { runAudit } from "./commands/audit";
32
33
  import { handleConfigCommand } from "./commands/config";
33
34
  import { handleContextCommand } from "./commands/context";
@@ -45,7 +46,7 @@ import { runTarget } from "./commands/target";
45
46
  import { runSync } from "./commands/sync";
46
47
  import { runInit } from "./commands/init";
47
48
 
48
- const KNOWN_COMMANDS = [
49
+ export const KNOWN_COMMANDS = [
49
50
  "context",
50
51
  "config",
51
52
  "completions",
@@ -69,39 +70,120 @@ const KNOWN_COMMANDS = [
69
70
  "local-vault",
70
71
  ];
71
72
 
72
- const LOCAL_ONLY_COMMANDS = new Set([
73
- "install",
74
- "update",
75
- "outdated",
76
- "sync",
77
- "core",
78
- "project",
79
- "target",
80
- "local-vault",
81
- "index",
82
- "models",
83
- "scan",
84
- "init",
85
- "serve",
86
- ]);
73
+ /**
74
+ * Declared context support for every command in KNOWN_COMMANDS — the single
75
+ * source of truth getLocalOnlyCommand() enforces against. A command missing
76
+ * from here, or misclassified, is a bug: see tests/cli-context-support.test.ts,
77
+ * which fails the build rather than letting a command silently drift out of
78
+ * sync the way `config init` did (it was never rejected for a remote context,
79
+ * nor actually remote-capable — it just silently ran local logic that choked
80
+ * on an unrecognized --context/--server flag with a confusing error).
81
+ *
82
+ * - "local-only": operates on this machine's vault/filesystem/agents only;
83
+ * a remote context is rejected outright.
84
+ * - "remote-capable": routed through ContextAdapter — same command, backed by
85
+ * LocalAdapter or RemoteAdapter depending on the resolved context.
86
+ * - "context-agnostic": the resolved context isn't used to decide behavior at
87
+ * all (context management is inherently local; completions never touch
88
+ * vault/server state).
89
+ *
90
+ * Subcommand-level exceptions within an otherwise-classified command (e.g.
91
+ * `config init`, which bootstraps *this machine's* config file and so is
92
+ * local-only despite `config` overall being remote-capable) are handled in
93
+ * getLocalOnlyCommand() itself, not in this top-level map.
94
+ */
95
+ export type CommandContextSupport = "local-only" | "remote-capable" | "context-agnostic";
96
+
97
+ export const COMMAND_CONTEXT_SUPPORT: Record<string, CommandContextSupport> = {
98
+ context: "context-agnostic",
99
+ config: "remote-capable",
100
+ completions: "context-agnostic",
101
+ serve: "local-only",
102
+ index: "local-only",
103
+ sync: "local-only",
104
+ init: "local-only",
105
+ project: "local-only",
106
+ target: "local-only",
107
+ core: "local-only",
108
+ report: "remote-capable",
109
+ audit: "remote-capable",
110
+ scan: "local-only",
111
+ install: "local-only",
112
+ outdated: "local-only",
113
+ update: "local-only",
114
+ eval: "remote-capable",
115
+ doctor: "remote-capable",
116
+ models: "local-only",
117
+ skill: "local-only",
118
+ "local-vault": "local-only",
119
+ };
87
120
 
88
- function getLocalOnlyCommand(command: string, subCommand: string): string | null {
89
- if (LOCAL_ONLY_COMMANDS.has(command)) {
90
- return command;
91
- }
121
+ const LOCAL_ONLY_COMMANDS = new Set(
122
+ Object.entries(COMMAND_CONTEXT_SUPPORT)
123
+ .filter(([, support]) => support === "local-only")
124
+ .map(([command]) => command),
125
+ );
126
+
127
+ export function getLocalOnlyCommand(command: string, subCommand: string): string | null {
92
128
  if (command === "skill" && (subCommand === "which" || !subCommand)) {
93
129
  return "skill which";
94
130
  }
131
+ if (command === "config" && subCommand === "init") {
132
+ return "config init";
133
+ }
134
+ if (LOCAL_ONLY_COMMANDS.has(command)) {
135
+ return command;
136
+ }
95
137
  return null;
96
138
  }
97
139
 
140
+ /**
141
+ * Why a local-only command can't take a remote context, keyed by the exact
142
+ * string getLocalOnlyCommand() returns. Drives the guidance sentence
143
+ * remoteContextUnsupported() appends, so the rejection points somewhere
144
+ * useful instead of just saying no.
145
+ */
146
+ type LocalOnlyReason = "vault-content" | "native-delivery" | "local-runtime" | "local-config";
147
+
148
+ const LOCAL_ONLY_REASON: Record<string, LocalOnlyReason> = {
149
+ install: "vault-content",
150
+ update: "vault-content",
151
+ outdated: "vault-content",
152
+ scan: "vault-content",
153
+ init: "native-delivery",
154
+ sync: "native-delivery",
155
+ target: "native-delivery",
156
+ core: "native-delivery",
157
+ project: "native-delivery",
158
+ "local-vault": "native-delivery",
159
+ "skill which": "native-delivery",
160
+ serve: "local-runtime",
161
+ models: "local-runtime",
162
+ index: "local-runtime",
163
+ "config init": "local-config",
164
+ };
165
+
166
+ const LOCAL_ONLY_GUIDANCE: Record<LocalOnlyReason, string> = {
167
+ "vault-content":
168
+ "To change a remote deployment's vault contents, update its git-backed source and redeploy or pull on that host — skillmux doesn't replicate vault checkouts over the network.",
169
+ "native-delivery":
170
+ "This manages skill delivery into agent directories on the machine you run it from; there's no remote equivalent — run it on the machine that owns those directories.",
171
+ "local-runtime":
172
+ "This operates on the local runtime process on the machine you run it from.",
173
+ "local-config":
174
+ "This bootstraps this machine's own config file. To inspect or change a remote deployment's configuration, use \"skillmux config show/set --context <name>\" instead.",
175
+ };
176
+
98
177
  function remoteContextUnsupported(rejectedCommand: string): CliError {
178
+ const reason = LOCAL_ONLY_REASON[rejectedCommand];
179
+ const guidance = reason ? ` ${LOCAL_ONLY_GUIDANCE[reason]}` : "";
99
180
  return new CliError(
100
- `\`${rejectedCommand}\` operates on the local vault only; --context/--server isn't supported here`,
181
+ `\`${rejectedCommand}\` operates on the local vault only; --context/--server isn't supported here.${guidance}`,
101
182
  2,
102
183
  "REMOTE_CONTEXT_UNSUPPORTED",
103
184
  {
104
185
  rejected_command: rejectedCommand,
186
+ ...(reason ? { reason } : {}),
105
187
  },
106
188
  );
107
189
  }
@@ -196,14 +278,14 @@ async function main() {
196
278
  else if (arg === "--server") flagServer = rawArgv[++i];
197
279
  }
198
280
 
199
- let resolvedTarget: ResolvedContext = { type: "local", name: "local" };
281
+ let resolvedContext: ResolvedContext = { type: "local", name: "local" };
200
282
 
201
283
  if (
202
284
  process.env.RUNNING_IN_DOCKER === "true" &&
203
285
  isDockerHostManagementCommand(command, subCommand)
204
286
  ) {
205
287
  await handleError(containerCommandUnsupported(command, subCommand), {
206
- target: resolvedTarget,
288
+ context: resolvedContext,
207
289
  isJson,
208
290
  isVerbose,
209
291
  });
@@ -218,38 +300,38 @@ async function main() {
218
300
  }
219
301
 
220
302
  try {
221
- resolvedTarget = await resolveContext({
303
+ resolvedContext = await resolveContext({
222
304
  context: flagContext,
223
305
  server: flagServer,
224
306
  });
225
307
  } catch (err: any) {
226
- await handleError(err, { target: resolvedTarget, isJson, isVerbose });
308
+ await handleError(err, { context: resolvedContext, isJson, isVerbose });
227
309
  return;
228
310
  }
229
311
 
230
312
  const localOnlyCommand = getLocalOnlyCommand(command, subCommand);
231
- if (localOnlyCommand && resolvedTarget.type === "remote") {
313
+ if (localOnlyCommand && resolvedContext.type === "remote") {
232
314
  await handleError(remoteContextUnsupported(localOnlyCommand), {
233
- target: resolvedTarget,
315
+ context: resolvedContext,
234
316
  isJson,
235
317
  isVerbose,
236
318
  });
237
319
  return;
238
320
  }
239
321
 
240
- const adapter = createTargetAdapter(resolvedTarget, { allowInsecure });
322
+ const adapter = createContextAdapter(resolvedContext, { allowInsecure });
241
323
 
242
324
  try {
243
325
  switch (command) {
244
326
  case "context":
245
327
  await handleContextCommand(subCommand, commandArgs, {
246
- target: resolvedTarget,
328
+ context: resolvedContext,
247
329
  isJson,
248
330
  });
249
331
  break;
250
332
  case "config":
251
333
  await handleConfigCommand(adapter, subCommand, commandArgs, {
252
- target: resolvedTarget,
334
+ context: resolvedContext,
253
335
  isJson,
254
336
  dryRun: isDryRun,
255
337
  });
@@ -308,7 +390,7 @@ async function main() {
308
390
  case "report":
309
391
  await runReport(rawArgv.slice(1), {
310
392
  isJson,
311
- target: resolvedTarget,
393
+ context: resolvedContext,
312
394
  allowInsecure,
313
395
  adapter,
314
396
  });
@@ -317,7 +399,7 @@ async function main() {
317
399
  await runAudit(subCommand, commandArgs, {
318
400
  isJson,
319
401
  dryRun: isDryRun,
320
- target: resolvedTarget,
402
+ context: resolvedContext,
321
403
  adapter,
322
404
  });
323
405
  break;
@@ -339,11 +421,16 @@ async function main() {
339
421
  } else if (subCommand === "") {
340
422
  await runEval({ isJson, adapter });
341
423
  } else {
342
- throw new Error(`usage: skillmux eval [promote --since <window> [--target <path>] [--dry-run] [--yes] [--json]]`);
424
+ throw new Error(`usage: skillmux eval [promote --since <window> [--out <path>] [--dry-run] [--yes] [--json]]`);
343
425
  }
344
426
  break;
345
427
  case "doctor":
346
- await runDoctor({ isJson, target: resolvedTarget, adapter });
428
+ await runDoctor({
429
+ isJson,
430
+ context: resolvedContext,
431
+ adapter,
432
+ args: rawArgv.slice(1),
433
+ });
347
434
  break;
348
435
  case "which":
349
436
  throw new Error(
@@ -375,7 +462,7 @@ async function main() {
375
462
  }
376
463
  }
377
464
  } catch (err: any) {
378
- await handleError(err, { target: resolvedTarget, isJson, isVerbose });
465
+ await handleError(err, { context: resolvedContext, isJson, isVerbose });
379
466
  }
380
467
  }
381
468
 
@@ -390,7 +477,7 @@ async function handleCompletionsCommand(shell: string) {
390
477
 
391
478
  async function handleError(
392
479
  err: any,
393
- opts: { target: ResolvedContext; isJson: boolean; isVerbose: boolean },
480
+ opts: { context: ResolvedContext; isJson: boolean; isVerbose: boolean },
394
481
  ) {
395
482
  const code = mapExitCode(err);
396
483
  process.exitCode = code;
@@ -411,7 +498,7 @@ async function handleError(
411
498
  if (opts.isJson) {
412
499
  const env = formatJsonEnvelope({
413
500
  ok: false,
414
- target: opts.target,
501
+ context: opts.context,
415
502
  error: {
416
503
  code: err instanceof CliError ? err.code : `EXIT_${code}`,
417
504
  message: msg,
@@ -456,6 +543,11 @@ usage:
456
543
  skillmux config diff
457
544
  skillmux config status
458
545
 
546
+ config init bootstraps this machine's config file from a populated vault. It
547
+ is not a prerequisite for anything: "skillmux init --vault <path>" runs the
548
+ same bootstrap when no config exists, so reach for config init only when you
549
+ are setting up the config without the guided init.
550
+
459
551
  Accepts --context <name> / --server <url> to target a remote deployment.`,
460
552
 
461
553
  completions: `completions: generate a shell completion script
@@ -476,45 +568,97 @@ alongside a stdio transport without opening the full HTTP surface.`,
476
568
  usage:
477
569
  skillmux index`,
478
570
 
479
- sync: `sync: apply the manifest to native client target directories
571
+ sync: `sync: apply the manifest to native agent target directories
480
572
 
481
573
  usage:
482
- skillmux sync [--dry-run] [--restore-monolith] [--install-hook] [--yes] [--json]`,
574
+ skillmux sync [--dry-run] [--restore-monolith] [--install-hook] [--yes] [--json]
575
+
576
+ --dry-run prints what would change without writing. --yes approves creating
577
+ a target directory that does not exist yet; without it, an unseen directory
578
+ is skipped rather than created.
579
+
580
+ --install-hook installs a git post-merge hook in the vault checkout so a
581
+ "git pull" re-syncs the targets automatically.
582
+
583
+ --restore-monolith undoes managed-pin delivery for a target: instead of
584
+ individual pinned skills, the target directory is replaced by a single
585
+ symlink to the whole vault. It refuses to touch a directory skillmux does
586
+ not own, one carrying a local_vault marker, or one whose marker points at a
587
+ different vault.`,
483
588
 
484
589
  init: `init: guided setup for native skill management
485
590
 
486
591
  usage:
487
- skillmux init [--client <name>...] [--target <name>...] [--dir <dir>]
488
- [--vault <path>] [--core <skill_id>...]
489
- [--migrate-full-vault] [--no-instructions] [--no-sync]
592
+ skillmux init [--agent <name>...] [--vault <path>] [--core <skill_id>...]
593
+ [--migrate-full-vault] [--show-mcp-setup] [--register-mcp]
594
+ [--no-instructions] [--no-sync]
490
595
  [--interactive|--yes|--dry-run] [--json]
491
596
 
492
- clients: claude-code, codex, gemini-cli, opencode, github-copilot, windsurf,
493
- antigravity, goose, hermes, skillmux-mcp
494
- targets: agent-skills, claude-code, codex, custom`,
597
+ agents: ${SUPPORTED_AGENT_IDS.join(", ")}
598
+
599
+ Native pins and MCP are independent — skip both of the flags below for
600
+ native-only setup, and init writes no instruction files (the managed
601
+ block only teaches resolve_skill/fetch_skill, which are MCP tools).
602
+ --show-mcp-setup prints the MCP registration snippet to copy in yourself,
603
+ for any agent, and also writes the instruction block for every selected
604
+ agent. --register-mcp instead runs that agent's own CLI to register
605
+ skillmux automatically, but only for claude-code and codex (the only
606
+ agents with a verified registration command), and writes the instruction
607
+ block just for those; interactively, init asks about this only when
608
+ you've selected one of those two. --no-instructions forces instruction
609
+ writes off even when an MCP flag is set. A tool not in the agents list
610
+ above isn't supported by init yet — add it to SUPPORTED_AGENT_IDS rather
611
+ than guessing a directory. To adopt an arbitrary existing directory
612
+ directly, use "skillmux target add <name> --dir <dir>" instead of init.`,
495
613
 
496
614
  project: `project: manage project-scoped skill pins and sync groups
497
615
 
498
616
  usage:
499
617
  skillmux project init [path] [--name <group>] [--skill <skill_id>...]
500
- [--client <name>...] [--target <name>...] [--no-sync]
501
- [--interactive|--yes|--dry-run] [--json]
618
+ [--agent <name>...] [--target <name>...] [--register-mcp]
619
+ [--no-sync] [--interactive|--yes|--dry-run] [--json]
502
620
  skillmux project list
503
621
  skillmux project show <group>
504
622
  skillmux project add-path <group> [path] --yes
505
623
  skillmux project remove-path <group> [path] --yes
506
624
  skillmux project pin <group> <skill_id>... --yes
507
625
  skillmux project unpin <group> <skill_id>... --yes
508
- skillmux project attach <group> (--client <id>... | --target <name>...) --yes
509
- skillmux project detach <group> (--client <id>... | --target <name>...) --yes`,
626
+ skillmux project attach <group> (--agent <id>... | --target <name>...) --yes
627
+ skillmux project detach <group> (--agent <id>... | --target <name>...) --yes
628
+
629
+ --agent and --target both name sync targets, and either may be repeated.
630
+ --target <name> names a target directly, including a custom one created by
631
+ "skillmux target add". --agent <id> is shorthand for "whatever target that
632
+ agent maps to", resolved from the targets init already configured, so it
633
+ fails if that agent was never set up or maps to no target at all (goose and
634
+ hermes use full-vault delivery and have none). Several agents can share one
635
+ target, so attaching two agents that map to the same directory attaches it
636
+ once.
637
+
638
+ --register-mcp is the project-local counterpart to "skillmux init
639
+ --register-mcp": only for claude-code (the only agent whose own CLI has a
640
+ project MCP scope — codex's mcp add has no scope flag, so it's always
641
+ global). It runs "claude mcp add -s project" for this project directory,
642
+ which writes a committed .mcp.json shared with your team, and writes a
643
+ project-root CLAUDE.md with the resolve_skill/fetch_skill discovery
644
+ paragraph — same reasoning as init: no instruction file is written unless
645
+ MCP is actually being registered.`,
510
646
 
511
647
  target: `target: manage native sync target directories
512
648
 
513
649
  usage:
514
650
  skillmux target list
515
651
  skillmux target show <name>
516
- skillmux target add <name> --dir <dir> --yes
517
- skillmux target remove <name> --yes`,
652
+ skillmux target add <name> [--dir <dir>] --yes
653
+ skillmux target remove <name> --yes
654
+
655
+ --dir may be omitted when <name> is a built-in target with a deterministic
656
+ path: agent-skills, claude-code, codex. Any other <name> requires --dir.
657
+
658
+ A target is a directory, not a product. Several agents can map to the same
659
+ one (opencode, github-copilot and windsurf all share agent-skills), which is
660
+ why "skillmux project attach" accepts --agent as shorthand for the target
661
+ that agent resolves to.`,
518
662
 
519
663
  core: `core: pin or unpin core-tier skills
520
664
 
@@ -537,28 +681,66 @@ Accepts --context <name> / --server <url> to prune a remote deployment's audit d
537
681
  scan: `scan: check the vault for install-time or integrity issues
538
682
 
539
683
  usage:
540
- skillmux scan [path] [--format text|json] [--fail-on low|medium|high] [--json]`,
684
+ skillmux scan [path] [--fail-on low|medium|high|none] [--json]
685
+
686
+ Scans [path], or the configured vault when omitted. Reporting only: it
687
+ exits 0 whatever it finds unless --fail-on names a severity, which is why
688
+ it has no default threshold while install and update default to high.
689
+
690
+ --format text|json is deprecated: it emits JSON outside the standard
691
+ envelope. Use --json instead; --format will be removed in a future 1.x
692
+ release.`,
541
693
 
542
694
  install: `install: install a skill from a git source
543
695
 
544
696
  usage:
545
- skillmux install <repo>[/path] [--force] [--fail-on low|medium|high] [--dry-run] [--allow-local-source] [--json]`,
697
+ skillmux install <repo>[/path] [--yes] [--force] [--fail-on low|medium|high|none] [--dry-run] [--allow-local-source] [--json]
698
+
699
+ --yes approves writing the skill into the vault. Without it an interactive
700
+ run asks first, and a non-interactive one (no TTY, or --json) fails rather
701
+ than installing unattended, matching "skillmux update".
702
+
703
+ The fetched skill is scanned before it is written to the vault. --fail-on
704
+ sets the severity that aborts the install and defaults to high; pass
705
+ --fail-on none to install despite findings. A lower threshold is stricter:
706
+ low aborts on low, medium and high.
707
+
708
+ --force overwrites a skill that already exists in the vault instead of
709
+ refusing. --dry-run reports where the skill would land without writing.
710
+ --allow-local-source permits a file:// or local path source, which is
711
+ otherwise rejected.`,
546
712
 
547
713
  outdated: `outdated: list installed skills with a newer upstream version
548
714
 
549
715
  usage:
550
- skillmux outdated [--allow-local-source] [--json]`,
716
+ skillmux outdated [--allow-local-source] [--json]
717
+
718
+ Read-only: it reports what "skillmux update" would change and writes
719
+ nothing. --allow-local-source includes skills installed from a local or
720
+ file:// source, which are skipped by default because their upstream is a
721
+ path on this machine rather than a shared remote.`,
551
722
 
552
723
  update: `update: update one or all skills to their latest source version
553
724
 
554
725
  usage:
555
- skillmux update [skill-id] [--yes] [--dry-run] [--force] [--allow-local-source] [--fail-on low|medium|high] [--json]`,
726
+ skillmux update [skill-id] [--yes] [--dry-run] [--force] [--allow-local-source] [--fail-on low|medium|high|none] [--json]
727
+
728
+ Updates every installed skill, or just <skill-id>. --yes is required to
729
+ apply non-interactively. --dry-run prints the plan without writing.
730
+
731
+ --fail-on works exactly as it does for install and defaults to high, so a
732
+ skill whose new version carries a high-severity finding is skipped rather
733
+ than updated; --fail-on none restores the old permissive behavior.
734
+
735
+ --force updates a skill whose local content no longer matches the hash
736
+ recorded at install time, which otherwise blocks the update to avoid
737
+ discarding local edits. --allow-local-source permits local/file:// sources.`,
556
738
 
557
739
  eval: `eval: run retrieval evaluation against the holdout set
558
740
 
559
741
  usage:
560
742
  skillmux eval [--json]
561
- skillmux eval promote --since <window> [--target <path>] [--dry-run] [--yes] [--json]
743
+ skillmux eval promote --since <window> [--out <path>] [--dry-run] [--yes] [--json]
562
744
 
563
745
  Accepts --context <name> / --server <url> to evaluate a remote deployment.`,
564
746
 
@@ -613,32 +795,33 @@ See docs/deployment.md for server deployment examples.`);
613
795
  console.log(`usage: skillmux <command> [options]
614
796
 
615
797
  Setup:
616
- skillmux config init --vault <path> --yes
617
- skillmux init [--client <name>...] [--target <name>...] [--dir <dir>]
618
- [--vault <path>] [--core <skill_id>...]
619
- [--migrate-full-vault] [--no-instructions] [--no-sync]
798
+ skillmux init [--agent <name>...] [--vault <path>] [--core <skill_id>...]
799
+ [--migrate-full-vault] [--show-mcp-setup] [--register-mcp]
800
+ [--no-instructions] [--no-sync]
620
801
  [--interactive|--yes|--dry-run] [--json]
621
802
  skillmux project init [path] [--name <group>] [--skill <skill_id>...]
622
- [--client <name>...] [--target <name>...] [--no-sync]
803
+ [--agent <name>...] [--target <name>...] [--no-sync]
623
804
  [--interactive|--yes|--dry-run] [--json]
624
805
  skillmux project <list|show|add-path|remove-path|pin|unpin|attach|detach>
625
- skillmux target <list|show|add|remove>
806
+ skillmux target <list|show|add|remove> (a target is a directory sync writes into)
626
807
  skillmux core <pin|unpin> <skill_id>... [--yes] [--dry-run] [--json]
627
808
  skillmux skill which <skill_id> (local vault shadow resolution; unrelated to MCP routing)
809
+ skillmux config init --vault <path> --yes
810
+ (bootstraps this machine's config on its own; not a
811
+ prerequisite, since "skillmux init --vault" does the same)
628
812
 
629
- Init clients:
630
- claude-code, codex, gemini-cli, opencode, github-copilot, windsurf,
631
- antigravity, goose, hermes, skillmux-mcp
632
-
633
- Init targets:
634
- agent-skills, claude-code, codex, custom
813
+ Init agents:
814
+ ${SUPPORTED_AGENT_IDS.join(", ")}
815
+ ("skillmux init --show-mcp-setup" also prints the MCP registration
816
+ snippet, independent of which agents you select. A tool not in this
817
+ list isn't supported by init yet — see "skillmux init --help".)
635
818
 
636
819
  Operations:
637
820
  skillmux report [--context <name> | --server <url> | --db <path>] --since <window> [--json]
638
821
  skillmux audit prune [--older-than <window>] [--dry-run] [--yes] [--json]
639
- skillmux eval promote --since <window> [--target <path>] [--dry-run] [--yes] [--json]
822
+ skillmux eval promote --since <window> [--out <path>] [--dry-run] [--yes] [--json]
640
823
  skillmux outdated [--allow-local-source] [--json]
641
- skillmux update [skill-id] [--yes] [--dry-run] [--force] [--allow-local-source] [--fail-on low|medium|high] [--json]
824
+ skillmux update [skill-id] [--yes] [--dry-run] [--force] [--allow-local-source] [--fail-on low|medium|high|none] [--json]
642
825
 
643
826
  Commands:
644
827
  serve, index, sync, init, project, target, core, report, audit, scan, install, outdated, update,
@@ -713,7 +896,7 @@ async function runIndex(): Promise<void> {
713
896
  }
714
897
  }
715
898
 
716
- async function runEval(options: { isJson: boolean; adapter: TargetAdapter }): Promise<void> {
899
+ async function runEval(options: { isJson: boolean; adapter: ContextAdapter }): Promise<void> {
717
900
  const config = await loadConfig();
718
901
  configure({ config, clients: createClients(config) });
719
902
 
@@ -1,13 +1,13 @@
1
1
  import { emitSuccess } from "../output";
2
2
  import { confirmIfNeeded } from "./shared";
3
- import type { TargetAdapter } from "../adapters";
3
+ import type { ContextAdapter } from "../adapters";
4
4
  import type { ResolvedContext } from "../context";
5
5
  import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
6
6
 
7
7
  export async function runAudit(
8
8
  subCommand: string,
9
9
  args: string[],
10
- options: { isJson: boolean; dryRun: boolean; target: ResolvedContext; adapter: TargetAdapter },
10
+ options: { isJson: boolean; dryRun: boolean; context: ResolvedContext; adapter: ContextAdapter },
11
11
  ): Promise<void> {
12
12
  if (subCommand !== "prune") {
13
13
  throw new Error("usage: skillmux audit prune [--older-than <window>] [--dry-run] [--yes] [--json]");