@haven_ai/connect 0.1.2-alpha → 0.1.4-alpha

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/cli.cjs CHANGED
@@ -208,14 +208,156 @@ async function restrictPermissions(path, mode, warn) {
208
208
  );
209
209
  }
210
210
  }
211
+ var MCP_RUNTIME_MANIFEST = {
212
+ mcpPackage: "@haven_ai/mcp",
213
+ mcpVersion: mcp.MCP_VERSION,
214
+ sdkPackage: "@haven_ai/sdk",
215
+ sdkVersion: "0.1.7",
216
+ signerPackage: "@haven_ai/signer",
217
+ signerVersion: "0.1.0-alpha",
218
+ minimumNodeVersion: "20.0.0",
219
+ supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
220
+ requiredTools: mcp.registeredToolNames()
221
+ };
222
+ function mcpPackageSpec() {
223
+ return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
224
+ }
225
+ function sdkPackageSpec() {
226
+ return `${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}`;
227
+ }
228
+ function signerPackageSpec() {
229
+ return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
230
+ }
231
+
232
+ // src/runtime-registry.ts
233
+ var RUNTIME_PROFILES = {
234
+ "claude-code": {
235
+ id: "claude-code",
236
+ label: "Claude Code",
237
+ restartMode: "restart-session",
238
+ canWriteRuntimeConfig: true
239
+ },
240
+ "codex-cli": {
241
+ id: "codex-cli",
242
+ label: "Codex CLI",
243
+ restartMode: "restart-session",
244
+ canWriteRuntimeConfig: true
245
+ },
246
+ "codex-desktop": {
247
+ id: "codex-desktop",
248
+ label: "Codex Desktop",
249
+ restartMode: "restart-session",
250
+ canWriteRuntimeConfig: true
251
+ },
252
+ cursor: {
253
+ id: "cursor",
254
+ label: "Cursor",
255
+ restartMode: "hot-reload",
256
+ canWriteRuntimeConfig: true
257
+ },
258
+ vscode: {
259
+ id: "vscode",
260
+ label: "VS Code",
261
+ restartMode: "hot-reload",
262
+ canWriteRuntimeConfig: true
263
+ },
264
+ "vscode-insiders": {
265
+ id: "vscode-insiders",
266
+ label: "VS Code Insiders",
267
+ restartMode: "hot-reload",
268
+ canWriteRuntimeConfig: true
269
+ },
270
+ "claude-desktop": {
271
+ id: "claude-desktop",
272
+ label: "Claude Desktop",
273
+ restartMode: "restart-app",
274
+ canWriteRuntimeConfig: true
275
+ },
276
+ other: {
277
+ id: "other",
278
+ label: "Other agent runtime",
279
+ restartMode: "manual",
280
+ canWriteRuntimeConfig: false
281
+ }
282
+ };
283
+ var RUNTIME_ALIASES = {
284
+ claude: "claude-code",
285
+ "claude-code": "claude-code",
286
+ claudecode: "claude-code",
287
+ "claude_code": "claude-code",
288
+ codex: "codex-cli",
289
+ "codex-cli": "codex-cli",
290
+ codexcli: "codex-cli",
291
+ "codex_cli": "codex-cli",
292
+ "codex-desktop": "codex-desktop",
293
+ "codex_desktop": "codex-desktop",
294
+ codexdesktop: "codex-desktop",
295
+ "codex-app": "codex-desktop",
296
+ "codex_app": "codex-desktop",
297
+ codexapp: "codex-desktop",
298
+ cursor: "cursor",
299
+ vscode: "vscode",
300
+ "vs-code": "vscode",
301
+ "vs_code": "vscode",
302
+ code: "vscode",
303
+ "vscode-insiders": "vscode-insiders",
304
+ "vscode_insiders": "vscode-insiders",
305
+ vscodeinsiders: "vscode-insiders",
306
+ "vs-code-insiders": "vscode-insiders",
307
+ "code-insiders": "vscode-insiders",
308
+ insiders: "vscode-insiders",
309
+ "claude-desktop": "claude-desktop",
310
+ "claude_desktop": "claude-desktop",
311
+ claudesktop: "claude-desktop",
312
+ desktop: "claude-desktop",
313
+ other: "other",
314
+ manual: "other"
315
+ };
316
+ function runtimeProfile(runtime, env = process.env) {
317
+ return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
318
+ }
319
+ function normalizeRuntime(runtime, env = process.env) {
320
+ const explicit = normalizeRuntimeName(runtime);
321
+ if (explicit) return explicit;
322
+ return detectRuntime(env) ?? "other";
323
+ }
324
+ function restartRequiredForRuntime(runtime, env = process.env) {
325
+ const mode = runtimeProfile(runtime, env).restartMode;
326
+ return mode === "restart-session" || mode === "restart-app";
327
+ }
328
+ function runtimeRequiresHardRestart(runtime) {
329
+ return runtime === "claude-desktop" || runtime === "codex-desktop";
330
+ }
331
+ function normalizeRuntimeName(runtime) {
332
+ const key = runtime?.trim().toLowerCase();
333
+ if (!key) return null;
334
+ return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
335
+ }
336
+ function detectRuntime(env) {
337
+ if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
338
+ if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
339
+ if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
340
+ return null;
341
+ }
342
+
343
+ // src/config-writers.ts
344
+ var InvalidCodexTomlError = class extends Error {
345
+ constructor(message) {
346
+ super(message);
347
+ this.name = "InvalidCodexTomlError";
348
+ }
349
+ };
211
350
  async function writeRuntimeConfig(input) {
212
351
  switch (input.runtime) {
213
352
  case "codex-cli":
353
+ case "codex-desktop":
214
354
  return writeCodexConfig(input);
215
355
  case "cursor":
216
356
  return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
217
357
  case "vscode":
218
358
  return writeJsonRuntimeConfig(input, vscodeConfigPath(input.homeDir), "servers");
359
+ case "vscode-insiders":
360
+ return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
219
361
  case "claude-desktop":
220
362
  return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
221
363
  default:
@@ -233,7 +375,7 @@ async function writeRuntimeConfig(input) {
233
375
  }
234
376
  }
235
377
  function buildHostedServer(hostedMcpUrl, apiKey, runtime) {
236
- if (runtime === "vscode") {
378
+ if (runtime === "vscode" || runtime === "vscode-insiders") {
237
379
  return {
238
380
  type: "http",
239
381
  url: hostedMcpUrl,
@@ -250,7 +392,7 @@ function buildSignerServer(signerPath, runtime) {
250
392
  command: "npx",
251
393
  args: ["-y", signerPackageName(), "--credentials", signerPath]
252
394
  };
253
- if (runtime === "vscode") return { type: "stdio", ...server };
395
+ if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
254
396
  return server;
255
397
  }
256
398
  function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer) {
@@ -265,18 +407,21 @@ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer
265
407
  return `${JSON.stringify(config, null, 2)}
266
408
  `;
267
409
  }
268
- function mergeCodexToml(existingToml, identityPath, signerPath) {
269
- let next = removeTomlTable(removeTomlTable(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
410
+ function mergeCodexToml(existingToml, localMcpCommand) {
411
+ let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
270
412
  next = next.trimEnd();
271
413
  const block = [
272
414
  "[mcp_servers.haven]",
273
- 'command = "npx"',
274
- `args = ["-y", ${tomlString(localMcpPackageName())}, "--identity", ${tomlString(identityPath)}, "--signer", ${tomlString(signerPath)}]`
415
+ `command = ${tomlString(localMcpCommand)}`,
416
+ "args = []",
417
+ "startup_timeout_sec = 120"
275
418
  ].join("\n");
276
- return `${next ? `${next}
419
+ validateCodexToml(block, "Generated Codex Haven config");
420
+ const merged = `${next ? `${next}
277
421
 
278
422
  ` : ""}${block}
279
423
  `;
424
+ return merged;
280
425
  }
281
426
  async function writeJsonRuntimeConfig(input, target, serverRoot) {
282
427
  try {
@@ -316,32 +461,39 @@ async function writeCodexConfig(input) {
316
461
  const target = codexConfigPath(input.homeDir);
317
462
  try {
318
463
  const existing = await readOptional(target);
319
- const merged = mergeCodexToml(existing ?? "", input.identityPath, input.signerPath);
464
+ if (!input.localMcpCommand) {
465
+ throw new Error("local MCP wrapper command is required");
466
+ }
467
+ const merged = mergeCodexToml(existing ?? "", input.localMcpCommand);
320
468
  await writeOwnerOnlyText(target, merged);
321
469
  return {
322
470
  hostedConfigured: false,
323
471
  signerConfigured: true,
324
472
  localMcpConfigured: true,
325
473
  runtimeMcpMode: "local_stdio",
326
- target: "Codex CLI config",
474
+ target: configTargetLabel(input.runtime),
327
475
  changed: existing !== merged,
328
476
  restartRequired: true,
329
477
  messages: [
330
- "Updated local Haven MCP entry in Codex CLI config.",
331
- "After Haven approval, restart Codex normally so it can load Haven tools."
478
+ `Updated local Haven MCP entry in ${configTargetLabel(input.runtime)}.`,
479
+ // Codex Desktop loads MCP servers at app launch; Codex CLI typically
480
+ // picks them up in the next session. Branch the copy so desktop users
481
+ // get the unambiguous instruction.
482
+ runtimeRequiresHardRestart(input.runtime) ? "After Haven approval, restart Codex Desktop so it can load Haven tools." : "After Haven approval, Haven tools should appear in your next Codex message. If they don't, restart Codex to load them."
332
483
  ]
333
484
  };
334
485
  } catch (err) {
486
+ const invalidToml = err instanceof InvalidCodexTomlError;
335
487
  return {
336
488
  hostedConfigured: false,
337
489
  signerConfigured: false,
338
490
  localMcpConfigured: false,
339
491
  runtimeMcpMode: "local_stdio",
340
- target: "Codex CLI config",
492
+ target: configTargetLabel(input.runtime),
341
493
  changed: false,
342
494
  restartRequired: true,
343
- messages: [`Could not update Codex CLI config: ${err instanceof Error ? err.message : String(err)}`],
344
- errorCode: "runtime_config_write_failed"
495
+ messages: [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
496
+ errorCode: invalidToml ? "codex_config_invalid" : "runtime_config_write_failed"
345
497
  };
346
498
  }
347
499
  }
@@ -365,24 +517,191 @@ function parseJsonObject(value) {
365
517
  }
366
518
  return parsed;
367
519
  }
368
- function removeTomlTable(toml, table) {
520
+ function removeTomlTableTree(toml, table) {
369
521
  const lines = toml.split(/\r?\n/);
370
- const start = `[${table}]`;
371
522
  const kept = [];
372
523
  let skipping = false;
373
524
  for (const line of lines) {
374
525
  const trimmed = line.trim();
375
- if (trimmed === start) {
376
- skipping = true;
377
- continue;
378
- }
379
- if (skipping && trimmed.startsWith("[") && trimmed.endsWith("]")) {
380
- skipping = false;
526
+ const tableName = tomlTableName(trimmed);
527
+ if (tableName) {
528
+ skipping = tableName === table || tableName.startsWith(`${table}.`);
529
+ if (skipping) continue;
381
530
  }
382
531
  if (!skipping) kept.push(line);
383
532
  }
384
533
  return kept.join("\n");
385
534
  }
535
+ function tomlTableName(line) {
536
+ if (line.startsWith("[[") && line.endsWith("]]")) return line.slice(2, -2).trim();
537
+ if (line.startsWith("[") && line.endsWith("]")) return line.slice(1, -1).trim();
538
+ return null;
539
+ }
540
+ function validateCodexToml(toml, label = "Codex config") {
541
+ const lines = toml.split(/\r?\n/);
542
+ let pendingValue = null;
543
+ for (let index = 0; index < lines.length; index += 1) {
544
+ const raw = lines[index];
545
+ const line = stripTomlComment(raw).trim();
546
+ if (!line) continue;
547
+ if (pendingValue) {
548
+ pendingValue.value = `${pendingValue.value}
549
+ ${line}`;
550
+ if (hasBalancedTomlContainers(pendingValue.value)) {
551
+ if (!isTomlValue(pendingValue.value)) {
552
+ throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${pendingValue.line}.`);
553
+ }
554
+ pendingValue = null;
555
+ }
556
+ continue;
557
+ }
558
+ if (isTomlTable(line)) continue;
559
+ const equalsIndex = line.indexOf("=");
560
+ if (equalsIndex <= 0) {
561
+ throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
562
+ }
563
+ const key = line.slice(0, equalsIndex).trim();
564
+ const value = line.slice(equalsIndex + 1).trim();
565
+ if (!isTomlKey(key)) {
566
+ throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
567
+ }
568
+ if (startsTomlContainer(value) && !hasBalancedTomlContainers(value)) {
569
+ pendingValue = { value, line: index + 1 };
570
+ continue;
571
+ }
572
+ if (!isTomlValue(value)) {
573
+ throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
574
+ }
575
+ }
576
+ if (pendingValue) {
577
+ throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${pendingValue.line}.`);
578
+ }
579
+ }
580
+ function isTomlTable(line) {
581
+ const table = tomlTableName(line);
582
+ return Boolean(table && splitTomlDottedKey(table).every(isTomlKeyPart));
583
+ }
584
+ function isTomlKey(value) {
585
+ return splitTomlDottedKey(value).every(isTomlKeyPart);
586
+ }
587
+ function splitTomlDottedKey(value) {
588
+ const parts = [];
589
+ let current = "";
590
+ let quote = null;
591
+ let escaped = false;
592
+ for (let i = 0; i < value.length; i += 1) {
593
+ const char = value[i];
594
+ if (quote) {
595
+ current += char;
596
+ if (quote === '"' && char === "\\" && !escaped) {
597
+ escaped = true;
598
+ continue;
599
+ }
600
+ if (char === quote && !escaped) quote = null;
601
+ escaped = false;
602
+ continue;
603
+ }
604
+ if (char === '"' || char === "'") {
605
+ quote = char;
606
+ current += char;
607
+ continue;
608
+ }
609
+ if (char === ".") {
610
+ parts.push(current.trim());
611
+ current = "";
612
+ continue;
613
+ }
614
+ current += char;
615
+ }
616
+ parts.push(current.trim());
617
+ return quote ? [] : parts;
618
+ }
619
+ function isTomlKeyPart(value) {
620
+ return isTomlBareKey(value) || isTomlQuotedString(value);
621
+ }
622
+ function isTomlBareKey(value) {
623
+ return /^[A-Za-z0-9_-]+$/.test(value);
624
+ }
625
+ function isTomlValue(value) {
626
+ if (!value) return false;
627
+ if (isTomlQuotedString(value)) return true;
628
+ if (/^(true|false)$/i.test(value)) return true;
629
+ if (/^[+-]?(?:inf|nan)$/i.test(value)) return true;
630
+ if (/^[+-]?(?:0|[1-9][0-9_]*)(?:\.[0-9_]+)?(?:[eE][+-]?[0-9_]+)?$/.test(value)) return true;
631
+ if (/^\d{4}-\d{2}-\d{2}(?:[Tt ][0-9:.+-Zz]+)?$/.test(value)) return true;
632
+ if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}")) {
633
+ return hasBalancedTomlContainers(value);
634
+ }
635
+ return false;
636
+ }
637
+ function startsTomlContainer(value) {
638
+ return value.startsWith("[") || value.startsWith("{");
639
+ }
640
+ function isTomlQuotedString(value) {
641
+ if (value.startsWith('"""') || value.startsWith("'''")) {
642
+ const marker = value.slice(0, 3);
643
+ return value.length >= 6 && value.endsWith(marker);
644
+ }
645
+ if ((!value.startsWith('"') || !value.endsWith('"')) && (!value.startsWith("'") || !value.endsWith("'"))) {
646
+ return false;
647
+ }
648
+ return hasBalancedTomlContainers(value);
649
+ }
650
+ function stripTomlComment(value) {
651
+ let quote = null;
652
+ let escaped = false;
653
+ for (let i = 0; i < value.length; i += 1) {
654
+ const char = value[i];
655
+ if (quote) {
656
+ if (quote === '"' && char === "\\" && !escaped) {
657
+ escaped = true;
658
+ continue;
659
+ }
660
+ if (char === quote && !escaped) quote = null;
661
+ escaped = false;
662
+ continue;
663
+ }
664
+ if (char === '"' || char === "'") {
665
+ quote = char;
666
+ continue;
667
+ }
668
+ if (char === "#") return value.slice(0, i);
669
+ }
670
+ return value;
671
+ }
672
+ function hasBalancedTomlContainers(value) {
673
+ const stack = [];
674
+ let quote = null;
675
+ let escaped = false;
676
+ for (let i = 0; i < value.length; i += 1) {
677
+ const char = value[i];
678
+ if (quote) {
679
+ if (quote === '"' && char === "\\" && !escaped) {
680
+ escaped = true;
681
+ continue;
682
+ }
683
+ if (char === quote && !escaped) quote = null;
684
+ escaped = false;
685
+ continue;
686
+ }
687
+ if (char === '"' || char === "'") {
688
+ quote = char;
689
+ continue;
690
+ }
691
+ if (char === "[" || char === "{") {
692
+ stack.push(char);
693
+ continue;
694
+ }
695
+ if (char === "]") {
696
+ if (stack.pop() !== "[") return false;
697
+ continue;
698
+ }
699
+ if (char === "}") {
700
+ if (stack.pop() !== "{") return false;
701
+ }
702
+ }
703
+ return stack.length === 0 && quote === null;
704
+ }
386
705
  function tomlString(value) {
387
706
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
388
707
  }
@@ -399,6 +718,13 @@ function vscodeConfigPath(homeDir = os.homedir()) {
399
718
  }
400
719
  return path.resolve(homeDir, ".config", "Code", "User", "mcp.json");
401
720
  }
721
+ function vscodeInsidersConfigPath(homeDir = os.homedir()) {
722
+ if (os.platform() === "darwin") return path.resolve(homeDir, "Library", "Application Support", "Code - Insiders", "User", "mcp.json");
723
+ if (os.platform() === "win32") {
724
+ return path.resolve(process.env.APPDATA ?? path.join(homeDir, "AppData", "Roaming"), "Code - Insiders", "User", "mcp.json");
725
+ }
726
+ return path.resolve(homeDir, ".config", "Code - Insiders", "User", "mcp.json");
727
+ }
402
728
  function claudeDesktopConfigPath(homeDir = os.homedir()) {
403
729
  if (os.platform() === "darwin") {
404
730
  return path.resolve(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
@@ -410,10 +736,16 @@ function claudeDesktopConfigPath(homeDir = os.homedir()) {
410
736
  }
411
737
  function configTargetLabel(runtime) {
412
738
  switch (runtime) {
739
+ case "codex-cli":
740
+ return "Codex CLI config";
741
+ case "codex-desktop":
742
+ return "Codex Desktop config";
413
743
  case "cursor":
414
744
  return "Cursor MCP config";
415
745
  case "vscode":
416
746
  return "VS Code MCP config";
747
+ case "vscode-insiders":
748
+ return "VS Code Insiders MCP config";
417
749
  case "claude-desktop":
418
750
  return "Claude Desktop config";
419
751
  default:
@@ -421,10 +753,7 @@ function configTargetLabel(runtime) {
421
753
  }
422
754
  }
423
755
  function signerPackageName() {
424
- return `@haven_ai/signer@${signer.SIGNER_VERSION}`;
425
- }
426
- function localMcpPackageName() {
427
- return `@haven_ai/mcp@${mcp.MCP_VERSION}`;
756
+ return signerPackageSpec();
428
757
  }
429
758
  async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
430
759
  try {
@@ -538,6 +867,72 @@ async function probeLocalSignerCredential(signerPath) {
538
867
  return false;
539
868
  }
540
869
  }
870
+ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
871
+ return new Promise((resolve6) => {
872
+ const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
873
+ let stdout = "";
874
+ let settled = false;
875
+ let sawInitialize = false;
876
+ const finish = (result) => {
877
+ if (settled) return;
878
+ settled = true;
879
+ clearTimeout(timeout);
880
+ child.kill();
881
+ resolve6(result);
882
+ };
883
+ const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
884
+ child.on("error", () => finish({ status: "process_error" }));
885
+ child.on("exit", (code) => {
886
+ if (!settled && code !== 0) finish({ status: "process_error" });
887
+ });
888
+ child.stdout.on("data", (chunk) => {
889
+ stdout += chunk.toString("utf8");
890
+ const lines = stdout.split(/\r?\n/);
891
+ stdout = lines.pop() ?? "";
892
+ for (const line of lines) {
893
+ const trimmed = line.trim();
894
+ if (!trimmed) continue;
895
+ let payload;
896
+ try {
897
+ payload = JSON.parse(trimmed);
898
+ } catch {
899
+ continue;
900
+ }
901
+ if (payload.error) {
902
+ finish({ status: "bad_response" });
903
+ return;
904
+ }
905
+ if (payload.id === 1 && !sawInitialize) {
906
+ sawInitialize = true;
907
+ writeJsonRpc(child, { jsonrpc: "2.0", method: "notifications/initialized", params: {} });
908
+ writeJsonRpc(child, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
909
+ continue;
910
+ }
911
+ if (payload.id === 2) {
912
+ const tools = payload.result?.tools;
913
+ const toolNames = Array.isArray(tools) ? tools.map((tool) => tool && typeof tool === "object" && "name" in tool ? tool.name : void 0).filter((name) => typeof name === "string") : [];
914
+ const missing = requiredTools.filter((name) => !toolNames.includes(name));
915
+ finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames });
916
+ return;
917
+ }
918
+ }
919
+ });
920
+ writeJsonRpc(child, {
921
+ jsonrpc: "2.0",
922
+ id: 1,
923
+ method: "initialize",
924
+ params: {
925
+ protocolVersion: "2025-06-18",
926
+ capabilities: {},
927
+ clientInfo: { name: "haven-connect-probe", version: "0.0.0" }
928
+ }
929
+ });
930
+ });
931
+ }
932
+ function writeJsonRpc(child, payload) {
933
+ child.stdin?.write(`${JSON.stringify(payload)}
934
+ `);
935
+ }
541
936
  function parseJsonRpcPayload(raw) {
542
937
  const trimmed = raw.trim();
543
938
  if (!trimmed) return null;
@@ -566,89 +961,160 @@ async function fetchWithTimeout(fetchImpl, url, init) {
566
961
  clearTimeout(timeout);
567
962
  }
568
963
  }
569
-
570
- // src/runtime-registry.ts
571
- var RUNTIME_PROFILES = {
572
- "claude-code": {
573
- id: "claude-code",
574
- label: "Claude Code",
575
- restartMode: "restart-session",
576
- canWriteRuntimeConfig: true
577
- },
578
- "codex-cli": {
579
- id: "codex-cli",
580
- label: "Codex CLI",
581
- restartMode: "restart-session",
582
- canWriteRuntimeConfig: true
583
- },
584
- cursor: {
585
- id: "cursor",
586
- label: "Cursor",
587
- restartMode: "hot-reload",
588
- canWriteRuntimeConfig: true
589
- },
590
- vscode: {
591
- id: "vscode",
592
- label: "VS Code",
593
- restartMode: "hot-reload",
594
- canWriteRuntimeConfig: true
595
- },
596
- "claude-desktop": {
597
- id: "claude-desktop",
598
- label: "Claude Desktop",
599
- restartMode: "restart-app",
600
- canWriteRuntimeConfig: true
601
- },
602
- other: {
603
- id: "other",
604
- label: "Other agent runtime",
605
- restartMode: "manual",
606
- canWriteRuntimeConfig: false
964
+ var execFileAsync = util.promisify(child_process.execFile);
965
+ var UnsupportedNodeVersionError = class extends Error {
966
+ code = "local_mcp_unsupported_node_version";
967
+ constructor(nodeVersion, minimumNodeVersion) {
968
+ super(`Node.js ${nodeVersion} is not supported. Haven local MCP requires Node.js >=${minimumNodeVersion}.`);
969
+ this.name = "UnsupportedNodeVersionError";
607
970
  }
608
971
  };
609
- var RUNTIME_ALIASES = {
610
- claude: "claude-code",
611
- "claude-code": "claude-code",
612
- claudecode: "claude-code",
613
- "claude_code": "claude-code",
614
- codex: "codex-cli",
615
- "codex-cli": "codex-cli",
616
- codexcli: "codex-cli",
617
- "codex_cli": "codex-cli",
618
- cursor: "cursor",
619
- vscode: "vscode",
620
- "vs-code": "vscode",
621
- "vs_code": "vscode",
622
- code: "vscode",
623
- "claude-desktop": "claude-desktop",
624
- "claude_desktop": "claude-desktop",
625
- claudesktop: "claude-desktop",
626
- desktop: "claude-desktop",
627
- other: "other",
628
- manual: "other"
629
- };
630
- function runtimeProfile(runtime, env = process.env) {
631
- return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
972
+ async function prepareLocalMcpRuntime(input, deps = {}) {
973
+ assertSupportedNodeVersion(input.nodeVersion);
974
+ const homeDir = input.homeDir ?? os.homedir();
975
+ const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
976
+ const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
977
+ const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
978
+ const messages = [];
979
+ await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
980
+ await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
981
+ await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
982
+ await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
983
+ if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
984
+ messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
985
+ } else {
986
+ await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps.runCommand);
987
+ messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
988
+ }
989
+ await assertFileExists(cliPath, "local Haven MCP CLI");
990
+ const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-mcp");
991
+ await writeWrapper({
992
+ wrapperPath,
993
+ cliPath,
994
+ identityPath: input.identityPath,
995
+ signerPath: input.signerPath
996
+ });
997
+ await writeRuntimeSidecar({
998
+ path: path.join(input.credentialDirectory, "mcp-runtime.json"),
999
+ wrapperPath,
1000
+ runtimeDirectory,
1001
+ npmCacheDirectory,
1002
+ cliPath
1003
+ });
1004
+ messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
1005
+ return {
1006
+ command: wrapperPath,
1007
+ args: [],
1008
+ wrapperPath,
1009
+ runtimeDirectory,
1010
+ npmCacheDirectory,
1011
+ cliPath,
1012
+ messages
1013
+ };
632
1014
  }
633
- function normalizeRuntime(runtime, env = process.env) {
634
- const explicit = normalizeRuntimeName(runtime);
635
- if (explicit) return explicit;
636
- return detectRuntime(env) ?? "other";
1015
+ function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion) {
1016
+ if (compareNodeVersions(nodeVersion, minimumNodeVersion) < 0) {
1017
+ throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion);
1018
+ }
637
1019
  }
638
- function restartRequiredForRuntime(runtime, env = process.env) {
639
- const mode = runtimeProfile(runtime, env).restartMode;
640
- return mode === "restart-session" || mode === "restart-app";
1020
+ function compareNodeVersions(left, right) {
1021
+ const leftParts = parseNodeVersion(left);
1022
+ const rightParts = parseNodeVersion(right);
1023
+ for (let i = 0; i < 3; i += 1) {
1024
+ if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
1025
+ }
1026
+ return 0;
641
1027
  }
642
- function normalizeRuntimeName(runtime) {
643
- const key = runtime?.trim().toLowerCase();
644
- if (!key) return null;
645
- return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
1028
+ function parseNodeVersion(value) {
1029
+ const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
1030
+ if (!match) return [0, 0, 0];
1031
+ return [
1032
+ Number(match[1] ?? 0),
1033
+ Number(match[2] ?? 0),
1034
+ Number(match[3] ?? 0)
1035
+ ];
1036
+ }
1037
+ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCommand) {
1038
+ const args = [
1039
+ "install",
1040
+ "--prefix",
1041
+ runtimeDirectory,
1042
+ "--cache",
1043
+ npmCacheDirectory,
1044
+ "--no-audit",
1045
+ "--no-fund",
1046
+ "--omit=dev",
1047
+ mcpPackageSpec(),
1048
+ sdkPackageSpec()
1049
+ ];
1050
+ try {
1051
+ if (runCommand) await runCommand("npm", args);
1052
+ else await execFileAsync("npm", args, { timeout: 12e4, maxBuffer: 1024 * 1024 });
1053
+ } catch (err) {
1054
+ throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
1055
+ }
646
1056
  }
647
- function detectRuntime(env) {
648
- if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
649
- if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
650
- if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
651
- return null;
1057
+ async function installedRuntimeMatches(runtimeDirectory, cliPath) {
1058
+ try {
1059
+ await assertFileExists(cliPath, "local Haven MCP CLI");
1060
+ const [mcpPackage, sdkPackage] = await Promise.all([
1061
+ readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
1062
+ readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
1063
+ ]);
1064
+ return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
1065
+ } catch {
1066
+ return false;
1067
+ }
1068
+ }
1069
+ async function readPackageJson(path) {
1070
+ return JSON.parse(await promises.readFile(path, "utf8"));
1071
+ }
1072
+ async function writeWrapper(input) {
1073
+ await promises.mkdir(path.dirname(input.wrapperPath), { recursive: true, mode: 448 });
1074
+ await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
1075
+ const source = [
1076
+ "#!/usr/bin/env node",
1077
+ "import { spawn } from 'node:child_process'",
1078
+ "",
1079
+ `const cliPath = ${JSON.stringify(input.cliPath)}`,
1080
+ `const identityPath = ${JSON.stringify(input.identityPath)}`,
1081
+ `const signerPath = ${JSON.stringify(input.signerPath)}`,
1082
+ "",
1083
+ "const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
1084
+ " stdio: 'inherit',",
1085
+ "})",
1086
+ "",
1087
+ "child.on('exit', (code, signal) => {",
1088
+ " if (signal) process.kill(process.pid, signal)",
1089
+ " else process.exit(code ?? 1)",
1090
+ "})",
1091
+ ""
1092
+ ].join("\n");
1093
+ await promises.writeFile(input.wrapperPath, source, { mode: 448 });
1094
+ await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
1095
+ }
1096
+ async function writeRuntimeSidecar(input) {
1097
+ const value = {
1098
+ mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
1099
+ mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
1100
+ sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
1101
+ sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
1102
+ minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
1103
+ wrapper_path: input.wrapperPath,
1104
+ runtime_directory: input.runtimeDirectory,
1105
+ npm_cache_directory: input.npmCacheDirectory,
1106
+ cli_path: input.cliPath
1107
+ };
1108
+ await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
1109
+ `, { mode: 384 });
1110
+ await promises.chmod(input.path, 384).catch(() => void 0);
1111
+ }
1112
+ async function assertFileExists(path, label) {
1113
+ try {
1114
+ await promises.access(path);
1115
+ } catch {
1116
+ throw new Error(`Missing ${label}: ${path}`);
1117
+ }
652
1118
  }
653
1119
  async function acknowledgeLocalSignerConsent(signerPath, log) {
654
1120
  try {
@@ -721,7 +1187,7 @@ function writeLogChunk2(log, chunk) {
721
1187
  }
722
1188
 
723
1189
  // src/runtime-install.ts
724
- var execFileAsync = util.promisify(child_process.execFile);
1190
+ var execFileAsync2 = util.promisify(child_process.execFile);
725
1191
  async function installRuntime(input, deps = {}) {
726
1192
  const runtime = normalizeRuntime(input.runtime, deps.env);
727
1193
  const profile = runtimeProfile(runtime, deps.env);
@@ -751,31 +1217,66 @@ async function installRuntime(input, deps = {}) {
751
1217
  ]
752
1218
  };
753
1219
  }
754
- const configResult = runtime === "claude-code" ? await configureClaudeCode(input, deps) : await writeRuntimeConfig({
1220
+ let localRuntimeInstall;
1221
+ let localRuntimeError;
1222
+ if (localRuntime) {
1223
+ try {
1224
+ localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
1225
+ } catch (err) {
1226
+ localRuntimeError = err;
1227
+ }
1228
+ }
1229
+ if (localRuntimeError) {
1230
+ const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
1231
+ return {
1232
+ runtime,
1233
+ runtimeMcpMode: "local_stdio",
1234
+ hostedMcpConfigured: false,
1235
+ localSignerConfigured: false,
1236
+ localMcpConfigured: false,
1237
+ probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
1238
+ restartRequired: true,
1239
+ nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
1240
+ errorCode: errorCode2,
1241
+ configTarget: profile.label,
1242
+ signerAcknowledged: signerConsent?.acknowledged,
1243
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
1244
+ activationCommand: void 0,
1245
+ messages: [
1246
+ ...consentMessages,
1247
+ `Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
1248
+ ]
1249
+ };
1250
+ }
1251
+ const configResult = runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await writeRuntimeConfig({
755
1252
  runtime,
756
1253
  hostedMcpUrl: input.hostedMcpUrl,
757
1254
  apiKey: input.apiKey,
758
1255
  identityPath: input.identityPath,
759
1256
  signerPath: input.signerPath,
760
1257
  credentialDirectory: input.credentialDirectory,
1258
+ localMcpCommand: localRuntimeInstall?.command,
761
1259
  homeDir: deps.homeDir
762
1260
  });
763
- const [hostedProbe, signerCredentialReady] = await Promise.all([
1261
+ const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
1262
+ const [hostedProbe, signerCredentialReady, localMcpProbe] = await Promise.all([
764
1263
  configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
765
- probeLocalSignerCredential(input.signerPath)
1264
+ probeLocalSignerCredential(input.signerPath),
1265
+ localProbePromise
766
1266
  ]);
767
1267
  const hostedOk = configResult.hostedConfigured && hostedProbe.status !== "unauthorized";
768
- const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged);
1268
+ const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
769
1269
  const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
770
1270
  const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
771
- const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpConsentErrorCode(signerCredentialReady, localMcpConsent) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1271
+ const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1272
+ const localProbeMessages = localMcpProbe && localMcpProbe.status !== "ok" ? [`Local Haven MCP handshake failed: ${localMcpProbe.status}.`] : localMcpProbe?.status === "ok" ? ["Verified local Haven MCP tools with a stdio handshake."] : [];
772
1273
  return {
773
1274
  runtime,
774
1275
  runtimeMcpMode: configResult.runtimeMcpMode,
775
1276
  hostedMcpConfigured: hostedOk,
776
1277
  localSignerConfigured: signerOk,
777
1278
  localMcpConfigured: localMcpOk,
778
- probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk),
1279
+ probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
779
1280
  restartRequired,
780
1281
  nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
781
1282
  errorCode,
@@ -783,7 +1284,7 @@ async function installRuntime(input, deps = {}) {
783
1284
  signerAcknowledged: signerConsent?.acknowledged,
784
1285
  localMcpAcknowledged: localMcpConsent?.acknowledged,
785
1286
  activationCommand: configResult.activationCommand,
786
- messages: [...consentMessages, ...configResult.messages]
1287
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages]
787
1288
  };
788
1289
  }
789
1290
  function runtimeInstallCapabilities(runtime, env = process.env) {
@@ -793,23 +1294,21 @@ function runtimeInstallCapabilities(runtime, env = process.env) {
793
1294
  restartRequired: restartRequiredForRuntime(runtime, env)
794
1295
  };
795
1296
  }
796
- async function configureClaudeCode(input, deps) {
1297
+ async function configureClaudeCode(deps, localMcpCommand) {
797
1298
  const runCommand = deps.runCommand ?? defaultRunCommand;
1299
+ const serverJson = JSON.stringify({
1300
+ type: "stdio",
1301
+ command: localMcpCommand,
1302
+ args: [],
1303
+ env: {}
1304
+ });
798
1305
  try {
799
- await runCommand("claude", [
800
- "mcp",
801
- "add",
802
- "haven",
803
- "--",
804
- "npx",
805
- "-y",
806
- localMcpPackageName2(),
807
- "--identity",
808
- input.identityPath,
809
- "--signer",
810
- input.signerPath
811
- ]);
1306
+ if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
1307
+ await runCommand("claude", ["mcp", "add-json", "haven", serverJson, "--scope", "user"]).catch(async () => {
1308
+ await runCommand("claude", ["mcp", "add", "haven", "--scope", "user", "--", localMcpCommand]);
1309
+ });
812
1310
  await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
1311
+ const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
813
1312
  return {
814
1313
  hostedConfigured: false,
815
1314
  signerConfigured: true,
@@ -820,7 +1319,8 @@ async function configureClaudeCode(input, deps) {
820
1319
  restartRequired: true,
821
1320
  messages: [
822
1321
  "Updated local Haven MCP entry with Claude Code.",
823
- "After Haven approval, restart Claude Code normally so it can load Haven tools."
1322
+ ...verified ? ["Verified Claude Code MCP entry."] : [],
1323
+ "After Haven approval, Haven tools should appear in your next Claude Code message. If they don't, restart the session to load them."
824
1324
  ]
825
1325
  };
826
1326
  } catch (err) {
@@ -841,11 +1341,12 @@ async function configureClaudeCode(input, deps) {
841
1341
  }
842
1342
  }
843
1343
  async function defaultRunCommand(command, args) {
844
- await execFileAsync(command, args, { timeout: 1e4 });
1344
+ await execFileAsync2(command, args, { timeout: 1e4 });
845
1345
  }
846
- function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady) {
1346
+ function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
847
1347
  if (mode === "local_stdio") {
848
- return localMcpReady ? "local_stdio_mcp_ready" : "local_stdio_mcp_unavailable";
1348
+ if (localMcpReady) return "local_stdio_mcp_ready";
1349
+ return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
849
1350
  }
850
1351
  const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
851
1352
  const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
@@ -880,25 +1381,46 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
880
1381
  if (!signerConsent?.acknowledged) return "local_signer_ack_required";
881
1382
  return void 0;
882
1383
  }
883
- function localMcpConsentErrorCode(signerCredentialReady, localMcpConsent) {
1384
+ function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
884
1385
  if (!signerCredentialReady) return "local_signer_credential_unavailable";
885
1386
  if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
1387
+ if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
886
1388
  return void 0;
887
1389
  }
888
1390
  function nextAction(runtime, restartMode, errorCode) {
889
1391
  if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
890
1392
  if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
891
- if (runtime === "codex-cli") return "return_to_haven_for_wallet_approval_then_restart_codex";
1393
+ if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
892
1394
  if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
893
1395
  if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
894
1396
  if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
895
1397
  return "return_to_haven_for_wallet_approval_then_configure_runtime";
896
1398
  }
897
- function localMcpPackageName2() {
898
- return `@haven_ai/mcp@${mcp.MCP_VERSION}`;
899
- }
900
1399
  function usesLocalMcp(runtime) {
901
- return runtime === "codex-cli" || runtime === "claude-code";
1400
+ return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
1401
+ }
1402
+ async function prepareRuntimeForLocalMcp(input, deps) {
1403
+ const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand }));
1404
+ return prepare({
1405
+ credentialDirectory: input.credentialDirectory,
1406
+ identityPath: input.identityPath,
1407
+ signerPath: input.signerPath,
1408
+ homeDir: deps.homeDir
1409
+ });
1410
+ }
1411
+ async function runLocalMcpProbe(runtimeInstall, deps) {
1412
+ const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
1413
+ try {
1414
+ return await probe(runtimeInstall.command, runtimeInstall.args, MCP_RUNTIME_MANIFEST.requiredTools);
1415
+ } catch {
1416
+ return { status: "process_error" };
1417
+ }
1418
+ }
1419
+ function localRuntimePrepareErrorCode(err) {
1420
+ if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
1421
+ return "local_mcp_unsupported_node_version";
1422
+ }
1423
+ return "local_mcp_runtime_install_failed";
902
1424
  }
903
1425
 
904
1426
  // src/runtime.ts
@@ -998,7 +1520,11 @@ async function runConnect(options, deps = {}) {
998
1520
  }
999
1521
  log("Return to Haven to approve the agent rules.");
1000
1522
  if (runtimeInstall.restartRequired) {
1001
- log("After approval, restart this agent normally so it can load Haven tools.");
1523
+ if (runtimeRequiresHardRestart(runtimeInstall.runtime)) {
1524
+ log("After approval, restart this agent so it can load Haven tools.");
1525
+ } else {
1526
+ log("After approval, Haven tools should appear in your next message. If they don't, restart this agent to load them.");
1527
+ }
1002
1528
  }
1003
1529
  return {
1004
1530
  setupId: registration.setup_id,
@@ -1097,7 +1623,7 @@ function helpText() {
1097
1623
  "Options:",
1098
1624
  " --setup <token> Short-lived setup token from Haven.",
1099
1625
  " --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
1100
- " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, cursor, vscode, or claude-desktop.",
1626
+ " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, or claude-desktop.",
1101
1627
  " --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
1102
1628
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
1103
1629
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",