@heretyc/subagent-mcp 2.6.1 → 2.6.2

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/dist/doctor.js CHANGED
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  // `subagent-mcp doctor` — read-only health check for the installed addon.
3
3
  //
4
- // Diagnoses without touching any file: install completeness, vendor presence,
5
- // and whether each vendor's wiring (MCP server + hooks) points at THIS install.
6
- // The fixer is always `subagent-mcp setup` (idempotent, self-repairing); doctor
7
- // just tells you whether you need it and what exactly is wrong.
4
+ // Diagnoses install completeness, vendor presence, and whether each vendor's
5
+ // wiring (MCP server + hooks) points at THIS install. Doctor self-repairs
6
+ // missing MCP registrations via vendor CLIs; use `subagent-mcp setup` for
7
+ // config-file and hook repairs.
8
8
  //
9
9
  // Exit code: 0 = everything healthy, 1 = at least one check failed.
10
10
  import { verifyWiring } from "./setup.js";
11
11
  export async function runDoctor() {
12
- console.log("subagent-mcp doctor (read-only changes nothing)\n");
12
+ console.log("subagent-mcp doctor (checks wiring; repairs missing MCP registrations via vendor CLIs)\n");
13
13
  const major = Number(process.versions.node.split(".")[0]);
14
14
  console.log(` ${major >= 18 ? "PASS" : "FAIL"} node version — ${process.versions.node}` +
15
15
  (major >= 18 ? "" : " (Node >= 18 required)"));
16
16
  let failed = major < 18 ? 1 : 0;
17
- for (const r of verifyWiring()) {
17
+ for (const r of verifyWiring(undefined, true)) {
18
18
  console.log(` ${r.ok ? "PASS" : "FAIL"} ${r.label} — ${r.detail}`);
19
19
  if (!r.ok)
20
20
  failed++;
package/dist/setup.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // - Every config file is backed up before its first edit.
17
17
  // - Failures never abort the run: they are collected and reported at the end
18
18
  // with a copy-paste repair prompt the user can hand to Claude/Codex.
19
- import { existsSync, readFileSync, writeFileSync, copyFileSync, } from "node:fs";
19
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, } from "node:fs";
20
20
  import { homedir } from "node:os";
21
21
  import { join, dirname, resolve } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
@@ -104,17 +104,14 @@ export function reconcileClaudeJson(cj, serverPath) {
104
104
  const cur = servers["subagent-mcp"];
105
105
  if (cur) {
106
106
  const args = cur.args;
107
- const exact = cur.command === "node" &&
108
- Array.isArray(args) &&
109
- args.length === 1 &&
110
- args[0] === serverPath;
107
+ const exact = cur.command === "subagent-mcp" && Array.isArray(args) && args.length === 0;
111
108
  if (exact)
112
109
  return { changed: false, status: "ok" };
113
110
  }
114
111
  servers["subagent-mcp"] = {
115
112
  type: "stdio",
116
- command: "node",
117
- args: [serverPath],
113
+ command: "subagent-mcp",
114
+ args: [],
118
115
  env: {},
119
116
  };
120
117
  return { changed: true, status: cur ? "repaired" : "added" };
@@ -144,7 +141,7 @@ export function reconcileCodexToml(toml, serverPath) {
144
141
  status: "added",
145
142
  };
146
143
  }
147
- if (m[0].includes(`args = ["${serverPath}"]`)) {
144
+ if (m[0].includes(`command = "node"`) && m[0].includes(`args = ["${serverPath}"]`)) {
148
145
  return { toml, changed: false, status: "ok" };
149
146
  }
150
147
  return {
@@ -242,26 +239,60 @@ function backup(file) {
242
239
  }
243
240
  }
244
241
  function runCmd(cmd, cmdArgs) {
242
+ return runCmdCapture(cmd, cmdArgs).ok;
243
+ }
244
+ function quoteWinShellArg(arg) {
245
+ if (!/[ \t"]/.test(arg))
246
+ return arg;
247
+ return `"${arg.replace(/"/g, '\\"')}"`;
248
+ }
249
+ function quoteWinShellExe(exe) {
250
+ return `"${exe.replace(/"/g, '\\"')}"`;
251
+ }
252
+ function runCmdCapture(cmd, cmdArgs) {
245
253
  console.log(` $ ${cmd} ${cmdArgs.join(" ")}`);
246
254
  if (DRY_RUN) {
247
255
  console.log(" (dry-run: skipped)");
248
- return true;
256
+ return { ok: true, stdout: "" };
249
257
  }
250
258
  try {
251
- if (process.platform === "win32") {
252
- // npm-installed CLIs are .cmd shims execFileSync can't spawn directly.
253
- const line = [cmd, ...cmdArgs.map((a) => (/\s/.test(a) ? `"${a}"` : a))].join(" ");
254
- execSync(line, { stdio: "pipe" });
255
- }
256
- else {
257
- execFileSync(cmd, cmdArgs, { stdio: "pipe" });
258
- }
259
- return true;
259
+ const exe = findOnPath(cmd) ?? cmd;
260
+ const isWinCmdShim = process.platform === "win32" && /\.(?:cmd|bat)$/i.test(exe);
261
+ const stdout = isWinCmdShim
262
+ ? execSync([quoteWinShellExe(exe), ...cmdArgs.map(quoteWinShellArg)].join(" "), { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
263
+ : execFileSync(exe, cmdArgs, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
264
+ return { ok: true, stdout };
260
265
  }
261
266
  catch {
262
- return false;
267
+ return { ok: false, stdout: "" };
263
268
  }
264
269
  }
270
+ export function claudeAddArgs() {
271
+ return ["mcp", "add", "subagent-mcp", "subagent-mcp", "-s", "user"];
272
+ }
273
+ export function claudeRemoveArgs() {
274
+ return ["mcp", "remove", "subagent-mcp", "-s", "user"];
275
+ }
276
+ export function codexAddArgs(serverPath) {
277
+ return ["mcp", "add", "subagent-mcp", "--", "node", serverPath];
278
+ }
279
+ export function codexRemoveArgs() {
280
+ return ["mcp", "remove", "subagent-mcp"];
281
+ }
282
+ function claudeRegisteredViaCli() {
283
+ const get = runCmdCapture("claude", ["mcp", "get", "subagent-mcp"]);
284
+ if (get.ok && get.stdout.includes("subagent-mcp"))
285
+ return true;
286
+ const list = runCmdCapture("claude", ["mcp", "list"]);
287
+ return list.ok && list.stdout.includes("subagent-mcp");
288
+ }
289
+ function codexRegisteredViaCli() {
290
+ const get = runCmdCapture("codex", ["mcp", "get", "subagent-mcp"]);
291
+ if (get.ok && get.stdout.includes("subagent-mcp"))
292
+ return true;
293
+ const list = runCmdCapture("codex", ["mcp", "list"]);
294
+ return list.ok && list.stdout.includes("subagent-mcp");
295
+ }
265
296
  const issues = [];
266
297
  function repairPromptFor(vendor, problem) {
267
298
  const p = serverPaths();
@@ -269,7 +300,7 @@ function repairPromptFor(vendor, problem) {
269
300
  return (`subagent-mcp setup hit a problem on my machine: ${problem}. ` +
270
301
  `The install root is "${fwd(INSTALL_ROOT)}". Please repair my Claude Code wiring: ` +
271
302
  `(1) register a user-scope MCP server named "subagent-mcp" running ` +
272
- `[node "${p.server}"] (use 'claude mcp add --scope user' or edit the mcpServers ` +
303
+ `the global bin shim "subagent-mcp" (use 'claude mcp add subagent-mcp subagent-mcp -s user' or edit the mcpServers ` +
273
304
  `key in ~/.claude.json), and (2) ensure ~/.claude/settings.json has a ` +
274
305
  `hooks.UserPromptSubmit entry {type:"command", command:"node", args:["${p.claudeHook}"]}. ` +
275
306
  `Back up any file before editing it.`);
@@ -308,20 +339,27 @@ function wireClaude() {
308
339
  const probe = JSON.parse(JSON.stringify(cj));
309
340
  const { status } = reconcileClaudeJson(probe, p.server);
310
341
  if (status === "ok") {
311
- describe("ok", "MCP server (user scope)");
342
+ let registered = claudeRegisteredViaCli();
343
+ if (!registered) {
344
+ runCmd("claude", claudeAddArgs());
345
+ registered = claudeRegisteredViaCli();
346
+ }
347
+ if (registered)
348
+ describe("ok", "MCP server (user scope)");
349
+ else
350
+ fail("claude", "MCP server file shape is correct, but 'claude mcp add' failed to register it with the CLI");
312
351
  }
313
352
  else {
314
353
  if (status === "repaired") {
315
354
  console.log(" MCP server registration points at a stale path — re-registering.");
316
- runCmd("claude", ["mcp", "remove", "-s", "user", "subagent-mcp"]);
355
+ runCmd("claude", claudeRemoveArgs());
317
356
  }
318
- const cliOk = runCmd("claude", [
319
- "mcp", "add", "--scope", "user", "subagent-mcp", "--", "node", p.server,
320
- ]);
357
+ const cliOk = runCmd("claude", claudeAddArgs());
358
+ const cliVerified = cliOk && claudeRegisteredViaCli();
321
359
  // Read back; if the CLI failed or didn't take, write the entry directly.
322
360
  const after = readJson(cjFile, {});
323
361
  const verify = reconcileClaudeJson(after, p.server);
324
- if (verify.status !== "ok" && !DRY_RUN) {
362
+ if (!cliVerified && verify.status !== "ok" && !DRY_RUN) {
325
363
  if (!cliOk)
326
364
  console.log(" 'claude mcp add' failed — writing ~/.claude.json directly.");
327
365
  backup(cjFile);
@@ -359,7 +397,14 @@ function wireCodex() {
359
397
  const cfg = join(codexDir, "config.toml");
360
398
  const toml = existsSync(cfg) ? readFileSync(cfg, "utf8") : "";
361
399
  const r = reconcileCodexToml(toml, p.server);
362
- if (r.changed && !DRY_RUN) {
400
+ if (r.status === "repaired") {
401
+ console.log(" MCP server registration points at a stale path — re-registering.");
402
+ runCmd("codex", codexRemoveArgs());
403
+ }
404
+ const cliOk = r.status === "ok" && codexRegisteredViaCli() ? true : runCmd("codex", codexAddArgs(p.server));
405
+ if (!cliOk && r.changed && !DRY_RUN) {
406
+ console.log(" 'codex mcp add' failed — writing ~/.codex/config.toml directly.");
407
+ mkdirSync(codexDir, { recursive: true });
363
408
  backup(cfg);
364
409
  writeFileSync(cfg, r.toml);
365
410
  }
@@ -377,6 +422,7 @@ function wireCodex() {
377
422
  const hookCmd = `node "${p.codexHook}"`;
378
423
  const { changed, statuses } = reconcileCodexHooks(h, hookCmd);
379
424
  if (changed && !DRY_RUN) {
425
+ mkdirSync(codexDir, { recursive: true });
380
426
  backup(hfile);
381
427
  writeFileSync(hfile, JSON.stringify(h, null, 2));
382
428
  }
@@ -392,7 +438,7 @@ function wireCodex() {
392
438
  fail("codex", `could not write hooks.json: ${e.message}`);
393
439
  }
394
440
  }
395
- export function verifyWiring(root = INSTALL_ROOT) {
441
+ export function verifyWiring(root = INSTALL_ROOT, repair = false) {
396
442
  const p = serverPaths(root);
397
443
  const results = [];
398
444
  const home = homedir();
@@ -403,7 +449,29 @@ export function verifyWiring(root = INSTALL_ROOT) {
403
449
  detail: missing.length === 0 ? `all present under ${fwd(root)}` : `missing: ${missing.join(", ")}`,
404
450
  });
405
451
  const hasClaude = findOnPath("claude") !== null;
452
+ const hasClaudeConfig = existsSync(join(home, ".claude.json"));
406
453
  if (hasClaude) {
454
+ const sj = readJson(join(home, ".claude", "settings.json"), {});
455
+ let registered = claudeRegisteredViaCli();
456
+ let repaired = false;
457
+ if (!registered && repair) {
458
+ runCmd("claude", claudeAddArgs());
459
+ repaired = true;
460
+ registered = claudeRegisteredViaCli();
461
+ }
462
+ const hk = reconcileClaudeSettings(JSON.parse(JSON.stringify(sj)), p.claudeHook);
463
+ results.push({
464
+ label: "claude: MCP server (user scope)",
465
+ ok: registered,
466
+ detail: registered ? (repaired ? "repaired" : "registered") : "not registered; CLI repair failed",
467
+ });
468
+ results.push({
469
+ label: "claude: UserPromptSubmit hook",
470
+ ok: hk.status === "ok",
471
+ detail: hk.status === "ok" ? "wired" : `${hk.status === "repaired" ? "stale path" : "not wired"} - run: subagent-mcp setup`,
472
+ });
473
+ }
474
+ else if (hasClaudeConfig) {
407
475
  const cj = readJson(join(home, ".claude.json"), {});
408
476
  const sj = readJson(join(home, ".claude", "settings.json"), {});
409
477
  const srv = reconcileClaudeJson(JSON.parse(JSON.stringify(cj)), p.server);
@@ -411,34 +479,48 @@ export function verifyWiring(root = INSTALL_ROOT) {
411
479
  results.push({
412
480
  label: "claude: MCP server (user scope)",
413
481
  ok: srv.status === "ok",
414
- detail: srv.status === "ok" ? "registered" : `${srv.status === "repaired" ? "stale path" : "not registered"} run: subagent-mcp setup`,
482
+ detail: srv.status === "ok" ? "registered (file fallback)" : "not registered; claude CLI not on PATH",
415
483
  });
416
484
  results.push({
417
485
  label: "claude: UserPromptSubmit hook",
418
486
  ok: hk.status === "ok",
419
- detail: hk.status === "ok" ? "wired" : `${hk.status === "repaired" ? "stale path" : "not wired"} run: subagent-mcp setup`,
487
+ detail: hk.status === "ok" ? "wired" : `${hk.status === "repaired" ? "stale path" : "not wired"} - run: subagent-mcp setup`,
420
488
  });
421
489
  }
422
- const hasCodex = findOnPath("codex") !== null || existsSync(join(home, ".codex"));
490
+ const hasCodexCli = findOnPath("codex") !== null;
491
+ const hasCodex = hasCodexCli || existsSync(join(home, ".codex"));
423
492
  if (hasCodex) {
424
493
  const cfg = join(home, ".codex", "config.toml");
425
494
  const toml = existsSync(cfg) ? readFileSync(cfg, "utf8") : "";
426
495
  const tomlR = reconcileCodexToml(toml, p.server);
427
496
  const hj = readJson(join(home, ".codex", "hooks.json"), { hooks: {} });
428
497
  const hkR = reconcileCodexHooks(hj, `node "${p.codexHook}"`);
498
+ let registered = false;
499
+ let repaired = false;
500
+ if (hasCodexCli) {
501
+ registered = codexRegisteredViaCli();
502
+ if (!registered && repair) {
503
+ runCmd("codex", codexAddArgs(p.server));
504
+ repaired = true;
505
+ registered = codexRegisteredViaCli();
506
+ }
507
+ }
508
+ else {
509
+ registered = tomlR.status === "ok";
510
+ }
429
511
  results.push({
430
512
  label: "codex: config.toml MCP server block",
431
- ok: tomlR.status === "ok",
432
- detail: tomlR.status === "ok" ? "registered" : `${tomlR.status === "repaired" ? "stale path" : "not registered"} run: subagent-mcp setup`,
513
+ ok: registered,
514
+ detail: registered ? (repaired ? "repaired" : "registered") : "not registered; CLI repair failed",
433
515
  });
434
516
  const allOk = Object.values(hkR.statuses).every((s) => s === "ok");
435
517
  results.push({
436
518
  label: "codex: SessionStart + UserPromptSubmit hooks",
437
519
  ok: allOk,
438
- detail: allOk ? "wired (trust via /hooks in Codex)" : "incomplete run: subagent-mcp setup",
520
+ detail: allOk ? "wired (trust via /hooks in Codex)" : "incomplete - run: subagent-mcp setup",
439
521
  });
440
522
  }
441
- if (!hasClaude && !hasCodex) {
523
+ if (!hasClaude && !hasClaudeConfig && !hasCodex) {
442
524
  results.push({
443
525
  label: "vendors",
444
526
  ok: false,
@@ -483,7 +565,7 @@ export async function runSetup() {
483
565
  // Read-back verification: report what is ACTUALLY on disk now.
484
566
  if (!DRY_RUN) {
485
567
  console.log("\n--- Verification (read-back) ---");
486
- for (const r of verifyWiring()) {
568
+ for (const r of verifyWiring(INSTALL_ROOT, false)) {
487
569
  console.log(` ${r.ok ? "PASS" : "FAIL"} ${r.label} — ${r.detail}`);
488
570
  }
489
571
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heretyc/subagent-mcp",
3
- "version": "2.6.1",
3
+ "version": "2.6.2",
4
4
  "description": "MCP server that launches and manages local Claude Code and Codex CLI sub-agents as child processes (no direct Anthropic/OpenAI API).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",