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