@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,29 +1,50 @@
1
1
  import { resolveConfigPath } from "../config";
2
2
  import { diagnose } from "../doctor";
3
3
  import { getEffectiveConfig } from "../config-service";
4
- import type { TargetAdapter } from "../adapters";
4
+ import type { ContextAdapter } from "../adapters";
5
5
  import type { ResolvedContext } from "../context";
6
+ import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
6
7
  import {
7
8
  emitSuccess,
8
9
  green,
9
10
  red,
10
- renderTargetBanner,
11
+ renderContextBanner,
11
12
  } from "../output";
12
13
 
14
+ /**
15
+ * doctor takes no options of its own, but it still has to reject unknown ones
16
+ * rather than silently ignoring them the way every other command does.
17
+ */
18
+ export function parseDoctorArgs(args: readonly string[]): void {
19
+ for (let i = 0; i < args.length; i++) {
20
+ const option = args[i];
21
+ if (isGlobalFlag(option, "--json", "--allow-insecure", "--verbose")) {
22
+ // handled globally by main(); recognized here so it isn't rejected
23
+ } else if (isGlobalFlagWithValue(option)) {
24
+ // handled globally by main()'s resolveContext(); skip its value too
25
+ i++;
26
+ } else {
27
+ throw new Error(`unknown doctor option: ${option}`);
28
+ }
29
+ }
30
+ }
31
+
13
32
  export async function runDoctor(options: {
14
33
  isJson: boolean;
15
- target: ResolvedContext;
16
- adapter: TargetAdapter;
34
+ context: ResolvedContext;
35
+ adapter: ContextAdapter;
36
+ args?: readonly string[];
17
37
  }): Promise<void> {
18
- if (options.target.type === "remote") {
19
- const target = options.target;
38
+ parseDoctorArgs(options.args ?? []);
39
+ if (options.context.type === "remote") {
40
+ const context = options.context;
20
41
  const [status, caps] = await Promise.all([
21
42
  options.adapter.configStatus(),
22
43
  options.adapter.getCapabilities(),
23
44
  ]);
24
45
  const remoteReport = {
25
- target: target.name || target.server,
26
- server: target.server,
46
+ target: context.name || context.server,
47
+ server: context.server,
27
48
  version: status.version,
28
49
  deployment_runtime: status.deployment_runtime,
29
50
  image_variant: status.image_variant ?? null,
@@ -34,8 +55,8 @@ export async function runDoctor(options: {
34
55
  restart_required_keys: status.restart_required_keys,
35
56
  last_reload_error: status.last_reload_error,
36
57
  };
37
- emitSuccess({ isJson: options.isJson, target: options.target }, remoteReport, () => {
38
- renderTargetBanner(options.target);
58
+ emitSuccess({ isJson: options.isJson, target: options.context }, remoteReport, () => {
59
+ renderContextBanner(options.context);
39
60
  console.log(`server: ${remoteReport.server}`);
40
61
  console.log(`version: ${remoteReport.version}`);
41
62
  console.log(`deployment runtime: ${remoteReport.deployment_runtime}`);
@@ -5,20 +5,22 @@ import { excludeExistingCases, parseEvalCases } from "../eval";
5
5
  import { emitSuccess, warn } from "../output";
6
6
  import { parseSince } from "../stats";
7
7
  import { confirmIfNeeded } from "./shared";
8
- import type { TargetAdapter } from "../adapters";
8
+ import type { ContextAdapter } from "../adapters";
9
9
  import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
10
10
 
11
11
  export async function runEvalPromote(
12
12
  args: string[],
13
- options: { isJson: boolean; dryRun: boolean; adapter: TargetAdapter },
13
+ options: { isJson: boolean; dryRun: boolean; adapter: ContextAdapter },
14
14
  ): Promise<void> {
15
15
  let since: string | undefined;
16
+ let out: string | undefined;
16
17
  let target: string | undefined;
17
18
  let dryRun = options.dryRun;
18
19
  let yes = false;
19
20
  for (let i = 0; i < args.length; i++) {
20
21
  const arg = args[i];
21
22
  if (arg === "--since") since = args[++i];
23
+ else if (arg === "--out") out = args[++i];
22
24
  else if (arg === "--target") target = args[++i];
23
25
  else if (arg === "--dry-run") dryRun = true;
24
26
  else if (arg === "--yes") yes = true;
@@ -31,12 +33,20 @@ export async function runEvalPromote(
31
33
  }
32
34
  }
33
35
  if (!since) {
34
- throw new Error("usage: skillmux eval promote --since <window> [--target <path>] [--dry-run] [--yes] [--json]");
36
+ throw new Error("usage: skillmux eval promote --since <window> [--out <path>] [--dry-run] [--yes] [--json]");
37
+ }
38
+
39
+ if (out !== undefined && target !== undefined && out !== target) {
40
+ throw new Error("cannot specify conflicting --out and --target");
41
+ }
42
+
43
+ if (target !== undefined) {
44
+ warn("--target is deprecated, use --out instead");
35
45
  }
36
46
 
37
47
  const config = await loadConfig();
38
48
  const stateDir = expandHome(config.state_dir);
39
- const targetPath = target ?? join(stateDir, "eval-observed.json");
49
+ const targetPath = out ?? target ?? join(stateDir, "eval-observed.json");
40
50
  const sinceDate = parseSince(since);
41
51
  const sinceIso = sinceDate.toISOString();
42
52
 
@@ -15,21 +15,25 @@ import {
15
15
  surfaceCandidates,
16
16
  } from "../init";
17
17
  import {
18
- assessClientReadiness,
19
- detectInstalledClients,
20
- planClientSurfaces,
21
- resolveBuiltInTarget,
22
- SUPPORTED_CLIENT_IDS,
23
- type ClientId,
18
+ assessAgentReadiness,
19
+ detectInstalledAgents,
20
+ planAgentSurfaces,
21
+ SUPPORTED_AGENT_IDS,
22
+ type AgentId,
24
23
  type ReadinessAxis,
25
- } from "../init-clients";
24
+ } from "../init-agents";
26
25
  import {
27
26
  applyInstructionPlan,
28
27
  planInstructionSetup,
29
28
  rollbackInstructionPlan,
30
29
  } from "../init-instructions";
31
30
  import { parseManifest, resolveManifestPath } from "../manifest";
32
- import { isInteractive, warn } from "../output";
31
+ import {
32
+ MCP_REGISTRABLE_AGENTS,
33
+ registerMcpServer,
34
+ type McpRegistrationResult,
35
+ } from "../mcp-registration";
36
+ import { isInteractive } from "../output";
33
37
  import { isGlobalFlag } from "../global-flags";
34
38
  import {
35
39
  parseCommaList,
@@ -49,47 +53,41 @@ import { confirmAction } from "./shared";
49
53
  import { runSync } from "./sync";
50
54
 
51
55
  function parseInitArgs(args: string[]): {
52
- targets: string[];
53
- clients: string[];
56
+ agents: string[];
54
57
  coreSkillIds: string[];
55
- customPath?: string;
56
58
  migrateFullVault: boolean;
59
+ showMcpSetup: boolean;
60
+ registerMcp: boolean;
57
61
  skipInstructions: boolean;
58
62
  sync: boolean;
59
63
  vaultPath?: string;
60
64
  yes: boolean;
61
65
  } {
62
- const targets: string[] = [];
63
- const clients: string[] = [];
66
+ const agents: string[] = [];
64
67
  const coreSkillIds: string[] = [];
65
- let customPath: string | undefined;
66
68
  let migrateFullVault = false;
69
+ let showMcpSetup = false;
70
+ let registerMcp = false;
67
71
  let skipInstructions = false;
68
72
  let sync = true;
69
73
  let vaultPath: string | undefined;
70
74
  let yes = false;
71
75
  for (let i = 0; i < args.length; i++) {
72
76
  const option = args[i];
73
- if (option === "--target") {
74
- const value = args[i + 1];
75
- if (!value) throw new Error("--target requires a name");
76
- targets.push(value);
77
- i++;
78
- } else if (option === "--client") {
77
+ if (option === "--target" || option === "--dir" || option === "--client") {
78
+ throw new Error(
79
+ `${option} was removed; select a specific supported agent with --agent instead (see "skillmux init --help")`,
80
+ );
81
+ } else if (option === "--agent") {
79
82
  const value = args[i + 1];
80
- if (!value) throw new Error("--client requires a name");
81
- clients.push(value);
83
+ if (!value) throw new Error("--agent requires a name");
84
+ agents.push(value);
82
85
  i++;
83
86
  } else if (option === "--vault") {
84
87
  const value = args[i + 1];
85
88
  if (!value) throw new Error("--vault requires a path");
86
89
  vaultPath = value;
87
90
  i++;
88
- } else if (option === "--dir") {
89
- const value = args[i + 1];
90
- if (!value) throw new Error("--dir requires a directory");
91
- customPath = value;
92
- i++;
93
91
  } else if (option === "--core") {
94
92
  const value = args[i + 1];
95
93
  if (!value) throw new Error("--core requires a skill_id");
@@ -102,6 +100,10 @@ function parseInitArgs(args: string[]): {
102
100
  continue;
103
101
  } else if (option === "--migrate-full-vault") {
104
102
  migrateFullVault = true;
103
+ } else if (option === "--show-mcp-setup") {
104
+ showMcpSetup = true;
105
+ } else if (option === "--register-mcp") {
106
+ registerMcp = true;
105
107
  } else if (option === "--no-instructions") {
106
108
  skipInstructions = true;
107
109
  } else if (option === "--no-sync") {
@@ -113,11 +115,11 @@ function parseInitArgs(args: string[]): {
113
115
  }
114
116
  }
115
117
  return {
116
- targets,
117
- clients,
118
+ agents,
118
119
  coreSkillIds,
119
- customPath,
120
120
  migrateFullVault,
121
+ showMcpSetup,
122
+ registerMcp,
121
123
  skipInstructions,
122
124
  sync,
123
125
  vaultPath,
@@ -125,16 +127,49 @@ function parseInitArgs(args: string[]): {
125
127
  };
126
128
  }
127
129
 
130
+ interface InitJsonPayload {
131
+ command: "init";
132
+ phase: "plan" | "result";
133
+ dry_run: boolean;
134
+ applied: boolean;
135
+ plan: unknown;
136
+ result?: unknown;
137
+ }
138
+
139
+ /**
140
+ * init predates the shared emitSuccess envelope and shipped its own top-level
141
+ * keys, which existing automation reads. Emit the documented envelope
142
+ * (`target`/`data`/`error`, see docs/cli.md) so generic consumers work, while
143
+ * keeping the original keys so nothing breaks. init's error path already goes
144
+ * through the shared handler and needs no bridging.
145
+ *
146
+ * DEPRECATED: the duplicated top-level `command`/`phase`/`dry_run`/`applied`/
147
+ * `plan`/`result` keys should be dropped in the next major version, leaving
148
+ * only the standard envelope. init is local-only, so `target` is always
149
+ * "local".
150
+ */
151
+ function initJsonEnvelope(payload: InitJsonPayload): string {
152
+ return JSON.stringify({
153
+ schema_version: 1,
154
+ ok: true,
155
+ context: "local",
156
+ target: "local",
157
+ data: payload,
158
+ error: null,
159
+ ...payload,
160
+ });
161
+ }
162
+
128
163
  export async function runInit(
129
164
  args: string[],
130
165
  options: { isJson: boolean; dryRun: boolean },
131
166
  ): Promise<void> {
132
167
  const {
133
- targets: explicitTargets,
134
- clients: requestedClients,
168
+ agents: requestedAgents,
135
169
  coreSkillIds,
136
- customPath,
137
170
  migrateFullVault,
171
+ showMcpSetup,
172
+ registerMcp: requestedRegisterMcp,
138
173
  skipInstructions,
139
174
  sync,
140
175
  vaultPath: requestedVaultPath,
@@ -150,9 +185,14 @@ export async function runInit(
150
185
  let configPlan: ConfigInitPlan | undefined;
151
186
  let vaultPath: string;
152
187
  if (!existsSync(configPath)) {
153
- const bootstrapVaultPath =
154
- requestedVaultPath ??
155
- (!options.isJson && isInteractive() ? "~/skills" : undefined);
188
+ let bootstrapVaultPath = requestedVaultPath;
189
+ if (!bootstrapVaultPath) {
190
+ if (guided) {
191
+ bootstrapVaultPath = await promptText("Skill vault path", "~/skills");
192
+ } else if (!options.isJson && isInteractive()) {
193
+ bootstrapVaultPath = "~/skills";
194
+ }
195
+ }
156
196
  if (!bootstrapVaultPath) {
157
197
  throw new Error(
158
198
  `machine config does not exist: ${configPath}; re-run with --vault <path>`,
@@ -161,7 +201,7 @@ export async function runInit(
161
201
  configPlan = planConfigInit(configPath, expandHome(bootstrapVaultPath));
162
202
  vaultPath = configPlan.vaultPath;
163
203
  if (!options.isJson) {
164
- console.log(`config create: ${configPath}`);
204
+ console.log(`config create: ${configPath} (vault: ${vaultPath})`);
165
205
  }
166
206
  } else {
167
207
  const config = await loadConfig();
@@ -178,25 +218,25 @@ export async function runInit(
178
218
  throw new Error(vaultHealth.message);
179
219
  }
180
220
 
181
- let selectedClients = requestedClients;
221
+ let selectedAgents = requestedAgents;
182
222
  if (guided) {
183
- const detected = detectInstalledClients({
223
+ const detected = detectInstalledAgents({
184
224
  codexHome: process.env.CODEX_HOME
185
225
  ? expandHome(process.env.CODEX_HOME)
186
226
  : undefined,
187
227
  });
188
228
  const evidence = new Map(
189
- detected.map((item) => [item.client, item.evidence]),
229
+ detected.map((item) => [item.agent, item.evidence]),
190
230
  );
191
- selectedClients = await promptMultiSelect(
192
- "Which clients do you use?",
193
- SUPPORTED_CLIENT_IDS.map((client) => ({
194
- value: client,
195
- label: client,
196
- detail: evidence.has(client)
197
- ? `detected: ${evidence.get(client)}`
231
+ selectedAgents = await promptMultiSelect(
232
+ "Which agents do you use?",
233
+ SUPPORTED_AGENT_IDS.map((agent) => ({
234
+ value: agent,
235
+ label: agent,
236
+ detail: evidence.has(agent)
237
+ ? `detected: ${evidence.get(agent)}`
198
238
  : undefined,
199
- selected: evidence.has(client) || requestedClients.includes(client),
239
+ selected: evidence.has(agent) || requestedAgents.includes(agent),
200
240
  })),
201
241
  );
202
242
  }
@@ -210,77 +250,79 @@ export async function runInit(
210
250
  );
211
251
  }
212
252
 
213
- const clientPlan = planClientSurfaces(selectedClients, {
253
+ // Native pins and MCP registration are independent choices — this stays
254
+ // opt-in and only offered for agents whose own CLI we've verified, so a
255
+ // user who only wants native skill management sees nothing new here.
256
+ const registrableAgents = selectedAgents.filter((agent) =>
257
+ MCP_REGISTRABLE_AGENTS.includes(agent as AgentId),
258
+ ) as AgentId[];
259
+ let registerMcp = requestedRegisterMcp;
260
+ if (guided && registrableAgents.length > 0) {
261
+ registerMcp = await confirmAction(
262
+ `Also register skillmux as an MCP server for ${registrableAgents.join(", ")}?`,
263
+ );
264
+ }
265
+
266
+ const agentPlan = planAgentSurfaces(selectedAgents, {
214
267
  codexHome: process.env.CODEX_HOME
215
268
  ? expandHome(process.env.CODEX_HOME)
216
269
  : undefined,
217
270
  });
271
+ // The managed instruction block only teaches an agent to call resolve_skill
272
+ // /fetch_skill (MCP tools) — writing it for an agent with no MCP registered
273
+ // or requested would tell that agent to call tools that don't exist. Only
274
+ // agents actually getting MCP this run (or whose snippet the user asked to
275
+ // see) get the block; native-only setups get no instruction writes at all.
276
+ const mcpInstructionAgentIds = agentPlan.agents
277
+ .map((agent) => agent.id)
278
+ .filter(
279
+ (agentId) =>
280
+ showMcpSetup || (registerMcp && registrableAgents.includes(agentId)),
281
+ );
218
282
  const instructionPlan = planInstructionSetup(
219
- skipInstructions ? [] : clientPlan.clients.map((client) => client.id),
283
+ skipInstructions ? [] : mcpInstructionAgentIds,
220
284
  {
221
285
  codexHome: process.env.CODEX_HOME
222
286
  ? expandHome(process.env.CODEX_HOME)
223
287
  : undefined,
224
288
  },
225
289
  );
226
- const instructionReadiness: Partial<Record<ClientId, ReadinessAxis>> = {};
290
+ const instructionReadiness: Partial<Record<AgentId, ReadinessAxis>> = {};
227
291
  for (const change of instructionPlan.changes) {
228
- for (const client of change.clients) {
229
- instructionReadiness[client] = {
292
+ for (const agent of change.agents) {
293
+ instructionReadiness[agent] = {
230
294
  status: change.status === "unchanged" ? "ready" : "planned",
231
295
  detail: change.path,
232
296
  };
233
297
  }
234
298
  }
235
299
  for (const manual of instructionPlan.manual) {
236
- instructionReadiness[manual.client] = {
300
+ instructionReadiness[manual.agent] = {
237
301
  status: "manual",
238
302
  detail: manual.reason,
239
303
  };
240
304
  }
241
- const builtInNames = new Set([
242
- "agent-skills",
243
- "claude-code",
244
- "codex",
245
- "custom",
246
- "agents",
247
- "claude",
248
- ]);
249
- const explicitSurfaceTargets = explicitTargets
250
- .filter((name) => builtInNames.has(name))
251
- .map((name) =>
252
- resolveBuiltInTarget(name, {
253
- codexHome: process.env.CODEX_HOME
254
- ? expandHome(process.env.CODEX_HOME)
255
- : undefined,
256
- customPath: customPath ? expandHome(customPath) : undefined,
257
- }),
258
- );
259
- if (customPath && !explicitTargets.includes("custom")) {
260
- throw new Error("--dir may only be used with --target custom");
261
- }
262
- for (const target of explicitSurfaceTargets) {
263
- if (target.warning) warn(target.warning);
305
+ for (const agent of agentPlan.agents) {
306
+ if (mcpInstructionAgentIds.includes(agent.id)) continue;
307
+ if (instructionReadiness[agent.id]) continue;
308
+ instructionReadiness[agent.id] = {
309
+ status: "not-applicable",
310
+ detail: "no MCP requested — see --show-mcp-setup / --register-mcp",
311
+ };
264
312
  }
265
- const targetByPath = new Map(
266
- explicitSurfaceTargets.map(
267
- (target) => [target.path, target.targetName] as const,
268
- ),
269
- );
270
313
  const existingManifestPath = resolveManifestPath(vaultPath);
271
314
  const existingManifest = existingManifestPath
272
315
  ? parseManifest(await Bun.file(existingManifestPath).text())
273
316
  : undefined;
274
- for (const surface of clientPlan.surfaces) {
275
- if (!targetByPath.has(surface.path)) {
276
- targetByPath.set(
277
- surface.path,
278
- existingManifest
279
- ? (configuredTargetForSurface(existingManifest, surface) ??
280
- surface.targetName)
281
- : surface.targetName,
282
- );
283
- }
317
+ const targetByPath = new Map<string, string>();
318
+ for (const surface of agentPlan.surfaces) {
319
+ targetByPath.set(
320
+ surface.path,
321
+ existingManifest
322
+ ? (configuredTargetForSurface(existingManifest, surface) ??
323
+ surface.targetName)
324
+ : surface.targetName,
325
+ );
284
326
  }
285
327
  const candidatePaths = [
286
328
  ...new Set([
@@ -327,11 +369,11 @@ export async function runInit(
327
369
  `${name} (${candidate.path}): ${kind}, ${candidate.skillCount} skills${marked}`,
328
370
  );
329
371
  }
330
- for (const readiness of assessClientReadiness(
331
- clientPlan,
372
+ for (const readiness of assessAgentReadiness(
373
+ agentPlan,
332
374
  instructionReadiness,
333
375
  )) {
334
- console.log(`\n${readiness.client} readiness:`);
376
+ console.log(`\n${readiness.agent} readiness:`);
335
377
  console.log(
336
378
  ` skill surface: ${readiness.skillSurface.status} — ${readiness.skillSurface.detail}`,
337
379
  );
@@ -344,20 +386,15 @@ export async function runInit(
344
386
  }
345
387
  for (const change of instructionPlan.changes) {
346
388
  console.log(
347
- `instructions ${change.status}: ${change.path} (${change.clients.join(", ")})`,
389
+ `instructions ${change.status}: ${change.path} (${change.agents.join(", ")})`,
348
390
  );
349
391
  }
350
392
  for (const manual of instructionPlan.manual) {
351
- console.log(`instructions manual: ${manual.client} — ${manual.reason}`);
393
+ console.log(`instructions manual: ${manual.agent} — ${manual.reason}`);
352
394
  }
353
395
  }
354
396
 
355
- const requestedTargets = [
356
- ...new Set([
357
- ...explicitTargets.filter((name) => !builtInNames.has(name)),
358
- ...targetByPath.values(),
359
- ]),
360
- ];
397
+ const requestedTargets = [...new Set(targetByPath.values())];
361
398
  const hasInstructionWrites = instructionPlan.changes.some(
362
399
  (change) => change.status !== "unchanged",
363
400
  );
@@ -402,7 +439,7 @@ export async function runInit(
402
439
  );
403
440
  }
404
441
  throw new Error(
405
- `unknown --target "${name}": not among detected surfaces`,
442
+ `target "${name}" not among detected surfaces`,
406
443
  );
407
444
  }
408
445
  }
@@ -425,22 +462,21 @@ export async function runInit(
425
462
  config: configPlan
426
463
  ? { path: configPlan.configPath, action: configPlan.action }
427
464
  : { path: configPath, action: "preserve" },
428
- clients: clientPlan.clients.map((client) => client.id),
465
+ agents: agentPlan.agents.map((agent) => agent.id),
429
466
  targets: confirmedTargets,
430
467
  core: plannedManifest.core.skills,
431
- instructions: instructionPlan.changes.map(({ path, clients, status }) => ({
468
+ instructions: instructionPlan.changes.map(({ path, agents, status }) => ({
432
469
  path,
433
- clients,
470
+ agents,
434
471
  status,
435
472
  })),
436
473
  manual: instructionPlan.manual,
474
+ register_mcp_for: registerMcp ? registrableAgents : [],
437
475
  };
438
476
  if (!hasChanges) {
439
477
  if (options.isJson) {
440
478
  console.log(
441
- JSON.stringify({
442
- schema_version: 1,
443
- ok: true,
479
+ initJsonEnvelope({
444
480
  command: "init",
445
481
  phase: "plan",
446
482
  dry_run: options.dryRun,
@@ -466,9 +502,7 @@ export async function runInit(
466
502
  if (options.dryRun) {
467
503
  if (options.isJson) {
468
504
  console.log(
469
- JSON.stringify({
470
- schema_version: 1,
471
- ok: true,
505
+ initJsonEnvelope({
472
506
  command: "init",
473
507
  phase: "plan",
474
508
  dry_run: true,
@@ -480,7 +514,8 @@ export async function runInit(
480
514
  console.log(
481
515
  `\ndry-run: ${confirmedTargets.length} target(s), ` +
482
516
  `${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} instruction file(s), ` +
483
- `core: ${plannedManifest.core.skills.join(", ") || "(unchanged)"}`,
517
+ `core: ${plannedManifest.core.skills.join(", ") || "(unchanged)"}, ` +
518
+ `MCP registration: ${registerMcp ? registrableAgents.join(", ") || "(none)" : "(none)"}`,
484
519
  );
485
520
  }
486
521
  return;
@@ -490,7 +525,7 @@ export async function runInit(
490
525
  if (!options.isJson && isInteractive()) {
491
526
  if (guided) {
492
527
  console.log("\nReview");
493
- console.log(` clients: ${selectedClients.join(", ") || "(none)"}`);
528
+ console.log(` agents: ${selectedAgents.join(", ") || "(none)"}`);
494
529
  console.log(
495
530
  ` targets: ${confirmedTargets.map((target) => `${target.name} -> ${target.dir}`).join(", ") || "(none)"}`,
496
531
  );
@@ -500,6 +535,9 @@ export async function runInit(
500
535
  console.log(
501
536
  ` core: ${plannedManifest.core.skills.join(", ") || "(none)"}`,
502
537
  );
538
+ console.log(
539
+ ` MCP registration: ${registerMcp ? registrableAgents.join(", ") || "(none)" : "(none)"}`,
540
+ );
503
541
  console.log(` sync: ${sync ? "yes" : "no"}`);
504
542
  if (!(await confirmAction("apply this setup plan?"))) {
505
543
  console.log("init cancelled");
@@ -572,11 +610,19 @@ export async function runInit(
572
610
  );
573
611
  }
574
612
 
613
+ // Best-effort and outside the rollback above: this mutates another tool's
614
+ // own config, not skillmux's, so a registration failure is reported, never
615
+ // rolled back — the successful native setup above still stands either way.
616
+ const mcpRegistrations: McpRegistrationResult[] = [];
617
+ if (registerMcp) {
618
+ for (const agent of registrableAgents) {
619
+ mcpRegistrations.push(await registerMcpServer(agent));
620
+ }
621
+ }
622
+
575
623
  if (options.isJson) {
576
624
  console.log(
577
- JSON.stringify({
578
- schema_version: 1,
579
- ok: true,
625
+ initJsonEnvelope({
580
626
  command: "init",
581
627
  phase: "result",
582
628
  dry_run: false,
@@ -589,6 +635,7 @@ export async function runInit(
589
635
  .filter((change) => change.status !== "unchanged")
590
636
  .map((change) => change.path),
591
637
  core: plannedManifest.core.skills,
638
+ mcp_registrations: mcpRegistrations,
592
639
  },
593
640
  }),
594
641
  );
@@ -605,11 +652,15 @@ export async function runInit(
605
652
  if (plannedManifest.core.skills.length === 0 && confirmedTargets.length > 0) {
606
653
  console.log("next: skillmux core pin <skill_id> --yes");
607
654
  }
608
- if (confirmedTargets.length > 0) console.log("next: skillmux sync");
609
- if (
610
- selectedClients.length === 0 ||
611
- selectedClients.includes("skillmux-mcp")
612
- ) {
655
+ if (!sync && confirmedTargets.length > 0) console.log("next: skillmux sync");
656
+ for (const registration of mcpRegistrations) {
657
+ console.log(
658
+ registration.ok
659
+ ? `registered skillmux as an MCP server for ${registration.agent}`
660
+ : `failed to register skillmux as an MCP server for ${registration.agent}: ${registration.error}`,
661
+ );
662
+ }
663
+ if (selectedAgents.length === 0 || showMcpSetup) {
613
664
  console.log(`\n${printLastMile()}`);
614
665
  }
615
666
  // Reaching this point already required approval above (--yes, or an accepted
@@ -617,5 +668,5 @@ export async function runInit(
617
668
  // new target directories this init just adopted, so runSync's own new-target
618
669
  // confirmation gate would just be a redundant (and non-interactively,
619
670
  // silently-skipping) re-ask.
620
- if (guided && sync && confirmedTargets.length > 0) await runSync(["--yes"]);
671
+ if (sync && confirmedTargets.length > 0) await runSync(["--yes"]);
621
672
  }