@klhapp/skillmux 1.10.0 → 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.
@@ -1,7 +1,11 @@
1
1
  import { existsSync, lstatSync } from "node:fs";
2
2
  import { basename } from "node:path";
3
3
  import { expandHome } from "../config";
4
- import { planClientSurfaces, SUPPORTED_CLIENT_IDS } from "../init-clients";
4
+ import { planAgentSurfaces, SUPPORTED_AGENT_IDS, type AgentId } from "../init-agents";
5
+ import {
6
+ applyInstructionPlan,
7
+ planProjectInstructionSetup,
8
+ } from "../init-instructions";
5
9
  import {
6
10
  parseManifest,
7
11
  pinProject,
@@ -12,6 +16,11 @@ import {
12
16
  validateManifest,
13
17
  writeManifestAtomic,
14
18
  } from "../manifest";
19
+ import {
20
+ MCP_PROJECT_REGISTRABLE_AGENTS,
21
+ registerMcpServer,
22
+ type McpRegistrationResult,
23
+ } from "../mcp-registration";
15
24
  import { resolveProjectDirectory, suggestProjectName } from "../project-setup";
16
25
  import {
17
26
  parseCommaList,
@@ -19,18 +28,19 @@ import {
19
28
  promptText,
20
29
  shouldUseWizard,
21
30
  } from "../prompts";
22
- import { emitSuccess, isInteractive } from "../output";
31
+ import { emitSuccess, isInteractive, unknownSubcommandError } from "../output";
23
32
  import { confirmAction, confirmIfNeeded, loadManifestContext } from "./shared";
24
33
  import { isGlobalFlag } from "../global-flags";
25
34
  const PROJECT_INIT_USAGE =
26
- "usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--client <id>...] [--target <name>...] [--yes] [--no-sync]";
35
+ "usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--agent <id>...] [--target <name>...] [--register-mcp] [--yes] [--no-sync]";
27
36
 
28
37
  interface ProjectInitArgs {
29
38
  path: string;
30
39
  name: string;
31
40
  skills: string[];
32
- clients: string[];
41
+ agents: string[];
33
42
  targets: string[];
43
+ registerMcp: boolean;
34
44
  yes: boolean;
35
45
  sync: boolean;
36
46
  }
@@ -45,16 +55,16 @@ export function configuredTargetForSurface(
45
55
  )?.[0];
46
56
  }
47
57
 
48
- function configuredTargetsForClients(
58
+ function configuredTargetsForAgents(
49
59
  manifest: ReturnType<typeof parseManifest>,
50
- clients: readonly string[],
60
+ agents: readonly string[],
51
61
  ): string[] {
52
- return planClientSurfaces(clients).surfaces.map((surface) => {
62
+ return planAgentSurfaces(agents).surfaces.map((surface) => {
53
63
  const target = configuredTargetForSurface(manifest, surface);
54
64
  if (target) return target;
55
- const client = surface.clients[0]!;
65
+ const agent = surface.agents[0]!;
56
66
  throw new Error(
57
- `client target for "${client}" is not configured; run "skillmux init --client ${client} --yes" first`,
67
+ `agent target for "${agent}" is not configured; run "skillmux init --agent ${agent} --yes" first`,
58
68
  );
59
69
  });
60
70
  }
@@ -63,8 +73,9 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
63
73
  let projectPath: string | undefined;
64
74
  let name: string | undefined;
65
75
  const skills: string[] = [];
66
- const clients: string[] = [];
76
+ const agents: string[] = [];
67
77
  const targets: string[] = [];
78
+ let registerMcp = false;
68
79
  let yes = false;
69
80
  let sync = true;
70
81
 
@@ -81,10 +92,12 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
81
92
  const target = args[++i];
82
93
  if (!target) throw new Error("--target requires a name");
83
94
  targets.push(target);
84
- } else if (arg === "--client") {
85
- const client = args[++i];
86
- if (!client) throw new Error("--client requires a name");
87
- clients.push(client);
95
+ } else if (arg === "--agent") {
96
+ const agent = args[++i];
97
+ if (!agent) throw new Error("--agent requires a name");
98
+ agents.push(agent);
99
+ } else if (arg === "--register-mcp") {
100
+ registerMcp = true;
88
101
  } else if (arg === "--yes") {
89
102
  yes = true;
90
103
  } else if (arg === "--no-sync") {
@@ -110,8 +123,9 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
110
123
  path,
111
124
  name: name ?? suggestProjectName(basename(path)),
112
125
  skills,
113
- clients,
126
+ agents,
114
127
  targets,
128
+ registerMcp,
115
129
  yes,
116
130
  sync,
117
131
  };
@@ -263,15 +277,15 @@ export async function runProject(
263
277
  const group = args[0];
264
278
  if (!group)
265
279
  throw new Error(
266
- `usage: skillmux project ${subCommand} <group> (--client <id>... | --target <name>...) --yes`,
280
+ `usage: skillmux project ${subCommand} <group> (--agent <id>... | --target <name>...) --yes`,
267
281
  );
268
- const clients: string[] = [];
282
+ const agents: string[] = [];
269
283
  const requestedTargets: string[] = [];
270
284
  for (let i = 1; i < args.length; i++) {
271
- if (args[i] === "--client") {
285
+ if (args[i] === "--agent") {
272
286
  const value = args[++i];
273
- if (!value) throw new Error("--client requires a name");
274
- clients.push(value);
287
+ if (!value) throw new Error("--agent requires a name");
288
+ agents.push(value);
275
289
  } else if (args[i] === "--target") {
276
290
  const value = args[++i];
277
291
  if (!value) throw new Error("--target requires a name");
@@ -286,11 +300,20 @@ export async function runProject(
286
300
  }
287
301
  const { config, vaultPath, manifestPath, manifest } =
288
302
  await loadManifestContext();
289
- const clientTargets = configuredTargetsForClients(manifest, clients);
290
- const targets = [...new Set([...requestedTargets, ...clientTargets])];
303
+ const agentTargets = configuredTargetsForAgents(manifest, agents);
304
+ const targets = [...new Set([...requestedTargets, ...agentTargets])];
291
305
  if (targets.length === 0) {
292
- throw new Error(`project ${subCommand} requires --client or --target`);
306
+ throw new Error(`project ${subCommand} requires --agent or --target`);
293
307
  }
308
+ // Several agents can share one target (e.g. opencode/windsurf both use
309
+ // agent-skills) — show the resolved directory, not just the target name,
310
+ // so it's clear at confirmation time which physical folder this affects.
311
+ const targetDirs = Object.fromEntries(
312
+ targets.map((t) => [t, manifest.targets[t]?.dir ?? "(unknown)"]),
313
+ );
314
+ const targetsDisplay = targets
315
+ .map((t) => `${t} (${targetDirs[t]})`)
316
+ .join(", ");
294
317
  const updated = updateProjectTargets(manifest, group, {
295
318
  ...(subCommand === "attach" ? { attach: targets } : { detach: targets }),
296
319
  });
@@ -302,10 +325,10 @@ export async function runProject(
302
325
  if (options.dryRun) {
303
326
  emitSuccess(
304
327
  { isJson: options.isJson },
305
- { subcommand: subCommand, group, targets },
328
+ { subcommand: subCommand, group, targets, target_dirs: targetDirs },
306
329
  () =>
307
330
  console.log(
308
- `${subCommand}: [project.${group}] ${targets.join(", ")} (dry-run)`,
331
+ `${subCommand}: [project.${group}] ${targetsDisplay} (dry-run)`,
309
332
  ),
310
333
  );
311
334
  return;
@@ -314,7 +337,7 @@ export async function runProject(
314
337
  !(await confirmIfNeeded({
315
338
  confirmed: args.includes("--yes"),
316
339
  isJson: options.isJson,
317
- prompt: `${subCommand} [project.${group}] to ${targets.join(", ")}?`,
340
+ prompt: `${subCommand} [project.${group}] to ${targetsDisplay}?`,
318
341
  nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
319
342
  }))
320
343
  )
@@ -322,12 +345,23 @@ export async function runProject(
322
345
  writeManifestAtomic(manifestPath, updated);
323
346
  emitSuccess(
324
347
  { isJson: options.isJson },
325
- { subcommand: subCommand, group, targets },
326
- () => console.log(`${subCommand}: [project.${group}] ${targets.join(", ")}`),
348
+ { subcommand: subCommand, group, targets, target_dirs: targetDirs },
349
+ () => console.log(`${subCommand}: [project.${group}] ${targetsDisplay}`),
327
350
  );
328
351
  return;
329
352
  }
330
- if (subCommand !== "init") throw new Error(PROJECT_INIT_USAGE);
353
+ if (subCommand !== "init")
354
+ throw unknownSubcommandError("project", subCommand, [
355
+ "init",
356
+ "list",
357
+ "show",
358
+ "add-path",
359
+ "remove-path",
360
+ "pin",
361
+ "unpin",
362
+ "attach",
363
+ "detach",
364
+ ]);
331
365
  let request = parseProjectInitArgs(args);
332
366
  const guided = shouldUseWizard(args, {
333
367
  interactive: isInteractive(),
@@ -345,20 +379,20 @@ export async function runProject(
345
379
  const localVaultPaths = config.local_vault_paths.map(expandHome);
346
380
  if (guided) {
347
381
  const name = await promptText("Project group", request.name);
348
- const availableClients = SUPPORTED_CLIENT_IDS.filter((client) => {
349
- const surface = planClientSurfaces([client]).surfaces[0];
382
+ const availableAgents = SUPPORTED_AGENT_IDS.filter((agent) => {
383
+ const surface = planAgentSurfaces([agent]).surfaces[0];
350
384
  return (
351
385
  surface !== undefined &&
352
386
  configuredTargetForSurface(manifest, surface) !== undefined
353
387
  );
354
388
  });
355
- const clients = await promptMultiSelect(
356
- "Which clients should receive project skills?",
357
- availableClients.map((client) => ({
358
- value: client,
359
- label: client,
389
+ const agents = await promptMultiSelect(
390
+ "Which agents should receive project skills?",
391
+ availableAgents.map((agent) => ({
392
+ value: agent,
393
+ label: agent,
360
394
  selected:
361
- request.clients.length === 0 || request.clients.includes(client),
395
+ request.agents.length === 0 || request.agents.includes(agent),
362
396
  })),
363
397
  );
364
398
  const skills = parseCommaList(
@@ -367,10 +401,25 @@ export async function runProject(
367
401
  request.skills.join(","),
368
402
  ),
369
403
  );
370
- request = { ...request, name, clients, skills };
404
+ request = { ...request, name, agents, skills };
371
405
  }
372
- const clientTargets = configuredTargetsForClients(manifest, request.clients);
373
- const targets = [...new Set([...request.targets, ...clientTargets])];
406
+ // Local MCP registration + instruction writing are independent of skill
407
+ // pins only offered for agents with a verified project-scoped CLI
408
+ // command (currently just claude-code; see MCP_PROJECT_REGISTRABLE_AGENTS).
409
+ const registrableAgents = request.agents.filter((agent) =>
410
+ MCP_PROJECT_REGISTRABLE_AGENTS.includes(agent as AgentId),
411
+ ) as AgentId[];
412
+ if (guided && registrableAgents.length > 0) {
413
+ request = {
414
+ ...request,
415
+ registerMcp: await confirmAction(
416
+ `Also register skillmux as a project-scoped MCP server for ${registrableAgents.join(", ")}? ` +
417
+ `This writes ${request.path}/.mcp.json, shared via git.`,
418
+ ),
419
+ };
420
+ }
421
+ const agentTargets = configuredTargetsForAgents(manifest, request.agents);
422
+ const targets = [...new Set([...request.targets, ...agentTargets])];
374
423
  const updated = upsertProject(manifest, {
375
424
  name: request.name,
376
425
  paths: [request.path],
@@ -378,15 +427,34 @@ export async function runProject(
378
427
  targets,
379
428
  });
380
429
  const { notes } = validateManifest(updated, vaultPath, localVaultPaths);
430
+
431
+ // The project-local instruction block only teaches an agent to call
432
+ // resolve_skill/fetch_skill (MCP tools) — write it only for agents that
433
+ // are actually getting a project-scoped MCP registration this run.
434
+ const mcpInstructionAgents = request.registerMcp ? registrableAgents : [];
435
+ const instructionPlan = planProjectInstructionSetup(
436
+ mcpInstructionAgents,
437
+ request.path,
438
+ );
439
+ const hasInstructionWrites = instructionPlan.changes.some(
440
+ (change) => change.status !== "unchanged",
441
+ );
442
+
381
443
  const plan = {
382
444
  mode: "project",
383
445
  project: request.name,
384
446
  path: request.path,
385
447
  skills: request.skills,
386
- clients: request.clients,
448
+ agents: request.agents,
387
449
  targets,
388
450
  sync: request.sync,
389
451
  notes,
452
+ instructions: instructionPlan.changes.map(({ path, agents, status }) => ({
453
+ path,
454
+ agents,
455
+ status,
456
+ })),
457
+ register_mcp_for: mcpInstructionAgents,
390
458
  };
391
459
 
392
460
  if (options.dryRun) {
@@ -401,8 +469,14 @@ export async function runProject(
401
469
  console.log("\nReview");
402
470
  console.log(` project: ${request.name}`);
403
471
  console.log(` path: ${request.path}`);
404
- console.log(` clients: ${request.clients.join(", ") || "(none)"}`);
472
+ console.log(` agents: ${request.agents.join(", ") || "(none)"}`);
405
473
  console.log(` skills: ${request.skills.join(", ") || "(none)"}`);
474
+ console.log(
475
+ ` instructions: ${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} file(s)`,
476
+ );
477
+ console.log(
478
+ ` MCP registration: ${mcpInstructionAgents.join(", ") || "(none)"}`,
479
+ );
406
480
  console.log(` sync: ${request.sync ? "yes" : "no"}`);
407
481
  }
408
482
  if (
@@ -421,6 +495,17 @@ export async function runProject(
421
495
  }
422
496
 
423
497
  writeManifestAtomic(manifestPath, updated);
498
+ if (hasInstructionWrites) {
499
+ try {
500
+ applyInstructionPlan(instructionPlan);
501
+ } catch (error) {
502
+ throw new Error(
503
+ `project configuration was saved, but writing instruction files failed: ${
504
+ error instanceof Error ? error.message : String(error)
505
+ }`,
506
+ );
507
+ }
508
+ }
424
509
  if (request.sync) {
425
510
  try {
426
511
  // Reaching here already required approval above (request.yes, or an
@@ -437,7 +522,39 @@ export async function runProject(
437
522
  );
438
523
  }
439
524
  }
440
- emitSuccess({ isJson: options.isJson }, { result: plan }, () =>
441
- console.log(`project "${request.name}" ready at ${request.path}`),
525
+
526
+ // Best-effort and outside the checks above: this mutates another tool's
527
+ // own config, not skillmux's, so a registration failure is reported, never
528
+ // rolled back — the successful project setup above still stands either way.
529
+ const mcpRegistrations: McpRegistrationResult[] = [];
530
+ if (request.registerMcp) {
531
+ for (const agent of registrableAgents) {
532
+ mcpRegistrations.push(
533
+ await registerMcpServer(agent, { scope: "project", cwd: request.path }),
534
+ );
535
+ }
536
+ }
537
+
538
+ emitSuccess(
539
+ { isJson: options.isJson },
540
+ {
541
+ result: {
542
+ ...plan,
543
+ instructions_changed: instructionPlan.changes
544
+ .filter((change) => change.status !== "unchanged")
545
+ .map((change) => change.path),
546
+ mcp_registrations: mcpRegistrations,
547
+ },
548
+ },
549
+ () => {
550
+ console.log(`project "${request.name}" ready at ${request.path}`);
551
+ for (const registration of mcpRegistrations) {
552
+ console.log(
553
+ registration.ok
554
+ ? `MCP registered: ${registration.agent} (project scope)`
555
+ : `MCP registration failed for ${registration.agent}: ${registration.error}`,
556
+ );
557
+ }
558
+ },
442
559
  );
443
560
  }
@@ -1,5 +1,5 @@
1
1
  import { Database } from "bun:sqlite";
2
- import type { TargetAdapter } from "../adapters";
2
+ import type { ContextAdapter } from "../adapters";
3
3
  import type { ResolvedContext } from "../context";
4
4
  import { emitSuccess } from "../output";
5
5
  import { getStats, renderStatsText } from "../stats";
@@ -36,14 +36,14 @@ function parseReportArgs(args: string[]): {
36
36
 
37
37
  export async function runReport(
38
38
  args: string[],
39
- options: { isJson: boolean; target: ResolvedContext; allowInsecure: boolean; adapter: TargetAdapter },
39
+ options: { isJson: boolean; context: ResolvedContext; allowInsecure: boolean; adapter: ContextAdapter },
40
40
  ): Promise<void> {
41
41
  const { db: dbPath, since } = parseReportArgs(args);
42
42
  if (!since)
43
43
  throw new Error(
44
44
  "usage: skillmux report [--context <name> | --server <url> | --db <path>] --since <window> [--json]",
45
45
  );
46
- if (dbPath && options.target.type === "remote")
46
+ if (dbPath && options.context.type === "remote")
47
47
  throw new Error("--db and --context/--server are mutually exclusive");
48
48
 
49
49
  if (dbPath) {
@@ -1,21 +1,14 @@
1
- import { createInterface } from "node:readline/promises";
2
1
  import { expandHome, loadConfig } from "../config";
3
2
  import { parseManifest, resolveManifestPath } from "../manifest";
4
3
  import { isInteractive } from "../output";
4
+ import { askQuestion, type PromptIO } from "../prompts";
5
5
 
6
- export async function confirmAction(prompt: string): Promise<boolean> {
7
- const readline = createInterface({
8
- input: process.stdin,
9
- output: process.stdout,
10
- });
11
- try {
12
- const answer = (await readline.question(`${prompt} [y/N] `))
13
- .trim()
14
- .toLowerCase();
15
- return answer === "y" || answer === "yes";
16
- } finally {
17
- readline.close();
18
- }
6
+ export async function confirmAction(
7
+ prompt: string,
8
+ io: PromptIO = {},
9
+ ): Promise<boolean> {
10
+ const answer = (await askQuestion(`${prompt} [y/N] `, io)).trim().toLowerCase();
11
+ return answer === "y" || answer === "yes";
19
12
  }
20
13
 
21
14
  export async function loadManifestContext() {
@@ -1,10 +1,11 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { expandHome, loadConfig } from "../config";
4
+ import { unknownSubcommandError } from "../output";
4
5
  import { vaultResolutionOrder } from "../vault";
5
6
 
6
7
  export async function runSkill(subCommand: string, args: string[]): Promise<void> {
7
- if (subCommand !== "which") throw new Error("usage: skillmux skill <which>");
8
+ if (subCommand !== "which") throw unknownSubcommandError("skill", subCommand, ["which"]);
8
9
  await runWhich(args);
9
10
  }
10
11
 
@@ -1,9 +1,15 @@
1
1
  import { expandHome } from "../config";
2
- import { planClientSurfaces, SUPPORTED_CLIENT_IDS } from "../init-clients";
2
+ import {
3
+ BUILT_IN_TARGET_NAMES,
4
+ planAgentSurfaces,
5
+ resolveBuiltInTarget,
6
+ SUPPORTED_AGENT_IDS,
7
+ } from "../init-agents";
3
8
  import { planInitManifest, applyInit } from "../init";
4
9
  import { writeManifestAtomic } from "../manifest";
5
- import { emitSuccess } from "../output";
10
+ import { emitSuccess, unknownSubcommandError } from "../output";
6
11
  import { confirmIfNeeded, loadManifestContext } from "./shared";
12
+
7
13
  export async function runTarget(
8
14
  subCommand: string,
9
15
  args: string[],
@@ -19,11 +25,11 @@ export async function runTarget(
19
25
  }
20
26
  const targets = names.map((name) => {
21
27
  const target = manifest.targets[name]!;
22
- const clients = SUPPORTED_CLIENT_IDS.filter((client) => {
23
- const surface = planClientSurfaces([client]).surfaces[0];
28
+ const agents = SUPPORTED_AGENT_IDS.filter((agent) => {
29
+ const surface = planAgentSurfaces([agent]).surfaces[0];
24
30
  return surface !== undefined && surface.path === expandHome(target.dir);
25
31
  });
26
- return { name, ...target, clients };
32
+ return { name, ...target, agents };
27
33
  });
28
34
  emitSuccess({ isJson: options.isJson }, { targets }, () => {
29
35
  if (targets.length === 0) {
@@ -33,7 +39,7 @@ export async function runTarget(
33
39
  console.log(`${target.name}:`);
34
40
  console.log(` dir: ${target.dir}`);
35
41
  console.log(` host: ${target.host ?? "(global)"}`);
36
- console.log(` clients: ${target.clients.join(", ") || "(custom)"}`);
42
+ console.log(` agents: ${target.agents.join(", ") || "(custom)"}`);
37
43
  console.log(
38
44
  ` projects: ${target.project_groups.join(", ") || "(none)"}`,
39
45
  );
@@ -47,9 +53,21 @@ export async function runTarget(
47
53
  const name = args[0];
48
54
  const dirIndex = args.indexOf("--dir");
49
55
  const rawPath = dirIndex === -1 ? undefined : args[dirIndex + 1];
50
- if (!name || !rawPath)
56
+ if (!name)
51
57
  throw new Error("usage: skillmux target add <name> --dir <dir> --yes");
52
- const path = expandHome(rawPath);
58
+
59
+ let path: string;
60
+ if (rawPath) {
61
+ path = expandHome(rawPath);
62
+ } else if (BUILT_IN_TARGET_NAMES.has(name)) {
63
+ path = resolveBuiltInTarget(name, {
64
+ codexHome: process.env.CODEX_HOME ? expandHome(process.env.CODEX_HOME) : undefined,
65
+ }).path;
66
+ } else {
67
+ throw new Error(
68
+ "usage: skillmux target add <name> --dir <dir> --yes (--dir may be omitted for built-in target names: agent-skills, claude-code, codex)",
69
+ );
70
+ }
53
71
  if (options.dryRun) {
54
72
  const planned = planInitManifest(vaultPath, [{ name, dir: path }], []);
55
73
  emitSuccess(
@@ -118,5 +136,5 @@ export async function runTarget(
118
136
  return;
119
137
  }
120
138
 
121
- throw new Error("usage: skillmux target <list|show|add|remove>");
139
+ throw unknownSubcommandError("target", subCommand, ["list", "show", "add", "remove"]);
122
140
  }
@@ -1,3 +1,5 @@
1
+ import { MANAGED_PINS_AGENT_IDS, SUPPORTED_AGENT_IDS } from "./init-agents";
2
+
1
3
  export type ShellType = "bash" | "zsh" | "fish";
2
4
 
3
5
  const TOP_LEVEL_COMMANDS: { name: string; description: string }[] = [
@@ -6,7 +8,7 @@ const TOP_LEVEL_COMMANDS: { name: string; description: string }[] = [
6
8
  { name: "serve", description: "Start MCP server" },
7
9
  { name: "index", description: "Rebuild local search index" },
8
10
  { name: "sync", description: "Synchronize vault skills" },
9
- { name: "init", description: "Configure this machine and its clients" },
11
+ { name: "init", description: "Configure this machine and its agents" },
10
12
  { name: "project", description: "Configure project-scoped skills" },
11
13
  { name: "target", description: "Manage advanced skill-delivery targets" },
12
14
  { name: "core", description: "Pin/unpin skills into [core]" },
@@ -62,18 +64,25 @@ _skillmux_completions() {
62
64
  local-vault)
63
65
  COMPREPLY=( $(compgen -W "init" -- "$cur") )
64
66
  ;;
65
- --client)
66
- COMPREPLY=( $(compgen -W "claude-code codex gemini-cli opencode github-copilot windsurf antigravity goose hermes skillmux-mcp" -- "$cur") )
67
+ eval)
68
+ COMPREPLY=( $(compgen -W "promote" -- "$cur") )
67
69
  ;;
68
- --target)
69
- COMPREPLY=( $(compgen -W "agent-skills claude-code codex custom" -- "$cur") )
70
+ --agent)
71
+ if [ "\${COMP_WORDS[1]}" = "project" ]; then
72
+ COMPREPLY=( $(compgen -W "${MANAGED_PINS_AGENT_IDS.join(" ")}" -- "$cur") )
73
+ else
74
+ COMPREPLY=( $(compgen -W "${SUPPORTED_AGENT_IDS.join(" ")}" -- "$cur") )
75
+ fi
70
76
  ;;
71
77
  esac
72
78
  if [ "\${COMP_WORDS[1]}" = "init" ] && [ "\${#COMPREPLY[@]}" -eq 0 ]; then
73
- COMPREPLY=( $(compgen -W "--client --target --dir --vault --core --migrate-full-vault --no-instructions --no-sync --interactive --yes --dry-run --json" -- "$cur") )
79
+ COMPREPLY=( $(compgen -W "--agent --vault --core --migrate-full-vault --show-mcp-setup --register-mcp --no-instructions --no-sync --interactive --yes --dry-run --json" -- "$cur") )
74
80
  fi
75
81
  if [ "\${COMP_WORDS[1]}" = "project" ] && [ "\${COMP_WORDS[2]}" = "init" ]; then
76
- COMPREPLY=( $(compgen -W "--name --skill --client --target --no-sync --interactive --yes --dry-run --json" -- "$cur") )
82
+ COMPREPLY=( $(compgen -W "--name --skill --agent --target --register-mcp --no-sync --interactive --yes --dry-run --json" -- "$cur") )
83
+ fi
84
+ if [ "\${COMP_WORDS[1]}" = "eval" ] && [ "\${COMP_WORDS[2]}" = "promote" ]; then
85
+ COMPREPLY=( $(compgen -W "--since --out --dry-run --yes --json" -- "$cur") )
77
86
  fi
78
87
  }
79
88
  complete -F _skillmux_completions skillmux
@@ -92,12 +101,12 @@ ${commands}
92
101
  _describe -t commands 'skillmux command' commands
93
102
  elif [[ "$words[2]" == "init" ]]; then
94
103
  _arguments \\
95
- '*--client[select a client]:client:(claude-code codex gemini-cli opencode github-copilot windsurf antigravity goose hermes skillmux-mcp)' \\
96
- '*--target[select a delivery target]:target:(agent-skills claude-code codex custom)' \\
97
- '--dir[custom target directory]:directory:_directories' \\
104
+ '*--agent[select an agent]:agent:(${SUPPORTED_AGENT_IDS.join(" ")})' \\
98
105
  '--vault[vault directory]:directory:_directories' \\
99
106
  '*--core[seed a core skill]:skill id:' \\
100
107
  '--migrate-full-vault[convert a full-vault symlink to managed pins]' \\
108
+ '--show-mcp-setup[also print the MCP registration snippet]' \\
109
+ '--register-mcp[register skillmux via the agent own CLI]' \\
101
110
  '--no-instructions[skip managed instruction files]' \\
102
111
  '--no-sync[save setup without synchronizing targets]' \\
103
112
  '--interactive[force guided setup]' \\
@@ -109,8 +118,9 @@ ${commands}
109
118
  '1:project directory:_directories' \\
110
119
  '--name[project group name]:group:' \\
111
120
  '*--skill[project skill]:skill id:' \\
112
- '*--client[select a client]:client:(claude-code codex gemini-cli opencode github-copilot windsurf antigravity)' \\
121
+ '*--agent[select an agent]:agent:(${MANAGED_PINS_AGENT_IDS.join(" ")})' \\
113
122
  '*--target[select an advanced target]:target:' \\
123
+ '--register-mcp[register a project-scoped MCP server for claude-code]' \\
114
124
  '--no-sync[save setup without synchronizing targets]' \\
115
125
  '--interactive[force guided setup]' \\
116
126
  '--yes[apply without prompts]' \\
@@ -118,6 +128,15 @@ ${commands}
118
128
  '--json[emit a JSON envelope]'
119
129
  elif [[ "$words[2]" == "project" && CURRENT == 3 ]]; then
120
130
  _values 'project command' init list show add-path remove-path pin unpin attach detach
131
+ elif [[ "$words[2]" == "eval" && "$words[3]" == "promote" ]]; then
132
+ _arguments \
133
+ '--since[time window]:window:' \
134
+ '--out[output file]:file:_files' \
135
+ '--dry-run[print the plan without writing]' \
136
+ '--yes[apply without prompts]' \
137
+ '--json[emit a JSON envelope]'
138
+ elif [[ "$words[2]" == "eval" && CURRENT == 3 ]]; then
139
+ _values 'eval command' promote
121
140
  elif [[ "$words[2]" == "target" && CURRENT == 3 ]]; then
122
141
  _values 'target command' list show add remove
123
142
  elif [[ "$words[2]" == "skill" && CURRENT == 3 ]]; then
@@ -139,12 +158,12 @@ _skillmux "$@"
139
158
  return `# fish completion for skillmux
140
159
  complete -c skillmux -f
141
160
  ${topLevel}
142
- complete -c skillmux -n "__fish_seen_subcommand_from init" -l client -x -a "claude-code codex gemini-cli opencode github-copilot windsurf antigravity goose hermes skillmux-mcp" -d "Select a client"
143
- complete -c skillmux -n "__fish_seen_subcommand_from init" -l target -x -a "agent-skills claude-code codex custom" -d "Select a delivery target"
144
- complete -c skillmux -n "__fish_seen_subcommand_from init" -l dir -r -d "Custom target directory"
161
+ complete -c skillmux -n "__fish_seen_subcommand_from init" -l agent -x -a "${SUPPORTED_AGENT_IDS.join(" ")}" -d "Select an agent"
145
162
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l vault -r -d "Vault directory"
146
163
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l core -x -d "Seed a core skill"
147
164
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l migrate-full-vault -d "Convert a full-vault symlink"
165
+ complete -c skillmux -n "__fish_seen_subcommand_from init" -l show-mcp-setup -d "Also print the MCP registration snippet"
166
+ complete -c skillmux -n "__fish_seen_subcommand_from init" -l register-mcp -d "Register skillmux via the agent's own CLI"
148
167
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l no-instructions -d "Skip managed instruction files"
149
168
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l no-sync -d "Save without synchronizing"
150
169
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l interactive -d "Force guided setup"
@@ -154,11 +173,18 @@ complete -c skillmux -n "__fish_seen_subcommand_from init" -l json -d "Emit a JS
154
173
  complete -c skillmux -n "__fish_seen_subcommand_from project" -a "init list show add-path remove-path pin unpin attach detach" -d "Manage projects"
155
174
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l name -x -d "Project group name"
156
175
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l skill -x -d "Project skill"
157
- complete -c skillmux -n "__fish_seen_subcommand_from project" -l client -x -a "claude-code codex gemini-cli opencode github-copilot windsurf antigravity" -d "Select a client"
176
+ complete -c skillmux -n "__fish_seen_subcommand_from project" -l agent -x -a "${MANAGED_PINS_AGENT_IDS.join(" ")}" -d "Select an agent"
158
177
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l target -x -d "Select an advanced delivery target"
178
+ complete -c skillmux -n "__fish_seen_subcommand_from project" -l register-mcp -d "Register a project-scoped MCP server for claude-code"
159
179
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l no-sync -d "Save without synchronizing"
160
180
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l interactive -d "Force guided setup"
161
181
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l yes -d "Apply without prompts"
182
+ complete -c skillmux -n "__fish_seen_subcommand_from eval" -a "promote" -d "Promote correlated fetches into eval cases"
183
+ complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l since -x -d "Time window"
184
+ complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l out -r -d "Output file"
185
+ complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l dry-run -d "Print the plan without writing"
186
+ complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l yes -d "Apply without prompts"
187
+ complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l json -d "Emit a JSON envelope"
162
188
  complete -c skillmux -n "__fish_seen_subcommand_from target" -a "list show add remove" -d "Manage targets"
163
189
  complete -c skillmux -n "__fish_seen_subcommand_from core" -a "pin unpin" -d "Manage [core] pins"
164
190
  complete -c skillmux -n "__fish_seen_subcommand_from skill" -a "which" -d "Show which root resolves a skill_id"
@@ -309,7 +309,7 @@ export async function getDottedKey(key: string, configPath?: string): Promise<un
309
309
  export async function setDottedKey(
310
310
  key: string,
311
311
  rawValStr: string,
312
- opts?: { configPath?: string; dryRun?: boolean; targetName?: string }
312
+ opts?: { configPath?: string; dryRun?: boolean; contextName?: string }
313
313
  ): Promise<SetConfigResult> {
314
314
  validateDottedKey(key);
315
315
  if (isEnvMasked(key)) {
@@ -317,7 +317,7 @@ export async function setDottedKey(
317
317
  }
318
318
 
319
319
  const path = opts?.configPath ?? process.env.SKILLMUX_CONFIG ?? DEFAULT_CONFIG_PATH;
320
- const targetName = opts?.targetName ?? "local";
320
+ const contextName = opts?.contextName ?? "local";
321
321
 
322
322
  const { effective: priorEffective, rawToml } = await getEffectiveConfig(path);
323
323
  const priorVal = getNestedValue(priorEffective as Record<string, any>, key);
@@ -364,7 +364,7 @@ export async function setDottedKey(
364
364
  key,
365
365
  prior_val: priorVal,
366
366
  resulting_val: parsedVal,
367
- target: targetName,
367
+ target: contextName,
368
368
  prior_revision: priorRevision,
369
369
  resulting_revision: resultingRevision,
370
370
  persistence,