@haven_ai/connect 0.1.2-alpha → 0.1.3-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,9 +208,50 @@ 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.6",
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: [
221
+ "haven_quote_x402",
222
+ "haven_pay_x402_quote",
223
+ "haven_resume_x402_payment",
224
+ "haven_quote_mpp",
225
+ "haven_pay_mpp_challenge",
226
+ "haven_resume_mpp_payment",
227
+ "haven_get_payment_status",
228
+ "haven_get_resume_state",
229
+ "haven_get_agent",
230
+ "haven_get_allowances",
231
+ "haven_list_receipts"
232
+ ]
233
+ };
234
+ function mcpPackageSpec() {
235
+ return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
236
+ }
237
+ function sdkPackageSpec() {
238
+ return `${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}`;
239
+ }
240
+ function signerPackageSpec() {
241
+ return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
242
+ }
243
+
244
+ // src/config-writers.ts
245
+ var InvalidCodexTomlError = class extends Error {
246
+ constructor(message) {
247
+ super(message);
248
+ this.name = "InvalidCodexTomlError";
249
+ }
250
+ };
211
251
  async function writeRuntimeConfig(input) {
212
252
  switch (input.runtime) {
213
253
  case "codex-cli":
254
+ case "codex-desktop":
214
255
  return writeCodexConfig(input);
215
256
  case "cursor":
216
257
  return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
@@ -265,18 +306,21 @@ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer
265
306
  return `${JSON.stringify(config, null, 2)}
266
307
  `;
267
308
  }
268
- function mergeCodexToml(existingToml, identityPath, signerPath) {
269
- let next = removeTomlTable(removeTomlTable(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
309
+ function mergeCodexToml(existingToml, localMcpCommand) {
310
+ let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
270
311
  next = next.trimEnd();
271
312
  const block = [
272
313
  "[mcp_servers.haven]",
273
- 'command = "npx"',
274
- `args = ["-y", ${tomlString(localMcpPackageName())}, "--identity", ${tomlString(identityPath)}, "--signer", ${tomlString(signerPath)}]`
314
+ `command = ${tomlString(localMcpCommand)}`,
315
+ "args = []",
316
+ "startup_timeout_sec = 120"
275
317
  ].join("\n");
276
- return `${next ? `${next}
318
+ validateCodexToml(block, "Generated Codex Haven config");
319
+ const merged = `${next ? `${next}
277
320
 
278
321
  ` : ""}${block}
279
322
  `;
323
+ return merged;
280
324
  }
281
325
  async function writeJsonRuntimeConfig(input, target, serverRoot) {
282
326
  try {
@@ -316,32 +360,36 @@ async function writeCodexConfig(input) {
316
360
  const target = codexConfigPath(input.homeDir);
317
361
  try {
318
362
  const existing = await readOptional(target);
319
- const merged = mergeCodexToml(existing ?? "", input.identityPath, input.signerPath);
363
+ if (!input.localMcpCommand) {
364
+ throw new Error("local MCP wrapper command is required");
365
+ }
366
+ const merged = mergeCodexToml(existing ?? "", input.localMcpCommand);
320
367
  await writeOwnerOnlyText(target, merged);
321
368
  return {
322
369
  hostedConfigured: false,
323
370
  signerConfigured: true,
324
371
  localMcpConfigured: true,
325
372
  runtimeMcpMode: "local_stdio",
326
- target: "Codex CLI config",
373
+ target: configTargetLabel(input.runtime),
327
374
  changed: existing !== merged,
328
375
  restartRequired: true,
329
376
  messages: [
330
- "Updated local Haven MCP entry in Codex CLI config.",
377
+ `Updated local Haven MCP entry in ${configTargetLabel(input.runtime)}.`,
331
378
  "After Haven approval, restart Codex normally so it can load Haven tools."
332
379
  ]
333
380
  };
334
381
  } catch (err) {
382
+ const invalidToml = err instanceof InvalidCodexTomlError;
335
383
  return {
336
384
  hostedConfigured: false,
337
385
  signerConfigured: false,
338
386
  localMcpConfigured: false,
339
387
  runtimeMcpMode: "local_stdio",
340
- target: "Codex CLI config",
388
+ target: configTargetLabel(input.runtime),
341
389
  changed: false,
342
390
  restartRequired: true,
343
- messages: [`Could not update Codex CLI config: ${err instanceof Error ? err.message : String(err)}`],
344
- errorCode: "runtime_config_write_failed"
391
+ messages: [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
392
+ errorCode: invalidToml ? "codex_config_invalid" : "runtime_config_write_failed"
345
393
  };
346
394
  }
347
395
  }
@@ -365,24 +413,191 @@ function parseJsonObject(value) {
365
413
  }
366
414
  return parsed;
367
415
  }
368
- function removeTomlTable(toml, table) {
416
+ function removeTomlTableTree(toml, table) {
369
417
  const lines = toml.split(/\r?\n/);
370
- const start = `[${table}]`;
371
418
  const kept = [];
372
419
  let skipping = false;
373
420
  for (const line of lines) {
374
421
  const trimmed = line.trim();
375
- if (trimmed === start) {
376
- skipping = true;
377
- continue;
378
- }
379
- if (skipping && trimmed.startsWith("[") && trimmed.endsWith("]")) {
380
- skipping = false;
422
+ const tableName = tomlTableName(trimmed);
423
+ if (tableName) {
424
+ skipping = tableName === table || tableName.startsWith(`${table}.`);
425
+ if (skipping) continue;
381
426
  }
382
427
  if (!skipping) kept.push(line);
383
428
  }
384
429
  return kept.join("\n");
385
430
  }
431
+ function tomlTableName(line) {
432
+ if (line.startsWith("[[") && line.endsWith("]]")) return line.slice(2, -2).trim();
433
+ if (line.startsWith("[") && line.endsWith("]")) return line.slice(1, -1).trim();
434
+ return null;
435
+ }
436
+ function validateCodexToml(toml, label = "Codex config") {
437
+ const lines = toml.split(/\r?\n/);
438
+ let pendingValue = null;
439
+ for (let index = 0; index < lines.length; index += 1) {
440
+ const raw = lines[index];
441
+ const line = stripTomlComment(raw).trim();
442
+ if (!line) continue;
443
+ if (pendingValue) {
444
+ pendingValue.value = `${pendingValue.value}
445
+ ${line}`;
446
+ if (hasBalancedTomlContainers(pendingValue.value)) {
447
+ if (!isTomlValue(pendingValue.value)) {
448
+ throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${pendingValue.line}.`);
449
+ }
450
+ pendingValue = null;
451
+ }
452
+ continue;
453
+ }
454
+ if (isTomlTable(line)) continue;
455
+ const equalsIndex = line.indexOf("=");
456
+ if (equalsIndex <= 0) {
457
+ throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
458
+ }
459
+ const key = line.slice(0, equalsIndex).trim();
460
+ const value = line.slice(equalsIndex + 1).trim();
461
+ if (!isTomlKey(key)) {
462
+ throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
463
+ }
464
+ if (startsTomlContainer(value) && !hasBalancedTomlContainers(value)) {
465
+ pendingValue = { value, line: index + 1 };
466
+ continue;
467
+ }
468
+ if (!isTomlValue(value)) {
469
+ throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
470
+ }
471
+ }
472
+ if (pendingValue) {
473
+ throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${pendingValue.line}.`);
474
+ }
475
+ }
476
+ function isTomlTable(line) {
477
+ const table = tomlTableName(line);
478
+ return Boolean(table && splitTomlDottedKey(table).every(isTomlKeyPart));
479
+ }
480
+ function isTomlKey(value) {
481
+ return splitTomlDottedKey(value).every(isTomlKeyPart);
482
+ }
483
+ function splitTomlDottedKey(value) {
484
+ const parts = [];
485
+ let current = "";
486
+ let quote = null;
487
+ let escaped = false;
488
+ for (let i = 0; i < value.length; i += 1) {
489
+ const char = value[i];
490
+ if (quote) {
491
+ current += char;
492
+ if (quote === '"' && char === "\\" && !escaped) {
493
+ escaped = true;
494
+ continue;
495
+ }
496
+ if (char === quote && !escaped) quote = null;
497
+ escaped = false;
498
+ continue;
499
+ }
500
+ if (char === '"' || char === "'") {
501
+ quote = char;
502
+ current += char;
503
+ continue;
504
+ }
505
+ if (char === ".") {
506
+ parts.push(current.trim());
507
+ current = "";
508
+ continue;
509
+ }
510
+ current += char;
511
+ }
512
+ parts.push(current.trim());
513
+ return quote ? [] : parts;
514
+ }
515
+ function isTomlKeyPart(value) {
516
+ return isTomlBareKey(value) || isTomlQuotedString(value);
517
+ }
518
+ function isTomlBareKey(value) {
519
+ return /^[A-Za-z0-9_-]+$/.test(value);
520
+ }
521
+ function isTomlValue(value) {
522
+ if (!value) return false;
523
+ if (isTomlQuotedString(value)) return true;
524
+ if (/^(true|false)$/i.test(value)) return true;
525
+ if (/^[+-]?(?:inf|nan)$/i.test(value)) return true;
526
+ if (/^[+-]?(?:0|[1-9][0-9_]*)(?:\.[0-9_]+)?(?:[eE][+-]?[0-9_]+)?$/.test(value)) return true;
527
+ if (/^\d{4}-\d{2}-\d{2}(?:[Tt ][0-9:.+-Zz]+)?$/.test(value)) return true;
528
+ if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}")) {
529
+ return hasBalancedTomlContainers(value);
530
+ }
531
+ return false;
532
+ }
533
+ function startsTomlContainer(value) {
534
+ return value.startsWith("[") || value.startsWith("{");
535
+ }
536
+ function isTomlQuotedString(value) {
537
+ if (value.startsWith('"""') || value.startsWith("'''")) {
538
+ const marker = value.slice(0, 3);
539
+ return value.length >= 6 && value.endsWith(marker);
540
+ }
541
+ if ((!value.startsWith('"') || !value.endsWith('"')) && (!value.startsWith("'") || !value.endsWith("'"))) {
542
+ return false;
543
+ }
544
+ return hasBalancedTomlContainers(value);
545
+ }
546
+ function stripTomlComment(value) {
547
+ let quote = null;
548
+ let escaped = false;
549
+ for (let i = 0; i < value.length; i += 1) {
550
+ const char = value[i];
551
+ if (quote) {
552
+ if (quote === '"' && char === "\\" && !escaped) {
553
+ escaped = true;
554
+ continue;
555
+ }
556
+ if (char === quote && !escaped) quote = null;
557
+ escaped = false;
558
+ continue;
559
+ }
560
+ if (char === '"' || char === "'") {
561
+ quote = char;
562
+ continue;
563
+ }
564
+ if (char === "#") return value.slice(0, i);
565
+ }
566
+ return value;
567
+ }
568
+ function hasBalancedTomlContainers(value) {
569
+ const stack = [];
570
+ let quote = null;
571
+ let escaped = false;
572
+ for (let i = 0; i < value.length; i += 1) {
573
+ const char = value[i];
574
+ if (quote) {
575
+ if (quote === '"' && char === "\\" && !escaped) {
576
+ escaped = true;
577
+ continue;
578
+ }
579
+ if (char === quote && !escaped) quote = null;
580
+ escaped = false;
581
+ continue;
582
+ }
583
+ if (char === '"' || char === "'") {
584
+ quote = char;
585
+ continue;
586
+ }
587
+ if (char === "[" || char === "{") {
588
+ stack.push(char);
589
+ continue;
590
+ }
591
+ if (char === "]") {
592
+ if (stack.pop() !== "[") return false;
593
+ continue;
594
+ }
595
+ if (char === "}") {
596
+ if (stack.pop() !== "{") return false;
597
+ }
598
+ }
599
+ return stack.length === 0 && quote === null;
600
+ }
386
601
  function tomlString(value) {
387
602
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
388
603
  }
@@ -410,6 +625,10 @@ function claudeDesktopConfigPath(homeDir = os.homedir()) {
410
625
  }
411
626
  function configTargetLabel(runtime) {
412
627
  switch (runtime) {
628
+ case "codex-cli":
629
+ return "Codex CLI config";
630
+ case "codex-desktop":
631
+ return "Codex Desktop config";
413
632
  case "cursor":
414
633
  return "Cursor MCP config";
415
634
  case "vscode":
@@ -421,10 +640,7 @@ function configTargetLabel(runtime) {
421
640
  }
422
641
  }
423
642
  function signerPackageName() {
424
- return `@haven_ai/signer@${signer.SIGNER_VERSION}`;
425
- }
426
- function localMcpPackageName() {
427
- return `@haven_ai/mcp@${mcp.MCP_VERSION}`;
643
+ return signerPackageSpec();
428
644
  }
429
645
  async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
430
646
  try {
@@ -538,6 +754,72 @@ async function probeLocalSignerCredential(signerPath) {
538
754
  return false;
539
755
  }
540
756
  }
757
+ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
758
+ return new Promise((resolve6) => {
759
+ const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
760
+ let stdout = "";
761
+ let settled = false;
762
+ let sawInitialize = false;
763
+ const finish = (result) => {
764
+ if (settled) return;
765
+ settled = true;
766
+ clearTimeout(timeout);
767
+ child.kill();
768
+ resolve6(result);
769
+ };
770
+ const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
771
+ child.on("error", () => finish({ status: "process_error" }));
772
+ child.on("exit", (code) => {
773
+ if (!settled && code !== 0) finish({ status: "process_error" });
774
+ });
775
+ child.stdout.on("data", (chunk) => {
776
+ stdout += chunk.toString("utf8");
777
+ const lines = stdout.split(/\r?\n/);
778
+ stdout = lines.pop() ?? "";
779
+ for (const line of lines) {
780
+ const trimmed = line.trim();
781
+ if (!trimmed) continue;
782
+ let payload;
783
+ try {
784
+ payload = JSON.parse(trimmed);
785
+ } catch {
786
+ continue;
787
+ }
788
+ if (payload.error) {
789
+ finish({ status: "bad_response" });
790
+ return;
791
+ }
792
+ if (payload.id === 1 && !sawInitialize) {
793
+ sawInitialize = true;
794
+ writeJsonRpc(child, { jsonrpc: "2.0", method: "notifications/initialized", params: {} });
795
+ writeJsonRpc(child, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
796
+ continue;
797
+ }
798
+ if (payload.id === 2) {
799
+ const tools = payload.result?.tools;
800
+ const toolNames = Array.isArray(tools) ? tools.map((tool) => tool && typeof tool === "object" && "name" in tool ? tool.name : void 0).filter((name) => typeof name === "string") : [];
801
+ const missing = requiredTools.filter((name) => !toolNames.includes(name));
802
+ finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames });
803
+ return;
804
+ }
805
+ }
806
+ });
807
+ writeJsonRpc(child, {
808
+ jsonrpc: "2.0",
809
+ id: 1,
810
+ method: "initialize",
811
+ params: {
812
+ protocolVersion: "2025-06-18",
813
+ capabilities: {},
814
+ clientInfo: { name: "haven-connect-probe", version: "0.0.0" }
815
+ }
816
+ });
817
+ });
818
+ }
819
+ function writeJsonRpc(child, payload) {
820
+ child.stdin?.write(`${JSON.stringify(payload)}
821
+ `);
822
+ }
541
823
  function parseJsonRpcPayload(raw) {
542
824
  const trimmed = raw.trim();
543
825
  if (!trimmed) return null;
@@ -566,6 +848,161 @@ async function fetchWithTimeout(fetchImpl, url, init) {
566
848
  clearTimeout(timeout);
567
849
  }
568
850
  }
851
+ var execFileAsync = util.promisify(child_process.execFile);
852
+ var UnsupportedNodeVersionError = class extends Error {
853
+ code = "local_mcp_unsupported_node_version";
854
+ constructor(nodeVersion, minimumNodeVersion) {
855
+ super(`Node.js ${nodeVersion} is not supported. Haven local MCP requires Node.js >=${minimumNodeVersion}.`);
856
+ this.name = "UnsupportedNodeVersionError";
857
+ }
858
+ };
859
+ async function prepareLocalMcpRuntime(input, deps = {}) {
860
+ assertSupportedNodeVersion(input.nodeVersion);
861
+ const homeDir = input.homeDir ?? os.homedir();
862
+ const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
863
+ const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
864
+ const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
865
+ const messages = [];
866
+ await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
867
+ await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
868
+ await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
869
+ await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
870
+ if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
871
+ messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
872
+ } else {
873
+ await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps.runCommand);
874
+ messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
875
+ }
876
+ await assertFileExists(cliPath, "local Haven MCP CLI");
877
+ const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-mcp");
878
+ await writeWrapper({
879
+ wrapperPath,
880
+ cliPath,
881
+ identityPath: input.identityPath,
882
+ signerPath: input.signerPath
883
+ });
884
+ await writeRuntimeSidecar({
885
+ path: path.join(input.credentialDirectory, "mcp-runtime.json"),
886
+ wrapperPath,
887
+ runtimeDirectory,
888
+ npmCacheDirectory,
889
+ cliPath
890
+ });
891
+ messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
892
+ return {
893
+ command: wrapperPath,
894
+ args: [],
895
+ wrapperPath,
896
+ runtimeDirectory,
897
+ npmCacheDirectory,
898
+ cliPath,
899
+ messages
900
+ };
901
+ }
902
+ function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion) {
903
+ if (compareNodeVersions(nodeVersion, minimumNodeVersion) < 0) {
904
+ throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion);
905
+ }
906
+ }
907
+ function compareNodeVersions(left, right) {
908
+ const leftParts = parseNodeVersion(left);
909
+ const rightParts = parseNodeVersion(right);
910
+ for (let i = 0; i < 3; i += 1) {
911
+ if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
912
+ }
913
+ return 0;
914
+ }
915
+ function parseNodeVersion(value) {
916
+ const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
917
+ if (!match) return [0, 0, 0];
918
+ return [
919
+ Number(match[1] ?? 0),
920
+ Number(match[2] ?? 0),
921
+ Number(match[3] ?? 0)
922
+ ];
923
+ }
924
+ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCommand) {
925
+ const args = [
926
+ "install",
927
+ "--prefix",
928
+ runtimeDirectory,
929
+ "--cache",
930
+ npmCacheDirectory,
931
+ "--no-audit",
932
+ "--no-fund",
933
+ "--omit=dev",
934
+ mcpPackageSpec(),
935
+ sdkPackageSpec()
936
+ ];
937
+ try {
938
+ if (runCommand) await runCommand("npm", args);
939
+ else await execFileAsync("npm", args, { timeout: 12e4, maxBuffer: 1024 * 1024 });
940
+ } catch (err) {
941
+ throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
942
+ }
943
+ }
944
+ async function installedRuntimeMatches(runtimeDirectory, cliPath) {
945
+ try {
946
+ await assertFileExists(cliPath, "local Haven MCP CLI");
947
+ const [mcpPackage, sdkPackage] = await Promise.all([
948
+ readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
949
+ readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
950
+ ]);
951
+ return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
952
+ } catch {
953
+ return false;
954
+ }
955
+ }
956
+ async function readPackageJson(path) {
957
+ return JSON.parse(await promises.readFile(path, "utf8"));
958
+ }
959
+ async function writeWrapper(input) {
960
+ await promises.mkdir(path.dirname(input.wrapperPath), { recursive: true, mode: 448 });
961
+ await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
962
+ const source = [
963
+ "#!/usr/bin/env node",
964
+ "import { spawn } from 'node:child_process'",
965
+ "",
966
+ `const cliPath = ${JSON.stringify(input.cliPath)}`,
967
+ `const identityPath = ${JSON.stringify(input.identityPath)}`,
968
+ `const signerPath = ${JSON.stringify(input.signerPath)}`,
969
+ "",
970
+ "const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
971
+ " stdio: 'inherit',",
972
+ "})",
973
+ "",
974
+ "child.on('exit', (code, signal) => {",
975
+ " if (signal) process.kill(process.pid, signal)",
976
+ " else process.exit(code ?? 1)",
977
+ "})",
978
+ ""
979
+ ].join("\n");
980
+ await promises.writeFile(input.wrapperPath, source, { mode: 448 });
981
+ await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
982
+ }
983
+ async function writeRuntimeSidecar(input) {
984
+ const value = {
985
+ mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
986
+ mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
987
+ sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
988
+ sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
989
+ minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
990
+ wrapper_path: input.wrapperPath,
991
+ runtime_directory: input.runtimeDirectory,
992
+ npm_cache_directory: input.npmCacheDirectory,
993
+ cli_path: input.cliPath
994
+ };
995
+ await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
996
+ `, { mode: 384 });
997
+ await promises.chmod(input.path, 384).catch(() => void 0);
998
+ }
999
+ async function assertFileExists(path, label) {
1000
+ try {
1001
+ await promises.access(path);
1002
+ } catch {
1003
+ throw new Error(`Missing ${label}: ${path}`);
1004
+ }
1005
+ }
569
1006
 
570
1007
  // src/runtime-registry.ts
571
1008
  var RUNTIME_PROFILES = {
@@ -581,6 +1018,12 @@ var RUNTIME_PROFILES = {
581
1018
  restartMode: "restart-session",
582
1019
  canWriteRuntimeConfig: true
583
1020
  },
1021
+ "codex-desktop": {
1022
+ id: "codex-desktop",
1023
+ label: "Codex Desktop",
1024
+ restartMode: "restart-session",
1025
+ canWriteRuntimeConfig: true
1026
+ },
584
1027
  cursor: {
585
1028
  id: "cursor",
586
1029
  label: "Cursor",
@@ -615,6 +1058,12 @@ var RUNTIME_ALIASES = {
615
1058
  "codex-cli": "codex-cli",
616
1059
  codexcli: "codex-cli",
617
1060
  "codex_cli": "codex-cli",
1061
+ "codex-desktop": "codex-desktop",
1062
+ "codex_desktop": "codex-desktop",
1063
+ codexdesktop: "codex-desktop",
1064
+ "codex-app": "codex-desktop",
1065
+ "codex_app": "codex-desktop",
1066
+ codexapp: "codex-desktop",
618
1067
  cursor: "cursor",
619
1068
  vscode: "vscode",
620
1069
  "vs-code": "vscode",
@@ -721,7 +1170,7 @@ function writeLogChunk2(log, chunk) {
721
1170
  }
722
1171
 
723
1172
  // src/runtime-install.ts
724
- var execFileAsync = util.promisify(child_process.execFile);
1173
+ var execFileAsync2 = util.promisify(child_process.execFile);
725
1174
  async function installRuntime(input, deps = {}) {
726
1175
  const runtime = normalizeRuntime(input.runtime, deps.env);
727
1176
  const profile = runtimeProfile(runtime, deps.env);
@@ -751,31 +1200,66 @@ async function installRuntime(input, deps = {}) {
751
1200
  ]
752
1201
  };
753
1202
  }
754
- const configResult = runtime === "claude-code" ? await configureClaudeCode(input, deps) : await writeRuntimeConfig({
1203
+ let localRuntimeInstall;
1204
+ let localRuntimeError;
1205
+ if (localRuntime) {
1206
+ try {
1207
+ localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
1208
+ } catch (err) {
1209
+ localRuntimeError = err;
1210
+ }
1211
+ }
1212
+ if (localRuntimeError) {
1213
+ const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
1214
+ return {
1215
+ runtime,
1216
+ runtimeMcpMode: "local_stdio",
1217
+ hostedMcpConfigured: false,
1218
+ localSignerConfigured: false,
1219
+ localMcpConfigured: false,
1220
+ probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
1221
+ restartRequired: true,
1222
+ nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
1223
+ errorCode: errorCode2,
1224
+ configTarget: profile.label,
1225
+ signerAcknowledged: signerConsent?.acknowledged,
1226
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
1227
+ activationCommand: void 0,
1228
+ messages: [
1229
+ ...consentMessages,
1230
+ `Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
1231
+ ]
1232
+ };
1233
+ }
1234
+ const configResult = runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await writeRuntimeConfig({
755
1235
  runtime,
756
1236
  hostedMcpUrl: input.hostedMcpUrl,
757
1237
  apiKey: input.apiKey,
758
1238
  identityPath: input.identityPath,
759
1239
  signerPath: input.signerPath,
760
1240
  credentialDirectory: input.credentialDirectory,
1241
+ localMcpCommand: localRuntimeInstall?.command,
761
1242
  homeDir: deps.homeDir
762
1243
  });
763
- const [hostedProbe, signerCredentialReady] = await Promise.all([
1244
+ const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
1245
+ const [hostedProbe, signerCredentialReady, localMcpProbe] = await Promise.all([
764
1246
  configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
765
- probeLocalSignerCredential(input.signerPath)
1247
+ probeLocalSignerCredential(input.signerPath),
1248
+ localProbePromise
766
1249
  ]);
767
1250
  const hostedOk = configResult.hostedConfigured && hostedProbe.status !== "unauthorized";
768
- const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged);
1251
+ const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
769
1252
  const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
770
1253
  const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
771
- const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpConsentErrorCode(signerCredentialReady, localMcpConsent) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1254
+ const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1255
+ 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
1256
  return {
773
1257
  runtime,
774
1258
  runtimeMcpMode: configResult.runtimeMcpMode,
775
1259
  hostedMcpConfigured: hostedOk,
776
1260
  localSignerConfigured: signerOk,
777
1261
  localMcpConfigured: localMcpOk,
778
- probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk),
1262
+ probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
779
1263
  restartRequired,
780
1264
  nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
781
1265
  errorCode,
@@ -783,7 +1267,7 @@ async function installRuntime(input, deps = {}) {
783
1267
  signerAcknowledged: signerConsent?.acknowledged,
784
1268
  localMcpAcknowledged: localMcpConsent?.acknowledged,
785
1269
  activationCommand: configResult.activationCommand,
786
- messages: [...consentMessages, ...configResult.messages]
1270
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages]
787
1271
  };
788
1272
  }
789
1273
  function runtimeInstallCapabilities(runtime, env = process.env) {
@@ -793,23 +1277,21 @@ function runtimeInstallCapabilities(runtime, env = process.env) {
793
1277
  restartRequired: restartRequiredForRuntime(runtime, env)
794
1278
  };
795
1279
  }
796
- async function configureClaudeCode(input, deps) {
1280
+ async function configureClaudeCode(deps, localMcpCommand) {
797
1281
  const runCommand = deps.runCommand ?? defaultRunCommand;
1282
+ const serverJson = JSON.stringify({
1283
+ type: "stdio",
1284
+ command: localMcpCommand,
1285
+ args: [],
1286
+ env: {}
1287
+ });
798
1288
  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
- ]);
1289
+ if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
1290
+ await runCommand("claude", ["mcp", "add-json", "haven", serverJson, "--scope", "user"]).catch(async () => {
1291
+ await runCommand("claude", ["mcp", "add", "haven", "--scope", "user", "--", localMcpCommand]);
1292
+ });
812
1293
  await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
1294
+ const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
813
1295
  return {
814
1296
  hostedConfigured: false,
815
1297
  signerConfigured: true,
@@ -820,6 +1302,7 @@ async function configureClaudeCode(input, deps) {
820
1302
  restartRequired: true,
821
1303
  messages: [
822
1304
  "Updated local Haven MCP entry with Claude Code.",
1305
+ ...verified ? ["Verified Claude Code MCP entry."] : [],
823
1306
  "After Haven approval, restart Claude Code normally so it can load Haven tools."
824
1307
  ]
825
1308
  };
@@ -841,11 +1324,12 @@ async function configureClaudeCode(input, deps) {
841
1324
  }
842
1325
  }
843
1326
  async function defaultRunCommand(command, args) {
844
- await execFileAsync(command, args, { timeout: 1e4 });
1327
+ await execFileAsync2(command, args, { timeout: 1e4 });
845
1328
  }
846
- function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady) {
1329
+ function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
847
1330
  if (mode === "local_stdio") {
848
- return localMcpReady ? "local_stdio_mcp_ready" : "local_stdio_mcp_unavailable";
1331
+ if (localMcpReady) return "local_stdio_mcp_ready";
1332
+ return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
849
1333
  }
850
1334
  const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
851
1335
  const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
@@ -880,25 +1364,46 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
880
1364
  if (!signerConsent?.acknowledged) return "local_signer_ack_required";
881
1365
  return void 0;
882
1366
  }
883
- function localMcpConsentErrorCode(signerCredentialReady, localMcpConsent) {
1367
+ function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
884
1368
  if (!signerCredentialReady) return "local_signer_credential_unavailable";
885
1369
  if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
1370
+ if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
886
1371
  return void 0;
887
1372
  }
888
1373
  function nextAction(runtime, restartMode, errorCode) {
889
1374
  if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
890
1375
  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";
1376
+ if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
892
1377
  if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
893
1378
  if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
894
1379
  if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
895
1380
  return "return_to_haven_for_wallet_approval_then_configure_runtime";
896
1381
  }
897
- function localMcpPackageName2() {
898
- return `@haven_ai/mcp@${mcp.MCP_VERSION}`;
899
- }
900
1382
  function usesLocalMcp(runtime) {
901
- return runtime === "codex-cli" || runtime === "claude-code";
1383
+ return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
1384
+ }
1385
+ async function prepareRuntimeForLocalMcp(input, deps) {
1386
+ const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand }));
1387
+ return prepare({
1388
+ credentialDirectory: input.credentialDirectory,
1389
+ identityPath: input.identityPath,
1390
+ signerPath: input.signerPath,
1391
+ homeDir: deps.homeDir
1392
+ });
1393
+ }
1394
+ async function runLocalMcpProbe(runtimeInstall, deps) {
1395
+ const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
1396
+ try {
1397
+ return await probe(runtimeInstall.command, runtimeInstall.args, MCP_RUNTIME_MANIFEST.requiredTools);
1398
+ } catch {
1399
+ return { status: "process_error" };
1400
+ }
1401
+ }
1402
+ function localRuntimePrepareErrorCode(err) {
1403
+ if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
1404
+ return "local_mcp_unsupported_node_version";
1405
+ }
1406
+ return "local_mcp_runtime_install_failed";
902
1407
  }
903
1408
 
904
1409
  // src/runtime.ts
@@ -1097,7 +1602,7 @@ function helpText() {
1097
1602
  "Options:",
1098
1603
  " --setup <token> Short-lived setup token from Haven.",
1099
1604
  " --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.",
1605
+ " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, or claude-desktop.",
1101
1606
  " --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
1102
1607
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
1103
1608
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",