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