@rubytech/create-maxy-code 0.1.392 → 0.1.394

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/__tests__/launchd-bootstrap.test.js +85 -0
  2. package/dist/__tests__/launchd-plist.test.js +41 -1
  3. package/dist/index.js +91 -19
  4. package/dist/launchd-bootstrap.js +57 -0
  5. package/dist/launchd-plist.js +27 -0
  6. package/dist/uninstall.js +56 -20
  7. package/package.json +1 -1
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -1
  9. package/payload/platform/services/claude-session-manager/dist/channel-mcp.d.ts +5 -0
  10. package/payload/platform/services/claude-session-manager/dist/channel-mcp.d.ts.map +1 -1
  11. package/payload/platform/services/claude-session-manager/dist/channel-mcp.js +1 -1
  12. package/payload/platform/services/claude-session-manager/dist/channel-mcp.js.map +1 -1
  13. package/payload/platform/services/claude-session-manager/dist/config.d.ts +6 -0
  14. package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
  15. package/payload/platform/services/claude-session-manager/dist/config.js +2 -0
  16. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  17. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  18. package/payload/platform/services/claude-session-manager/dist/http-server.js +8 -1
  19. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  20. package/payload/platform/services/claude-session-manager/dist/index.js +1 -0
  21. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  22. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +10 -0
  23. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  24. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +17 -2
  25. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  26. package/payload/platform/services/claude-session-manager/dist/session-cap-audit.d.ts +5 -0
  27. package/payload/platform/services/claude-session-manager/dist/session-cap-audit.d.ts.map +1 -1
  28. package/payload/platform/services/claude-session-manager/dist/session-cap-audit.js +22 -4
  29. package/payload/platform/services/claude-session-manager/dist/session-cap-audit.js.map +1 -1
  30. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +20 -0
  31. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -1
  32. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +18 -0
  33. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -1
  34. package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.d.ts +5 -0
  35. package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.d.ts.map +1 -1
  36. package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.js +1 -1
  37. package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.js.map +1 -1
  38. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts +5 -0
  39. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts.map +1 -1
  40. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js +1 -1
  41. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts +5 -0
  43. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts.map +1 -1
  44. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js +1 -1
  45. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js.map +1 -1
  46. package/payload/server/server.js +15 -0
@@ -0,0 +1,85 @@
1
+ // acceptance grid for launchd-bootstrap.ts (Task 1401).
2
+ //
3
+ // Locks the darwin session-manager bootstrap contract: bootout (idempotent,
4
+ // rc 3 "No such process" ignored) then bootstrap, with a bounded retry on
5
+ // exit 5 ("Input/output error" — a known transient/racey launchctl failure on
6
+ // re-install). Pure logic — the launchctl runner and the retry sleep are both
7
+ // injected, so no real launchctl or wall-clock delay runs here.
8
+ //
9
+ // Compiles to dist/__tests__/launchd-bootstrap.test.js so
10
+ // `node --test dist/__tests__/*.test.js` picks it up after build.
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { bootstrapLaunchAgent } from "../launchd-bootstrap.js";
14
+ const OPTS = { gui: "gui/502", label: "com.rubytech.host-claude-session-manager", plistPath: "/tmp/sm.plist" };
15
+ const noSleep = () => { };
16
+ test("first bootstrap succeeds — one bootout, one bootstrap, attempts=1", () => {
17
+ const calls = [];
18
+ const run = (args) => {
19
+ calls.push(args);
20
+ return args[0] === "bootstrap" ? { status: 0, stderr: "" } : { status: 3, stderr: "No such process" };
21
+ };
22
+ const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn: noSleep });
23
+ assert.equal(r.ok, true);
24
+ assert.equal(r.attempts, 1);
25
+ assert.deepEqual(calls[0], ["bootout", "gui/502/com.rubytech.host-claude-session-manager"]);
26
+ assert.deepEqual(calls[1], ["bootstrap", "gui/502", "/tmp/sm.plist"]);
27
+ assert.equal(calls.length, 2);
28
+ });
29
+ test("exit 5 twice then 0 — retries and reports the winning attempt", () => {
30
+ let bootstraps = 0;
31
+ const sleeps = [];
32
+ const run = (args) => {
33
+ if (args[0] !== "bootstrap")
34
+ return { status: 3, stderr: "No such process" };
35
+ bootstraps += 1;
36
+ return bootstraps < 3 ? { status: 5, stderr: "Bootstrap failed: 5: Input/output error" } : { status: 0, stderr: "" };
37
+ };
38
+ const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn: (ms) => { sleeps.push(ms); } });
39
+ assert.equal(r.ok, true);
40
+ assert.equal(r.attempts, 3);
41
+ assert.equal(bootstraps, 3);
42
+ assert.equal(sleeps.length, 2); // slept before each of the two retries
43
+ });
44
+ test("exit 5 on every attempt — fails after the attempt cap, not before", () => {
45
+ let bootstraps = 0;
46
+ const run = (args) => {
47
+ if (args[0] !== "bootstrap")
48
+ return { status: 3, stderr: "No such process" };
49
+ bootstraps += 1;
50
+ return { status: 5, stderr: "Bootstrap failed: 5: Input/output error" };
51
+ };
52
+ const r = bootstrapLaunchAgent(run, { ...OPTS, maxAttempts: 3, sleepFn: noSleep });
53
+ assert.equal(r.ok, false);
54
+ assert.equal(r.attempts, 3);
55
+ assert.equal(r.bootstrapStatus, 5);
56
+ assert.equal(bootstraps, 3);
57
+ });
58
+ test("non-5 non-zero bootstrap aborts immediately — no retry", () => {
59
+ let bootstraps = 0;
60
+ const run = (args) => {
61
+ if (args[0] !== "bootstrap")
62
+ return { status: 3, stderr: "No such process" };
63
+ bootstraps += 1;
64
+ return { status: 9, stderr: "some other launchd failure" };
65
+ };
66
+ const r = bootstrapLaunchAgent(run, { ...OPTS, maxAttempts: 3, sleepFn: noSleep });
67
+ assert.equal(r.ok, false);
68
+ assert.equal(r.attempts, 1);
69
+ assert.equal(r.bootstrapStatus, 9);
70
+ assert.equal(bootstraps, 1);
71
+ });
72
+ test("emits one structured log line per attempt with bootout and bootstrap rc", () => {
73
+ let bootstraps = 0;
74
+ const lines = [];
75
+ const run = (args) => {
76
+ if (args[0] !== "bootstrap")
77
+ return { status: 3, stderr: "No such process" };
78
+ bootstraps += 1;
79
+ return bootstraps < 2 ? { status: 5, stderr: "eio" } : { status: 0, stderr: "" };
80
+ };
81
+ bootstrapLaunchAgent(run, { ...OPTS, sleepFn: noSleep, logger: (l) => lines.push(l) });
82
+ assert.equal(lines.length, 2);
83
+ assert.match(lines[0], /agent=com\.rubytech\.host-claude-session-manager bootout=3 bootstrap=5 attempt=1/);
84
+ assert.match(lines[1], /bootout=3 bootstrap=0 attempt=2/);
85
+ });
@@ -10,7 +10,7 @@
10
10
  // package so `node --test dist/__tests__/*.test.js` picks it up after build.
11
11
  import test from "node:test";
12
12
  import assert from "node:assert/strict";
13
- import { renderPlist, buildSessionManagerWrapper } from "../launchd-plist.js";
13
+ import { renderPlist, buildSessionManagerWrapper, buildHeartbeatWrapper } from "../launchd-plist.js";
14
14
  // ---------------------------------------------------------------------------
15
15
  // Standard render — every required field present, no escaping needed.
16
16
  // ---------------------------------------------------------------------------
@@ -213,6 +213,46 @@ test("buildSessionManagerWrapper sources .env and execs the manager", () => {
213
213
  assert.match(w, /cd "\/Users\/x\/maxy-code\/platform\/services\/claude-session-manager"/);
214
214
  assert.match(w, /exec \/usr\/local\/bin\/node dist\/index\.js\n$/);
215
215
  });
216
+ // ---------------------------------------------------------------------------
217
+ // StartInterval — the darwin heartbeat timer (Task 1403). Emitted as
218
+ // <integer> only when set; absent from the standard server/manager plists.
219
+ // ---------------------------------------------------------------------------
220
+ test("renderPlist emits StartInterval as <integer> when set", () => {
221
+ const xml = renderPlist({
222
+ label: "com.rubytech.maxy-code-heartbeat",
223
+ programArguments: ["/bin/bash", "/Users/x/.maxy-code/heartbeat-wrapper.sh"],
224
+ stdoutPath: "/Users/x/.maxy-code/logs/check-due-events.log",
225
+ stderrPath: "/Users/x/.maxy-code/logs/check-due-events.log",
226
+ keepAlive: false,
227
+ runAtLoad: false,
228
+ startInterval: 60,
229
+ });
230
+ assert.match(xml, /<key>StartInterval<\/key>\s*<integer>60<\/integer>/);
231
+ assert.match(xml, /<key>KeepAlive<\/key>\s*<false\/>/);
232
+ assert.match(xml, /<key>RunAtLoad<\/key>\s*<false\/>/);
233
+ });
234
+ test("renderPlist omits StartInterval key when absent (standard plists unchanged)", () => {
235
+ const xml = renderPlist({
236
+ label: "com.rubytech.maxy-code",
237
+ programArguments: ["/bin/bash", "/Users/x/.maxy-code/launchd-wrapper.sh"],
238
+ stdoutPath: "/dev/null",
239
+ stderrPath: "/dev/null",
240
+ keepAlive: true,
241
+ runAtLoad: true,
242
+ });
243
+ assert.ok(!/StartInterval/.test(xml), "StartInterval must not appear when omitted");
244
+ });
245
+ test("buildHeartbeatWrapper sources .env, exports PLATFORM_ROOT, execs the tick", () => {
246
+ const w = buildHeartbeatWrapper({
247
+ envPath: "/Users/x/.maxy-code/.env",
248
+ installDir: "/Users/x/maxy-code",
249
+ nodeBin: "/opt/homebrew/bin/node",
250
+ });
251
+ assert.match(w, /^#!\/bin\/bash\n/);
252
+ assert.match(w, /set -a; \[ -f "\/Users\/x\/\.maxy-code\/\.env" \] && \. "\/Users\/x\/\.maxy-code\/\.env"; set \+a/);
253
+ assert.match(w, /export PLATFORM_ROOT="\/Users\/x\/maxy-code\/platform"/);
254
+ assert.match(w, /exec \/opt\/homebrew\/bin\/node \/Users\/x\/maxy-code\/platform\/plugins\/scheduling\/mcp\/dist\/scripts\/check-due-events\.js\n$/);
255
+ });
216
256
  test("session-manager plist renders bash wrapper as sole ProgramArguments", () => {
217
257
  const xml = renderPlist({
218
258
  label: "com.rubytech.maxy-code-claude-session-manager",
package/dist/index.js CHANGED
@@ -16,7 +16,8 @@ import { requireSupportedPlatform, detectPlatform } from "./platform-detect.js";
16
16
  import { decideClaudeUpgradePrivilege } from "./claude-upgrade-privilege.js";
17
17
  import { resolveNeo4jDataDir } from "./neo4j-datadir.js";
18
18
  import { protectCommands, unprotectCommands, lsattrShowsImmutable } from "./install-immutability.js";
19
- import { renderPlist, buildSessionManagerWrapper } from "./launchd-plist.js";
19
+ import { renderPlist, buildSessionManagerWrapper, buildHeartbeatWrapper } from "./launchd-plist.js";
20
+ import { bootstrapLaunchAgent } from "./launchd-bootstrap.js";
20
21
  import { enumerateClaudeCandidates, pickCanonicalClaude } from "./claude-bin.js";
21
22
  import { installAllBrewPackages } from "./brew-install.js";
22
23
  import { parseSwVers, isSupportedMacosVersion } from "./macos-version.js";
@@ -1898,7 +1899,16 @@ function capOllamaServiceCpu() {
1898
1899
  function installOllama(embedModel) {
1899
1900
  if (!commandExists("ollama")) {
1900
1901
  log("5", TOTAL, "Installing Ollama...");
1901
- spawnSync("bash", ["-c", "curl -fsSL https://ollama.ai/install.sh | sh"], { stdio: "inherit" });
1902
+ // The upstream curl|sh installer only places a Linux systemd daemon; on
1903
+ // darwin it leaves no `ollama` CLI, so ensureOllamaServing()'s `ollama serve`
1904
+ // spawn would fail. Install via Homebrew instead, mirroring installNeo4j /
1905
+ // installCloudflared (Task 1403).
1906
+ if (requireSupportedPlatform(process.platform) === "darwin") {
1907
+ installAllBrewPackages(["ollama"], logFile);
1908
+ }
1909
+ else {
1910
+ spawnSync("bash", ["-c", "curl -fsSL https://ollama.ai/install.sh | sh"], { stdio: "inherit" });
1911
+ }
1902
1912
  }
1903
1913
  else {
1904
1914
  log("5", TOTAL, "Ollama already installed.");
@@ -3300,6 +3310,15 @@ function installServiceDarwin() {
3300
3310
  `setupAccount() (setup-account.sh) should have created one. Refusing to write .env ` +
3301
3311
  `without ACCOUNT_ID; the boot validator would FATAL on every kickstart.`);
3302
3312
  }
3313
+ // Resolve node once, up front: the launchd wrapper execs it (below) and the
3314
+ // .env pins it as NODE_BIN so the session-manager's per-session channel MCP
3315
+ // descriptors invoke node by absolute path. The bare launchd PATH excludes an
3316
+ // nvm node, so a bare `node` command in an MCP descriptor fails to spawn and
3317
+ // the channel never boots (Task 1402). Intel `/usr/local/bin/node`, Apple
3318
+ // Silicon `/opt/homebrew/bin/node`, or nvm's versioned path — whatever the
3319
+ // installer's own shell resolves.
3320
+ const nodeProbe = spawnSync("command", ["-v", "node"], { encoding: "utf-8", shell: true });
3321
+ const nodeBin = (nodeProbe.stdout ?? "").trim() || "/usr/local/bin/node";
3303
3322
  const envPath = join(persistDir, ".env");
3304
3323
  try {
3305
3324
  let envContent = "";
@@ -3350,8 +3369,19 @@ function installServiceDarwin() {
3350
3369
  else
3351
3370
  envContent = envContent.trimEnd() + (envContent.length > 0 ? "\n" : "") + `CLAUDE_BIN=${canonicalClaude}\n`;
3352
3371
  }
3372
+ // Task 1402 — pin the resolved node the same way. The session-manager reads
3373
+ // NODE_BIN and stamps it as the `command` of every per-session channel MCP
3374
+ // descriptor, so a webchat/WhatsApp/Telegram channel spawns node by absolute
3375
+ // path instead of a bare `node` the launchd PATH can't resolve.
3376
+ {
3377
+ const re = /^NODE_BIN=.*$/m;
3378
+ if (re.test(envContent))
3379
+ envContent = envContent.replace(re, `NODE_BIN=${nodeBin}`);
3380
+ else
3381
+ envContent = envContent.trimEnd() + (envContent.length > 0 ? "\n" : "") + `NODE_BIN=${nodeBin}\n`;
3382
+ }
3353
3383
  writeFileSync(envPath, envContent);
3354
- logFile(` .env: DISPLAY_MODE=${DISPLAY_MODE}, EMBED_MODEL=${EMBED_MODEL}, EMBED_DIMENSIONS=${EMBED_DIMS}, NEO4J_URI=bolt://localhost:${NEO4J_PORT}, PORT=${PORT}, MAXY_UI_INTERNAL_PORT=${PORT} (darwin-collapsed), CLAUDE_SESSION_MANAGER_PORT=${BRAND.claudeSessionManagerPort}, HOSTNAME=0.0.0.0, ACCOUNT_ID=${installAccountId.slice(0, 8)}…, CLAUDE_CONFIG_DIR=${persistDir}/.claude, CLAUDE_BIN=${canonicalClaude ?? "unresolved(PATH)"}`);
3384
+ logFile(` .env: DISPLAY_MODE=${DISPLAY_MODE}, EMBED_MODEL=${EMBED_MODEL}, EMBED_DIMENSIONS=${EMBED_DIMS}, NEO4J_URI=bolt://localhost:${NEO4J_PORT}, PORT=${PORT}, MAXY_UI_INTERNAL_PORT=${PORT} (darwin-collapsed), CLAUDE_SESSION_MANAGER_PORT=${BRAND.claudeSessionManagerPort}, HOSTNAME=0.0.0.0, ACCOUNT_ID=${installAccountId.slice(0, 8)}…, CLAUDE_CONFIG_DIR=${persistDir}/.claude, CLAUDE_BIN=${canonicalClaude ?? "unresolved(PATH)"}, NODE_BIN=${nodeBin}`);
3355
3385
  }
3356
3386
  catch (err) {
3357
3387
  console.error(` WARNING: failed to write .env to ${envPath}: ${err instanceof Error ? err.message : String(err)}`);
@@ -3361,11 +3391,7 @@ function installServiceDarwin() {
3361
3391
  // in the child env. Without this, ProgramArguments executes node directly
3362
3392
  // and the .env values are unread — server binds the wrong port.
3363
3393
  const wrapperPath = join(persistDir, "launchd-wrapper.sh");
3364
- // Resolve node binary at install time so the wrapper picks the right
3365
- // path on both Intel (/usr/local/bin/node) and Apple Silicon
3366
- // (/opt/homebrew/bin/node) — Homebrew's prefix differs by arch.
3367
- const nodeProbe = spawnSync("command", ["-v", "node"], { encoding: "utf-8", shell: true });
3368
- const nodeBin = (nodeProbe.stdout ?? "").trim() || "/usr/local/bin/node";
3394
+ // `nodeBin` resolved once above (also pinned as NODE_BIN in .env).
3369
3395
  const wrapperBody = [
3370
3396
  "#!/bin/bash",
3371
3397
  "# generated by create-maxy installService(). Reads.env then",
@@ -3441,21 +3467,28 @@ function installServiceDarwin() {
3441
3467
  });
3442
3468
  writeFileSync(smPlistPath, smPlist);
3443
3469
  logFile(` ${smPlistPath} written (${smPlist.length} bytes)`);
3444
- spawnSync("launchctl", ["bootout", `${gui()}/${smLabel}`], { stdio: "pipe" });
3445
- const smBootstrap = spawnSync("launchctl", ["bootstrap", gui(), smPlistPath], {
3446
- stdio: "pipe",
3447
- encoding: "utf-8",
3448
- timeout: 15_000,
3470
+ // Task 1401 — `launchctl bootstrap` intermittently returns exit 5 ("Input/
3471
+ // output error") on a re-install even when the label is not loaded (its
3472
+ // bootout returns 3). A single bare bootstrap aborted phase 11 on that first
3473
+ // EIO; bootstrapLaunchAgent bootout-then-bootstraps with a bounded retry on
3474
+ // exit 5, so a transient failure that clears on retry no longer fails install.
3475
+ const smBoot = bootstrapLaunchAgent((args) => {
3476
+ const r = spawnSync("launchctl", args, { stdio: "pipe", encoding: "utf-8", timeout: 15_000 });
3477
+ return { status: r.status, stderr: (r.stderr ?? "").toString() };
3478
+ }, {
3479
+ gui: gui(),
3480
+ label: smLabel,
3481
+ plistPath: smPlistPath,
3482
+ logger: (line) => { console.log(` ${line}`); logFile(` ${line}`); },
3449
3483
  });
3450
- if (smBootstrap.status === 0) {
3484
+ if (smBoot.ok) {
3451
3485
  console.log(` [launchd] bootstrap ${gui()}/${smLabel} ok`);
3452
- logFile(` [create-maxy] launchd-plist=${smLabel} loaded=true`);
3486
+ logFile(` [create-maxy] launchd-plist=${smLabel} loaded=true attempts=${smBoot.attempts}`);
3453
3487
  }
3454
3488
  else {
3455
- const smStderr = (smBootstrap.stderr ?? "").trim();
3456
- console.error(` [launchd] bootstrap ${smLabel} returned ${smBootstrap.status}: ${smStderr}`);
3457
- logFile(` [create-maxy] launchd-plist=${smLabel} loaded=false exit=${smBootstrap.status}`);
3458
- throw new Error(`launchctl bootstrap ${smLabel} failed (exit ${smBootstrap.status}): ${smStderr}`);
3489
+ console.error(` [launchd] bootstrap ${smLabel} returned ${smBoot.bootstrapStatus} after ${smBoot.attempts} attempt(s): ${smBoot.stderr}`);
3490
+ logFile(` [create-maxy] launchd-plist=${smLabel} loaded=false exit=${smBoot.bootstrapStatus} attempts=${smBoot.attempts}`);
3491
+ throw new Error(`launchctl bootstrap ${smLabel} failed (exit ${smBoot.bootstrapStatus}) after ${smBoot.attempts} attempt(s): ${smBoot.stderr}`);
3459
3492
  }
3460
3493
  // Health-probe the manager (/healthz) so a silent boot failure surfaces at
3461
3494
  // install time, not at first session spawn as `manager-unreachable`.
@@ -3473,6 +3506,45 @@ function installServiceDarwin() {
3473
3506
  if (!smUp) {
3474
3507
  console.log(` Session manager not yet healthy on ${BRAND.claudeSessionManagerPort} — check ${join(logsDir, "server.log")}.`);
3475
3508
  }
3509
+ // Task 1403 — the scheduling heartbeat. On Linux a per-minute cron ticks
3510
+ // check-due-events.js (installCrons/buildHeartbeatCronBlock); darwin has no
3511
+ // cron and installCrons early-returns, so operator-created bookings would
3512
+ // never dispatch. Author a StartInterval=60 LaunchAgent as the timer analogue:
3513
+ // keepAlive=false (a periodic one-shot, not a supervised daemon). A bootstrap
3514
+ // failure warns rather than aborting install — the server + manager (core
3515
+ // function) are already up; scheduling retries on the next installer run.
3516
+ const hbLabel = `${launchdLabel()}-heartbeat`;
3517
+ const hbPlistPath = join(launchAgentsDir(), `${hbLabel}.plist`);
3518
+ const hbWrapperPath = join(persistDir, "heartbeat-wrapper.sh");
3519
+ writeFileSync(hbWrapperPath, buildHeartbeatWrapper({ envPath, installDir: INSTALL_DIR, nodeBin }));
3520
+ chmodSync(hbWrapperPath, 0o755);
3521
+ const hbPlist = renderPlist({
3522
+ label: hbLabel,
3523
+ programArguments: ["/bin/bash", hbWrapperPath],
3524
+ stdoutPath: join(logsDir, "check-due-events.log"),
3525
+ stderrPath: join(logsDir, "check-due-events.log"),
3526
+ keepAlive: false,
3527
+ runAtLoad: false,
3528
+ startInterval: 60,
3529
+ });
3530
+ writeFileSync(hbPlistPath, hbPlist);
3531
+ logFile(` ${hbPlistPath} written (${hbPlist.length} bytes)`);
3532
+ const hbBoot = bootstrapLaunchAgent((args) => {
3533
+ const r = spawnSync("launchctl", args, { stdio: "pipe", encoding: "utf-8", timeout: 15_000 });
3534
+ return { status: r.status, stderr: (r.stderr ?? "").toString() };
3535
+ }, {
3536
+ gui: gui(),
3537
+ label: hbLabel,
3538
+ plistPath: hbPlistPath,
3539
+ logger: (line) => { console.log(` ${line}`); logFile(` ${line}`); },
3540
+ });
3541
+ if (hbBoot.ok) {
3542
+ logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=true attempts=${hbBoot.attempts}`);
3543
+ }
3544
+ else {
3545
+ console.error(` WARNING: heartbeat LaunchAgent ${hbLabel} bootstrap returned ${hbBoot.bootstrapStatus} after ${hbBoot.attempts} attempt(s): ${hbBoot.stderr} — scheduling ticks will not fire until a re-install succeeds.`);
3546
+ logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=false exit=${hbBoot.bootstrapStatus} attempts=${hbBoot.attempts}`);
3547
+ }
3476
3548
  // Wait for the server to come up.
3477
3549
  console.log(" Waiting for web server...");
3478
3550
  let webServerUp = false;
@@ -0,0 +1,57 @@
1
+ // Task 1401 — darwin session-manager LaunchAgent bootstrap, made idempotent and
2
+ // EIO-tolerant.
3
+ //
4
+ // `launchctl bootstrap` intermittently returns exit 5 ("Bootstrap failed: 5:
5
+ // Input/output error") on a re-install even when the target label is not
6
+ // loaded (a `bootout` of it returns 3, "No such process"). It is a known
7
+ // transient/racey launchd failure: bootstrapping the same plist from a clean
8
+ // state succeeds. The earlier phase-11 code issued a single bare `bootstrap`
9
+ // and treated any non-zero exit as fatal, so one EIO aborted the whole install.
10
+ //
11
+ // This helper bootout-then-bootstraps with a bounded retry on exit 5. Both the
12
+ // launchctl runner and the retry sleep are injected, so the logic is unit-tested
13
+ // with no real launchctl and no wall-clock delay. Kept synchronous to match the
14
+ // spawnSync-based installer body (installService is a synchronous `void`).
15
+ /** `launchctl bootstrap` exit code for "Input/output error" — the transient,
16
+ * racey failure this retry loop exists to absorb. */
17
+ const BOOTSTRAP_EIO = 5;
18
+ /** Blocking sleep with no imports, safe inside the synchronous installer. */
19
+ function blockingSleep(ms) {
20
+ if (ms <= 0)
21
+ return;
22
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23
+ }
24
+ export function bootstrapLaunchAgent(run, o) {
25
+ const maxAttempts = Math.max(1, o.maxAttempts ?? 3);
26
+ const retryDelayMs = o.retryDelayMs ?? 500;
27
+ const sleepFn = o.sleepFn ?? blockingSleep;
28
+ const log = o.logger ?? (() => { });
29
+ let attempt = 0;
30
+ let last = { status: null, stderr: "" };
31
+ for (attempt = 1; attempt <= maxAttempts; attempt++) {
32
+ // Idempotent: bootout any prior instance so the following bootstrap starts
33
+ // clean. rc 3 ("No such process") is the expected fresh case; it and any
34
+ // other bootout rc are ignored — bootout is cleanup, not the verified op.
35
+ const bootout = run(["bootout", `${o.gui}/${o.label}`]);
36
+ const bootstrap = run(["bootstrap", o.gui, o.plistPath]);
37
+ last = { status: bootstrap.status, stderr: (bootstrap.stderr ?? "").trim() };
38
+ log(`[launchd] agent=${o.label} bootout=${bootout.status ?? "null"} bootstrap=${bootstrap.status ?? "null"} attempt=${attempt}`);
39
+ if (bootstrap.status === 0) {
40
+ return { ok: true, attempts: attempt, bootstrapStatus: 0, stderr: last.stderr };
41
+ }
42
+ // Only exit 5 (EIO) is retried, and only while attempts remain. Every other
43
+ // non-zero status is a real failure that aborts now rather than burning the
44
+ // retry budget on a deterministic error.
45
+ if (bootstrap.status === BOOTSTRAP_EIO && attempt < maxAttempts) {
46
+ sleepFn(retryDelayMs);
47
+ continue;
48
+ }
49
+ break;
50
+ }
51
+ return {
52
+ ok: false,
53
+ attempts: Math.min(attempt, maxAttempts),
54
+ bootstrapStatus: last.status,
55
+ stderr: last.stderr,
56
+ };
57
+ }
@@ -52,6 +52,29 @@ export function buildSessionManagerWrapper(o) {
52
52
  "",
53
53
  ].join("\n");
54
54
  }
55
+ /**
56
+ * Bash wrapper for the darwin scheduling-heartbeat LaunchAgent (Task 1403).
57
+ * launchd's StartInterval timer is the analogue of the Linux per-minute cron
58
+ * (buildHeartbeatCronBlock) that ticks `check-due-events.js`. The wrapper
59
+ * sources the brand `.env` for NEO4J_URI + MAXY_UI_INTERNAL_PORT, exports
60
+ * PLATFORM_ROOT (the tick's own env contract, matching the cron line's
61
+ * `PLATFORM_ROOT=` prefix), and execs the tick once. launchd re-invokes it
62
+ * every StartInterval seconds; there is no long-running process to supervise.
63
+ */
64
+ export function buildHeartbeatWrapper(o) {
65
+ const script = `${o.installDir}/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js`;
66
+ return [
67
+ "#!/bin/bash",
68
+ "# generated by create-maxy installServiceDarwin() — Task 1403 heartbeat.",
69
+ "# launchd StartInterval analogue of the Linux per-minute scheduling cron.",
70
+ "# Sources .env (NEO4J_URI, MAXY_UI_INTERNAL_PORT), exports PLATFORM_ROOT,",
71
+ "# execs one check-due-events.js tick.",
72
+ `set -a; [ -f "${o.envPath}" ] && . "${o.envPath}"; set +a`,
73
+ `export PLATFORM_ROOT="${o.installDir}/platform"`,
74
+ `exec ${o.nodeBin} ${script}`,
75
+ "",
76
+ ].join("\n");
77
+ }
55
78
  export function renderPlist(spec) {
56
79
  const lines = [];
57
80
  lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
@@ -78,6 +101,10 @@ export function renderPlist(spec) {
78
101
  lines.push(` ${spec.keepAlive ? "<true/>" : "<false/>"}`);
79
102
  lines.push(` <key>RunAtLoad</key>`);
80
103
  lines.push(` ${spec.runAtLoad ? "<true/>" : "<false/>"}`);
104
+ if (spec.startInterval !== undefined) {
105
+ lines.push(` <key>StartInterval</key>`);
106
+ lines.push(` <integer>${spec.startInterval}</integer>`);
107
+ }
81
108
  lines.push(`</dict>`);
82
109
  lines.push(`</plist>`);
83
110
  return lines.join("\n") + "\n";
package/dist/uninstall.js CHANGED
@@ -78,8 +78,21 @@ function isDarwin() {
78
78
  function launchdLabel() {
79
79
  return `com.rubytech.${BRAND.hostname}`;
80
80
  }
81
+ /** Every LaunchAgent label installServiceDarwin() creates for this brand: the
82
+ * main server, the claude-session-manager (Task 1395), and the scheduling
83
+ * heartbeat (Task 1403). Uninstall boots out and removes all three. */
84
+ function darwinLaunchdLabels() {
85
+ return [
86
+ launchdLabel(),
87
+ `${launchdLabel()}-claude-session-manager`,
88
+ `${launchdLabel()}-heartbeat`,
89
+ ];
90
+ }
91
+ function plistPathFor(label) {
92
+ return resolve(HOME, "Library/LaunchAgents", `${label}.plist`);
93
+ }
81
94
  function plistPath() {
82
- return resolve(HOME, "Library/LaunchAgents", `${launchdLabel()}.plist`);
95
+ return plistPathFor(launchdLabel());
83
96
  }
84
97
  function gui() {
85
98
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
@@ -163,13 +176,18 @@ function peerBrandPresent() {
163
176
  // ---------------------------------------------------------------------------
164
177
  function stopServices() {
165
178
  log("1", "Stopping services...");
166
- // darwin: bootout the LaunchAgent. bootout exits 0 on success,
167
- // 113 ("Unknown service") when the agent isn't loaded — both are
179
+ // darwin: bootout every LaunchAgent this install created. bootout exits 0 on
180
+ // success, 113 ("Unknown service") when the agent isn't loaded — both are
168
181
  // acceptable end-states for "service stopped". The plist removal in
169
- // removeSystemdService() finishes the cleanup.
182
+ // removeSystemdService() finishes the cleanup. Three labels share the brand
183
+ // prefix: the main server, the claude-session-manager (Task 1395), and the
184
+ // scheduling heartbeat (Task 1403). The latter two were previously never
185
+ // booted out on uninstall.
170
186
  if (isDarwin()) {
171
- spawnSync("launchctl", ["bootout", `${gui()}/${launchdLabel()}`], { stdio: "pipe", timeout: 15_000 });
172
- console.log(` Booted out ${launchdLabel()}`);
187
+ for (const label of darwinLaunchdLabels()) {
188
+ spawnSync("launchctl", ["bootout", `${gui()}/${label}`], { stdio: "pipe", timeout: 15_000 });
189
+ console.log(` Booted out ${label}`);
190
+ }
173
191
  return;
174
192
  }
175
193
  // Stop platform user service
@@ -465,6 +483,14 @@ function removeAppDirs() {
465
483
  // ---------------------------------------------------------------------------
466
484
  function removeNeo4jData() {
467
485
  log("5", "Removing Neo4j data...");
486
+ // This function's teardown is Linux-shaped: `sudo systemctl daemon-reload`
487
+ // and `sudo rm -rf /var/lib/neo4j/*`. On darwin that raises a spurious sudo
488
+ // prompt and logs a FAILED. Skip here (Task 1403); darwin Neo4j data teardown
489
+ // belongs to Task 1396 (dedicated Neo4j on darwin).
490
+ if (!isLinux()) {
491
+ console.log(" Not Linux — skipping.");
492
+ return;
493
+ }
468
494
  // Brand isolation: remove only THIS brand's Neo4j data. Shared
469
495
  // 7687 data is skipped entirely when a peer brand is present. Dedicated
470
496
  // branded instances live at /var/lib/neo4j-<hostname>/ and are always
@@ -666,24 +692,26 @@ function removeSystemConfig() {
666
692
  function removeSystemdService() {
667
693
  log("8", "Removing systemd service...");
668
694
  // darwin: bootout (idempotent — already done in stopServices,
669
- // re-running is a no-op exit 113) and delete the plist. The wrapper
670
- // shell script lives in CONFIG_DIR which step 4 (removeAppDirs) wipes,
671
- // so no separate cleanup is needed.
695
+ // re-running is a no-op exit 113) and delete the plist for every LaunchAgent
696
+ // this brand created. The wrapper shell scripts live in CONFIG_DIR which
697
+ // step 4 (removeAppDirs) wipes, so no separate cleanup is needed.
672
698
  if (isDarwin()) {
673
- spawnSync("launchctl", ["bootout", `${gui()}/${launchdLabel()}`], { stdio: "pipe", timeout: 15_000 });
674
- const plist = plistPath();
675
- if (existsSync(plist)) {
676
- try {
677
- rmSync(plist);
678
- console.log(` Removed ${plist}`);
699
+ for (const label of darwinLaunchdLabels()) {
700
+ spawnSync("launchctl", ["bootout", `${gui()}/${label}`], { stdio: "pipe", timeout: 15_000 });
701
+ const plist = plistPathFor(label);
702
+ if (existsSync(plist)) {
703
+ try {
704
+ rmSync(plist);
705
+ console.log(` Removed ${plist}`);
706
+ }
707
+ catch (err) {
708
+ console.log(` Failed to remove ${plist}: ${err instanceof Error ? err.message : String(err)}`);
709
+ }
679
710
  }
680
- catch (err) {
681
- console.log(` Failed to remove ${plist}: ${err instanceof Error ? err.message : String(err)}`);
711
+ else {
712
+ console.log(` ${plist} not found skipping`);
682
713
  }
683
714
  }
684
- else {
685
- console.log(` ${plist} not found — skipping`);
686
- }
687
715
  return;
688
716
  }
689
717
  // Disable the service
@@ -737,6 +765,14 @@ function removeSystemdService() {
737
765
  // ---------------------------------------------------------------------------
738
766
  function removeOllama() {
739
767
  log("9", "Removing Ollama...");
768
+ // The stop/disable below is `sudo systemctl` — Linux-only. On darwin it would
769
+ // raise a spurious sudo prompt and log a FAILED. Skip like removeSamba /
770
+ // removeSystemConfig (Task 1403); brew-ollama teardown on darwin is out of
771
+ // scope here.
772
+ if (!isLinux()) {
773
+ console.log(" Not Linux — skipping.");
774
+ return;
775
+ }
740
776
  // Brand isolation: the Ollama binary and systemd service are
741
777
  // device-wide singletons — every brand on this device shares one Ollama
742
778
  // process. Leave both in place when a peer brand is still installed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.392",
3
+ "version": "0.1.394",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:11f2a3458fb6227490d33fcf54f859595c215184dbb78051484df353b6fbf227
4
+ content-hash: sha256:bbee5596607064b556bd814bce3ebe7578ae948a7117ee0dd6bff5e5fbe4b209
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -620,6 +620,8 @@ cat ~/Library/LaunchAgents/com.rubytech.maxy-code.plist
620
620
 
621
621
  The plist points at the wrapper script the installer wrote and at log files under `$HOME/.maxy-code/logs/`. `launchctl bootstrap`'s exit code is recorded in the install log as `[create-maxy] launchd-plist=… loaded=true|false`.
622
622
 
623
+ Each brand runs two LaunchAgents: the main service (`com.rubytech.maxy-code`) and a session manager (`com.rubytech.maxy-code-claude-session-manager`). On a re-install `launchctl bootstrap` of the session-manager agent can return a transient `exit 5: Input/output error` even when the agent was not already loaded; the installer boots the agent out and retries automatically. Each attempt is logged as `[launchd] agent=<label> bootout=<rc> bootstrap=<rc> attempt=<n>`, and only an EIO that persists across every attempt aborts the install.
624
+
623
625
  ## 4. Open the admin UI
624
626
 
625
627
  The install log's final block prints the URL. For the default brand on a default install:
@@ -8,6 +8,11 @@ export interface ChannelMcpServerEntry {
8
8
  export declare function buildChannelMcpServers(p: {
9
9
  sessionId: string;
10
10
  targets: ChannelTargetSet;
11
+ /** Absolute path to the node binary that runs the channel server. On darwin
12
+ * the launchd server PATH excludes an nvm node, so a bare `node` command
13
+ * fails to spawn and the channel never boots (Task 1402); callers pass the
14
+ * installer-resolved NODE_BIN. */
15
+ nodeBin: string;
11
16
  }): Record<string, ChannelMcpServerEntry>;
12
17
  export declare function channelDevChannelsArgv(): string[];
13
18
  export declare function channelMcpConfigPath(sessionId: string, dir: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"channel-mcp.d.ts","sourceRoot":"","sources":["../src/channel-mcp.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,eAAO,MAAM,mBAAmB,YAAY,CAAA;AAE5C,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC5B;AAED,wBAAgB,sBAAsB,CAAC,CAAC,EAAE;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,gBAAgB,CAAA;CAC1B,GAAG,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAexC;AAED,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAEhE"}
1
+ {"version":3,"file":"channel-mcp.d.ts","sourceRoot":"","sources":["../src/channel-mcp.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,eAAO,MAAM,mBAAmB,YAAY,CAAA;AAE5C,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC5B;AAED,wBAAgB,sBAAsB,CAAC,CAAC,EAAE;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,gBAAgB,CAAA;IACzB;;;uCAGmC;IACnC,OAAO,EAAE,MAAM,CAAA;CAChB,GAAG,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAexC;AAED,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAEhE"}
@@ -13,7 +13,7 @@ export function buildChannelMcpServers(p) {
13
13
  throw new Error('buildChannelMcpServers: empty target set');
14
14
  return {
15
15
  [CHANNEL_SERVER_NAME]: {
16
- command: 'node',
16
+ command: p.nodeBin,
17
17
  args: [anyTarget.serverPath],
18
18
  env: {
19
19
  CHANNEL_SESSION_ID: p.sessionId,
@@ -1 +1 @@
1
- {"version":3,"file":"channel-mcp.js","sourceRoot":"","sources":["../src/channel-mcp.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,+EAA+E;AAC/E,+EAA+E;AAC/E,6EAA6E;AAC7E,iFAAiF;AAEjF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAGhC,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAA;AAQ5C,MAAM,UAAU,sBAAsB,CAAC,CAGtC;IACC,mEAAmE;IACnE,wDAAwD;IACxD,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,OAAO,CAAC,OAAO,CAAA;IACzD,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;IAC3E,OAAO;QACL,CAAC,mBAAmB,CAAC,EAAE;YACrB,OAAO,EAAE,MAAM;YACf,IAAI,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC;YAC5B,GAAG,EAAE;gBACH,kBAAkB,EAAE,CAAC,CAAC,SAAS;gBAC/B,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;aAC3C;SACF;KACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO,CAAC,yCAAyC,EAAE,UAAU,mBAAmB,EAAE,CAAC,CAAA;AACrF,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE,GAAW;IACjE,OAAO,IAAI,CAAC,GAAG,EAAE,gBAAgB,SAAS,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;AACpF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,aAAqB;IACpD,OAAO,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,sBAAsB,EAAE,CAAC,CAAA;AACrE,CAAC"}
1
+ {"version":3,"file":"channel-mcp.js","sourceRoot":"","sources":["../src/channel-mcp.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,+EAA+E;AAC/E,+EAA+E;AAC/E,6EAA6E;AAC7E,iFAAiF;AAEjF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAGhC,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAA;AAQ5C,MAAM,UAAU,sBAAsB,CAAC,CAQtC;IACC,mEAAmE;IACnE,wDAAwD;IACxD,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,OAAO,CAAC,OAAO,CAAA;IACzD,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;IAC3E,OAAO;QACL,CAAC,mBAAmB,CAAC,EAAE;YACrB,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,IAAI,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC;YAC5B,GAAG,EAAE;gBACH,kBAAkB,EAAE,CAAC,CAAC,SAAS;gBAC/B,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;aAC3C;SACF;KACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO,CAAC,yCAAyC,EAAE,UAAU,mBAAmB,EAAE,CAAC,CAAA;AACrF,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE,GAAW;IACjE,OAAO,IAAI,CAAC,GAAG,EAAE,gBAAgB,SAAS,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;AACpF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,aAAqB;IACpD,OAAO,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,sBAAsB,EAAE,CAAC,CAAA;AACrE,CAAC"}
@@ -10,6 +10,12 @@ export interface ManagerConfig {
10
10
  port: number;
11
11
  persistDir: string;
12
12
  claudeBin: string;
13
+ /** Absolute node binary for spawned channel-MCP children. On darwin the
14
+ * launchd server PATH excludes an nvm node, so channel descriptors must name
15
+ * node explicitly rather than relying on a bare `node` on PATH (Task 1402).
16
+ * Falls back to bare `node` when NODE_BIN is unset (Linux, where node is on
17
+ * the base PATH). */
18
+ nodeBin: string;
13
19
  spawnCwd: string;
14
20
  urlCaptureTimeoutMs: number;
15
21
  killGraceMs: number;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,yBAAyB,CAAA;AAGhC,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD;;4DAEwD;IACxD,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB;;4EAEwE;IACxE,aAAa,EAAE,MAAM,CAAA;IACrB;;;gEAG4D;IAC5D,eAAe,EAAE,MAAM,CAAA;IACvB;;2EAEuE;IACvE,WAAW,EAAE,MAAM,CAAA;IACnB;;0EAEsE;IACtE,iBAAiB,EAAE,MAAM,CAAA;IACzB,IAAI,EAAE,cAAc,CAAA;IACpB;;;;;;;8BAO0B;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB;;;qDAGiD;IACjD,iBAAiB,EAAE,iBAAiB,CAAA;IACpC;;gFAE4E;IAC5E,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAwLD;;;;;;;6EAO6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,CAyBvE;AAED;;eAEe;AACf,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAE9D;AAED;;;;;;;kCAOkC;AAClC,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI/F;AAUD,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,aAAa,CAuE9E"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,yBAAyB,CAAA;AAGhC,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD;;4DAEwD;IACxD,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB;;;;0BAIsB;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB;;4EAEwE;IACxE,aAAa,EAAE,MAAM,CAAA;IACrB;;;gEAG4D;IAC5D,eAAe,EAAE,MAAM,CAAA;IACvB;;2EAEuE;IACvE,WAAW,EAAE,MAAM,CAAA;IACnB;;0EAEsE;IACtE,iBAAiB,EAAE,MAAM,CAAA;IACzB,IAAI,EAAE,cAAc,CAAA;IACpB;;;;;;;8BAO0B;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB;;;qDAGiD;IACjD,iBAAiB,EAAE,iBAAiB,CAAA;IACpC;;gFAE4E;IAC5E,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAwLD;;;;;;;6EAO6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,CAyBvE;AAED;;eAEe;AACf,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAE9D;AAED;;;;;;;kCAOkC;AAClC,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI/F;AAUD,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,aAAa,CAyE9E"}
@@ -241,6 +241,7 @@ export function loadConfig(env = process.env) {
241
241
  }
242
242
  const persistDir = env.CLAUDE_SESSION_MANAGER_PERSIST_DIR ?? join(homedir(), '.maxy');
243
243
  const claudeBin = env.CLAUDE_BIN ?? 'claude';
244
+ const nodeBin = env.NODE_BIN ?? 'node';
244
245
  const brand = loadBrand(env);
245
246
  const platformRoot = env.PLATFORM_ROOT ?? env.MAXY_PLATFORM_ROOT;
246
247
  if (!platformRoot) {
@@ -281,6 +282,7 @@ export function loadConfig(env = process.env) {
281
282
  port,
282
283
  persistDir,
283
284
  claudeBin,
285
+ nodeBin,
284
286
  spawnCwd,
285
287
  urlCaptureTimeoutMs: DEFAULT_URL_TIMEOUT_MS,
286
288
  killGraceMs: DEFAULT_KILL_GRACE_MS,