@haven_ai/connect 0.1.21-alpha.0 → 0.1.23-alpha.0

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/index.cjs CHANGED
@@ -7,10 +7,13 @@ var os = require('os');
7
7
  var path = require('path');
8
8
  var child_process = require('child_process');
9
9
  var util = require('util');
10
+ var yaml = require('yaml');
10
11
  var mcp = require('@haven_ai/mcp');
11
12
  var sdk = require('@haven_ai/sdk');
12
13
  var signer = require('@haven_ai/signer');
14
+ var url = require('url');
13
15
 
16
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
14
17
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
18
 
16
19
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
@@ -179,11 +182,12 @@ async function writeCredentialFiles(input) {
179
182
  agentPath,
180
183
  {
181
184
  agent_id: input.agentId,
185
+ delegate_address: input.delegateAddress,
182
186
  safe_address: input.safeAddress,
183
187
  chain_id: input.chainId,
184
188
  network: input.network,
185
189
  agent_budget: input.agentBudget,
186
- note: "Non-secret orientation for the agent: identity + configured budget. Contains no API key or signing key. For the live remaining budget, call haven_get_allowances."
190
+ note: "Non-secret orientation for the agent: public delegate/Haven wallet identity + configured budget. Contains no API key or signing key. For the live remaining budget, call haven_get_allowances."
187
191
  },
188
192
  input.warn
189
193
  );
@@ -235,9 +239,9 @@ var MCP_RUNTIME_MANIFEST = {
235
239
  mcpPackage: "@haven_ai/mcp",
236
240
  mcpVersion: mcp.MCP_VERSION,
237
241
  sdkPackage: "@haven_ai/sdk",
238
- sdkVersion: "0.1.21-alpha.0",
242
+ sdkVersion: "0.1.23-alpha.0",
239
243
  signerPackage: "@haven_ai/signer",
240
- signerVersion: "0.1.21-alpha.0",
244
+ signerVersion: "0.1.23-alpha.0",
241
245
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
242
246
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
243
247
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -259,13 +263,20 @@ function signerPackageSpec() {
259
263
  }
260
264
 
261
265
  // src/config-writers.ts
266
+ var HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
267
+ var HermesConfigRecoveryError = class extends Error {
268
+ constructor() {
269
+ super("Hermes configuration recovery did not complete");
270
+ this.name = "HermesConfigRecoveryError";
271
+ }
272
+ };
262
273
  var InvalidCodexTomlError = class extends Error {
263
274
  constructor(message) {
264
275
  super(message);
265
276
  this.name = "InvalidCodexTomlError";
266
277
  }
267
278
  };
268
- async function writeRuntimeConfig(input) {
279
+ async function writeRuntimeConfig(input, deps = {}) {
269
280
  switch (input.runtime) {
270
281
  case "codex-cli":
271
282
  case "codex-desktop":
@@ -278,6 +289,8 @@ async function writeRuntimeConfig(input) {
278
289
  return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
279
290
  case "claude-desktop":
280
291
  return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
292
+ case "hermes":
293
+ return writeHermesConfig(input, deps);
281
294
  default:
282
295
  return {
283
296
  hostedConfigured: false,
@@ -331,6 +344,115 @@ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer
331
344
  return `${JSON.stringify(config, null, 2)}
332
345
  `;
333
346
  }
347
+ function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
348
+ if (!existingYaml?.trim()) return renderHermesYaml({ haven: hostedServer, "haven-signer": signerServer });
349
+ const doc = yaml.parseDocument(existingYaml, { keepSourceTokens: true });
350
+ if (doc.errors.length > 0 || !yaml.isMap(doc.contents)) {
351
+ throw new Error("Hermes config must be a YAML object");
352
+ }
353
+ const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
354
+ const existingServers = mcpPair && yaml.isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
355
+ const servers = isRecord(existingServers) ? existingServers : {};
356
+ const mergedServers = {
357
+ ...servers,
358
+ haven: hostedServer,
359
+ "haven-signer": signerServer
360
+ };
361
+ if (!mcpPair) {
362
+ return appendHermesMcpServers(existingYaml, mergedServers);
363
+ }
364
+ if (!mcpPair.value?.range) return replaceEmptyHermesMcpServers(existingYaml, mcpPair.key?.range, mergedServers);
365
+ return replaceHermesMcpServers(existingYaml, mcpPair.key?.range, mcpPair.value.range, mergedServers);
366
+ }
367
+ function mergeHermesEnv(existingEnv, apiKey) {
368
+ if (/[\r\n]/.test(apiKey)) throw new Error("Hermes API key must be a single line");
369
+ const assignment = `${HERMES_API_KEY_ENV}=${apiKey}`;
370
+ if (!existingEnv) return `${assignment}
371
+ `;
372
+ const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
373
+ const hasTrailingNewline = /\r?\n$/.test(existingEnv);
374
+ const lines = existingEnv.split(/\r?\n/);
375
+ if (hasTrailingNewline) lines.pop();
376
+ let found = false;
377
+ const merged = lines.flatMap((line) => {
378
+ if (isHermesEnvAssignment(line)) {
379
+ if (found) return [];
380
+ found = true;
381
+ return [assignment];
382
+ }
383
+ if (isAmbiguousHermesEnvLine(line)) {
384
+ throw new Error("Hermes environment contains an ambiguous managed key");
385
+ }
386
+ return [line];
387
+ });
388
+ if (!found) {
389
+ return `${existingEnv}${hasTrailingNewline ? "" : lineEnding}${assignment}${lineEnding}`;
390
+ }
391
+ return `${merged.join(lineEnding)}${hasTrailingNewline ? lineEnding : ""}`;
392
+ }
393
+ function isHermesEnvAssignment(line) {
394
+ return /^\s*(?:export[ \t]+)?MCP_HAVEN_API_KEY[ \t]*=/.test(line);
395
+ }
396
+ function isAmbiguousHermesEnvLine(line) {
397
+ return /^\s*(?:export[ \t]+)?MCP_HAVEN_API_KEY\b/.test(line);
398
+ }
399
+ function appendHermesMcpServers(source, servers) {
400
+ const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
401
+ if (documentEnd) {
402
+ const markerStart = documentEnd.index + (documentEnd[0].startsWith("\n") ? 1 : 0);
403
+ const beforeMarker = source.slice(0, markerStart);
404
+ return `${beforeMarker}${beforeMarker.endsWith("\n") ? "" : "\n"}${renderHermesMcpServers(servers, "")}
405
+ ${source.slice(markerStart)}`;
406
+ }
407
+ const separator = source.endsWith("\n") ? "" : "\n";
408
+ return `${source}${separator}${renderHermesMcpServers(servers, "")}
409
+ `;
410
+ }
411
+ function replaceHermesMcpServers(source, keyRange, valueRange, servers) {
412
+ const valueStart = valueRange[0];
413
+ const valueEnd = valueRange[1];
414
+ const valueLineStart = source.lastIndexOf("\n", valueStart - 1) + 1;
415
+ const keyStartsOnValueLine = keyRange?.[0] !== void 0 && keyRange[0] >= valueLineStart;
416
+ const nestedIndent = `${source.slice(valueLineStart, keyRange?.[0] ?? valueLineStart)} `;
417
+ if (keyStartsOnValueLine) {
418
+ const keyStart = keyRange[0];
419
+ const keyEnd = keyRange[1];
420
+ const replacement2 = `${source.slice(keyStart, keyEnd)}:
421
+ ${renderHermesMcpServerEntries(servers, nestedIndent)}`;
422
+ const needsTrailingNewline2 = valueEnd < source.length && !source.slice(valueEnd).startsWith("\n");
423
+ return `${source.slice(0, keyStart)}${replacement2}${needsTrailingNewline2 ? "\n" : ""}${source.slice(valueEnd)}`;
424
+ }
425
+ const indent = source.slice(valueLineStart, valueStart);
426
+ const rendered = yaml.stringify(servers, { lineWidth: 120 }).trimEnd();
427
+ const replacement = rendered.split("\n").map((line, index) => index === 0 ? line : `${indent}${line}`).join("\n");
428
+ const needsTrailingNewline = valueEnd < source.length && !source.slice(valueEnd).startsWith("\n");
429
+ return `${source.slice(0, valueStart)}${replacement}${needsTrailingNewline ? "\n" : ""}${source.slice(valueEnd)}`;
430
+ }
431
+ function replaceEmptyHermesMcpServers(source, keyRange, servers) {
432
+ if (!keyRange) throw new Error("Hermes config mcp_servers key is invalid");
433
+ const keyStart = keyRange[0];
434
+ const keyEnd = keyRange[1];
435
+ const lineStart = source.lastIndexOf("\n", keyStart - 1) + 1;
436
+ const lineEnd = source.indexOf("\n", keyEnd);
437
+ const nestedIndent = `${source.slice(lineStart, keyStart)} `;
438
+ const replacement = `${source.slice(keyStart, keyEnd)}:
439
+ ${renderHermesMcpServerEntries(servers, nestedIndent)}`;
440
+ return `${source.slice(0, keyStart)}${replacement}${lineEnd === -1 ? "\n" : source.slice(lineEnd)}`;
441
+ }
442
+ function renderHermesYaml(servers) {
443
+ return `${renderHermesMcpServers(servers, "")}
444
+ `;
445
+ }
446
+ function renderHermesMcpServers(servers, indent) {
447
+ return `mcp_servers:
448
+ ${renderHermesMcpServerEntries(servers, `${indent} `)}`;
449
+ }
450
+ function renderHermesMcpServerEntries(servers, indent) {
451
+ return yaml.stringify(servers, { lineWidth: 120 }).trimEnd().split("\n").map((line) => `${indent}${line}`).join("\n");
452
+ }
453
+ function isRecord(value) {
454
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
455
+ }
334
456
  function mergeCodexToml(existingToml, localMcpCommand) {
335
457
  let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
336
458
  next = next.trimEnd();
@@ -401,6 +523,82 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
401
523
  };
402
524
  }
403
525
  }
526
+ async function writeHermesConfig(input, deps) {
527
+ const target = hermesConfigPath(input.homeDir);
528
+ const envTarget = hermesEnvPath(input.homeDir);
529
+ try {
530
+ const [existing, existingEnv] = await Promise.all([readOptional(target), readOptional(envTarget)]);
531
+ const hostedServer = {
532
+ ...buildHostedServer(input.hostedMcpUrl, `\${${HERMES_API_KEY_ENV}}`, input.runtime),
533
+ enabled: true
534
+ };
535
+ const signerServer = {
536
+ ...buildSignerServer(resolveSignerLaunchSpec(input), input.runtime),
537
+ enabled: true
538
+ };
539
+ const merged = mergeHermesYaml(
540
+ existing,
541
+ hostedServer,
542
+ signerServer
543
+ );
544
+ const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey);
545
+ const writeText = deps.writeOwnerOnlyText ?? writeOwnerOnlyText;
546
+ await writeText(envTarget, mergedEnv);
547
+ try {
548
+ await writeText(target, merged);
549
+ } catch (err) {
550
+ const recovered = await restoreHermesFiles(target, existing, envTarget, existingEnv, writeText);
551
+ if (!recovered) throw new HermesConfigRecoveryError();
552
+ throw err;
553
+ }
554
+ return {
555
+ hostedConfigured: true,
556
+ signerConfigured: true,
557
+ localMcpConfigured: false,
558
+ runtimeMcpMode: "hosted_plus_signer",
559
+ target: "Hermes Agent config",
560
+ changed: existing !== merged || existingEnv !== mergedEnv,
561
+ restartRequired: true,
562
+ messages: [
563
+ `Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
564
+ "Restart Hermes (start a new session; gateway users: /restart), then verify with `hermes mcp list`, `hermes mcp test haven`, and `hermes mcp test haven-signer`.",
565
+ "If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
566
+ ]
567
+ };
568
+ } catch (err) {
569
+ const recoveryIncomplete = err instanceof HermesConfigRecoveryError;
570
+ return {
571
+ hostedConfigured: false,
572
+ signerConfigured: false,
573
+ localMcpConfigured: false,
574
+ runtimeMcpMode: "hosted_plus_signer",
575
+ target: "Hermes Agent config",
576
+ changed: false,
577
+ restartRequired: true,
578
+ messages: [recoveryIncomplete ? "Could not update Hermes Agent config. Recovery did not complete; inspect the Hermes configuration before retrying." : "Could not update Hermes Agent config. Existing configuration was left unchanged."],
579
+ errorCode: "runtime_config_write_failed"
580
+ };
581
+ }
582
+ }
583
+ async function restoreHermesFiles(configPath, existingConfig, envPath, existingEnv, writeText) {
584
+ const [envResult, configResult] = await Promise.allSettled([
585
+ restoreOptionalText(envPath, existingEnv, writeText),
586
+ restoreOptionalText(configPath, existingConfig, writeText)
587
+ ]);
588
+ return envResult.status === "fulfilled" && configResult.status === "fulfilled";
589
+ }
590
+ async function restoreOptionalText(path, existing, writeText) {
591
+ if (existing !== null) {
592
+ await writeText(path, existing);
593
+ return;
594
+ }
595
+ try {
596
+ await promises.unlink(path);
597
+ } catch (err) {
598
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
599
+ throw err;
600
+ }
601
+ }
404
602
  async function writeCodexConfig(input) {
405
603
  const target = codexConfigPath(input.homeDir);
406
604
  const local = input.mode === "local";
@@ -691,6 +889,15 @@ function claudeDesktopConfigPath(homeDir = os.homedir()) {
691
889
  }
692
890
  return path.resolve(homeDir, ".config", "Claude", "claude_desktop_config.json");
693
891
  }
892
+ function hermesConfigPath(homeDir) {
893
+ return path.join(hermesHomePath(homeDir), "config.yaml");
894
+ }
895
+ function hermesEnvPath(homeDir) {
896
+ return path.join(hermesHomePath(homeDir), ".env");
897
+ }
898
+ function hermesHomePath(homeDir) {
899
+ return process.env.HERMES_HOME ?? path.join(homeDir ?? os.homedir(), ".hermes");
900
+ }
694
901
  function configTargetLabel(runtime) {
695
902
  switch (runtime) {
696
903
  case "codex-cli":
@@ -1196,49 +1403,64 @@ var RUNTIME_PROFILES = {
1196
1403
  id: "claude-code",
1197
1404
  label: "Claude Code",
1198
1405
  restartMode: "restart-session",
1199
- canWriteRuntimeConfig: true
1406
+ canWriteRuntimeConfig: true,
1407
+ activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
1200
1408
  },
1201
1409
  "codex-cli": {
1202
1410
  id: "codex-cli",
1203
1411
  label: "Codex CLI",
1204
1412
  restartMode: "restart-session",
1205
- canWriteRuntimeConfig: true
1413
+ canWriteRuntimeConfig: true,
1414
+ activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
1206
1415
  },
1207
1416
  "codex-desktop": {
1208
1417
  id: "codex-desktop",
1209
1418
  label: "Codex Desktop",
1210
1419
  restartMode: "restart-session",
1211
- canWriteRuntimeConfig: true
1420
+ canWriteRuntimeConfig: true,
1421
+ activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
1212
1422
  },
1213
1423
  cursor: {
1214
1424
  id: "cursor",
1215
1425
  label: "Cursor",
1216
1426
  restartMode: "hot-reload",
1217
- canWriteRuntimeConfig: true
1427
+ canWriteRuntimeConfig: true,
1428
+ activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
1218
1429
  },
1219
1430
  vscode: {
1220
1431
  id: "vscode",
1221
1432
  label: "VS Code",
1222
1433
  restartMode: "hot-reload",
1223
- canWriteRuntimeConfig: true
1434
+ canWriteRuntimeConfig: true,
1435
+ activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
1224
1436
  },
1225
1437
  "vscode-insiders": {
1226
1438
  id: "vscode-insiders",
1227
1439
  label: "VS Code Insiders",
1228
1440
  restartMode: "hot-reload",
1229
- canWriteRuntimeConfig: true
1441
+ canWriteRuntimeConfig: true,
1442
+ activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
1230
1443
  },
1231
1444
  "claude-desktop": {
1232
1445
  id: "claude-desktop",
1233
1446
  label: "Claude Desktop",
1234
1447
  restartMode: "restart-app",
1235
- canWriteRuntimeConfig: true
1448
+ canWriteRuntimeConfig: true,
1449
+ activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
1450
+ },
1451
+ hermes: {
1452
+ id: "hermes",
1453
+ label: "Hermes Agent",
1454
+ restartMode: "restart-session",
1455
+ canWriteRuntimeConfig: true,
1456
+ activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
1236
1457
  },
1237
1458
  other: {
1238
1459
  id: "other",
1239
1460
  label: "Other agent runtime",
1240
1461
  restartMode: "manual",
1241
- canWriteRuntimeConfig: false
1462
+ canWriteRuntimeConfig: false,
1463
+ activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
1242
1464
  }
1243
1465
  };
1244
1466
  var RUNTIME_ALIASES = {
@@ -1271,6 +1493,10 @@ var RUNTIME_ALIASES = {
1271
1493
  "claude_desktop": "claude-desktop",
1272
1494
  claudesktop: "claude-desktop",
1273
1495
  desktop: "claude-desktop",
1496
+ hermes: "hermes",
1497
+ "hermes-agent": "hermes",
1498
+ hermes_agent: "hermes",
1499
+ hermesagent: "hermes",
1274
1500
  other: "other",
1275
1501
  manual: "other"
1276
1502
  };
@@ -1286,8 +1512,9 @@ function restartRequiredForRuntime(runtime, env = process.env) {
1286
1512
  const mode = runtimeProfile(runtime, env).restartMode;
1287
1513
  return mode === "restart-session" || mode === "restart-app";
1288
1514
  }
1289
- function runtimeRequiresHardRestart(runtime) {
1290
- return runtime === "claude-desktop" || runtime === "codex-desktop";
1515
+ function runtimeVerificationInstruction(runtime) {
1516
+ const label = RUNTIME_PROFILES[runtime].label;
1517
+ return `In ${label}, run the read-only \`haven_get_agent\` and \`haven_get_allowances\` tools to confirm the Haven wallet and live budget. Do not sign, fund, or create a payment to verify setup.`;
1291
1518
  }
1292
1519
  function normalizeRuntimeName(runtime) {
1293
1520
  const key = runtime?.trim().toLowerCase();
@@ -1298,6 +1525,7 @@ function detectRuntime(env) {
1298
1525
  if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
1299
1526
  if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
1300
1527
  if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
1528
+ if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
1301
1529
  return null;
1302
1530
  }
1303
1531
  async function acknowledgeLocalSignerConsent(signerPath, log) {
@@ -1472,11 +1700,12 @@ async function installRuntime(input, deps = {}) {
1472
1700
  probeLocalSignerCredential(input.signerPath),
1473
1701
  localProbePromise
1474
1702
  ]);
1475
- const hostedOk = configResult.hostedConfigured && hostedProbe.status !== "unauthorized";
1703
+ const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
1476
1704
  const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
1477
1705
  const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
1478
1706
  const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
1479
- const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1707
+ const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent));
1708
+ const hostedProbeMessages = configResult.hostedConfigured && hostedProbe.status !== "ok" ? [`Hosted Haven MCP probe failed: ${hostedProbe.status}.`] : configResult.hostedConfigured ? ["Verified hosted Haven MCP tools with a read-only handshake."] : [];
1480
1709
  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."] : [];
1481
1710
  const skillInstall = runtime === "claude-code" && !configResult.errorCode ? await installClaudeSkill(deps.homeDir) : void 0;
1482
1711
  return {
@@ -1495,7 +1724,7 @@ async function installRuntime(input, deps = {}) {
1495
1724
  activationCommand: configResult.activationCommand,
1496
1725
  skillInstalled: skillInstall?.installed,
1497
1726
  signerRuntimePrepared,
1498
- messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1727
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1499
1728
  };
1500
1729
  }
1501
1730
  function runtimeInstallCapabilities(runtime, env = process.env) {
@@ -1656,6 +1885,10 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
1656
1885
  if (!signerConsent?.acknowledged) return "local_signer_ack_required";
1657
1886
  return void 0;
1658
1887
  }
1888
+ function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
1889
+ if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
1890
+ return `hosted_mcp_probe_${hostedProbeStatus}`;
1891
+ }
1659
1892
  function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
1660
1893
  if (!signerCredentialReady) return "local_signer_credential_unavailable";
1661
1894
  if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
@@ -1707,13 +1940,17 @@ function localRuntimePrepareErrorCode(err) {
1707
1940
  }
1708
1941
 
1709
1942
  // src/runtime.ts
1710
- var CONNECTOR_VERSION = "0.1.21-alpha.0";
1943
+ var CONNECTOR_VERSION = "0.1.23-alpha.0";
1944
+ var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
1711
1945
  async function runConnect(options, deps = {}) {
1712
1946
  assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
1713
1947
  const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
1714
1948
  const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
1715
- const log = secureLogger(deps.log ?? ((message) => process.stdout.write(`${message}
1716
- `)));
1949
+ const log = secureLogger(
1950
+ deps.log ?? ((message) => process.stdout.write(`${message}
1951
+ `)),
1952
+ deps.redactPaths === true
1953
+ );
1717
1954
  const writeCredentials = deps.writeCredentials ?? writeCredentialFiles;
1718
1955
  const preflightStorage = deps.preflightStorage ?? preflightCredentialStorage;
1719
1956
  const runRuntimeInstall = deps.installRuntime ?? installRuntime;
@@ -1734,6 +1971,7 @@ async function runConnect(options, deps = {}) {
1734
1971
  connectorVersion,
1735
1972
  runtime: options.runtime
1736
1973
  });
1974
+ assertSetupChallengeIsUsable(setup.challenge.expires_at);
1737
1975
  printSetupSummary(setup, log);
1738
1976
  await preflightStorage({ baseDir: options.credentialsDir, warn: log });
1739
1977
  log("Checked local credential storage \u2014 all clear.");
@@ -1742,21 +1980,31 @@ async function runConnect(options, deps = {}) {
1742
1980
  log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
1743
1981
  const proofSignature = await localKey.signChallenge(setup.challenge.message);
1744
1982
  log("Introducing your agent to Haven\u2026");
1745
- const registration = await api.registerSetup({
1746
- setupToken: options.setupToken,
1747
- connectorVersion,
1748
- runtime: options.runtime,
1749
- challengeId: setup.challenge.id,
1750
- delegateAddress: localKey.address,
1751
- proofSignature,
1752
- apiKeyHash: hashAgentApiKey(localApiKey),
1753
- apiKeyPrefix: agentApiKeyPrefix(localApiKey),
1754
- connectorContext: {
1755
- environment_label: options.environmentLabel ?? "Local workspace",
1756
- config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
1757
- },
1758
- installCapabilities
1759
- });
1983
+ let registration;
1984
+ try {
1985
+ registration = await api.registerSetup({
1986
+ setupToken: options.setupToken,
1987
+ connectorVersion,
1988
+ runtime: options.runtime,
1989
+ challengeId: setup.challenge.id,
1990
+ delegateAddress: localKey.address,
1991
+ proofSignature,
1992
+ apiKeyHash: hashAgentApiKey(localApiKey),
1993
+ apiKeyPrefix: agentApiKeyPrefix(localApiKey),
1994
+ connectorContext: {
1995
+ environment_label: options.environmentLabel ?? "Local workspace",
1996
+ config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
1997
+ },
1998
+ installCapabilities
1999
+ });
2000
+ } catch (err) {
2001
+ if (isExpiredSetupChallenge(err)) {
2002
+ throw new Error(
2003
+ "The Haven setup challenge expired while connecting. Return to Haven, start a fresh connection, and run its new Connect command. Do not reuse or paste credentials."
2004
+ );
2005
+ }
2006
+ throw err;
2007
+ }
1760
2008
  log(`Registered signing address with Haven: ${shortAddress(registration.delegate_address)}.`);
1761
2009
  log("Tucking your credentials away safely on disk\u2026");
1762
2010
  const credentialPaths = await writeCredentials({
@@ -1833,7 +2081,69 @@ async function runConnect(options, deps = {}) {
1833
2081
  setupId: registration.setup_id,
1834
2082
  agentId: registration.agent_id,
1835
2083
  delegateAddress: registration.delegate_address,
1836
- credentialPaths
2084
+ credentialPaths,
2085
+ outcome: completionOutcome({
2086
+ runtimeInstall,
2087
+ delegateAddress: registration.delegate_address,
2088
+ setupChallengeExpiresAt: setup.challenge.expires_at,
2089
+ approvalRequired: registration.agent_status === "pending_approval"
2090
+ })
2091
+ };
2092
+ }
2093
+ function completionOutcome(input) {
2094
+ const { runtimeInstall } = input;
2095
+ const manualSetup = runtimeInstall.errorCode === "manual_runtime_setup_required";
2096
+ const nextAction2 = runtimeInstall.nextUserAction;
2097
+ const outcome = {
2098
+ schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
2099
+ outcome: runtimeInstall.errorCode ? "action_required" : "complete",
2100
+ runtime: runtimeInstall.runtime,
2101
+ topology: runtimeInstall.runtimeMcpMode,
2102
+ configuration: {
2103
+ hosted_mcp: runtimeInstall.hostedMcpConfigured,
2104
+ local_signer: runtimeInstall.localSignerConfigured,
2105
+ local_mcp: runtimeInstall.localMcpConfigured
2106
+ },
2107
+ probe: { result: runtimeInstall.probeResult },
2108
+ activation: {
2109
+ restart_required: runtimeInstall.restartRequired,
2110
+ instruction: manualSetup ? "Finish the manual MCP setup using the secret-free references shown in normal Connect output, then start a fresh session." : runtimeProfile(runtimeInstall.runtime).activationInstruction
2111
+ },
2112
+ next_action: nextAction2,
2113
+ approval: { required: input.approvalRequired, expires_at: null },
2114
+ verification: {
2115
+ tools: ["haven_get_agent", "haven_get_allowances"],
2116
+ instruction: runtimeVerificationInstruction(runtimeInstall.runtime)
2117
+ },
2118
+ // The backend contract supplies a 20-byte address. If an unexpected
2119
+ // malformed value arrives, do not echo it into an automation-facing
2120
+ // record; the human log has already been redacted separately.
2121
+ delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
2122
+ ...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
2123
+ ...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
2124
+ };
2125
+ return outcome;
2126
+ }
2127
+ function failedConnectOutcome(runtimeHint, error) {
2128
+ const message = error instanceof Error ? error.message : "";
2129
+ const code = /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
2130
+ const runtime = normalizeRuntime(runtimeHint);
2131
+ const nextAction2 = code === "setup_challenge_expired_or_invalid" ? "return_to_haven_for_fresh_setup" : code === "unsupported_node_version" ? "install_supported_node_and_rerun_connect" : code === "local_mcp_unsupported_runtime" ? "rerun_without_local_mcp" : "review_the_safe_error_output_and_start_a_fresh_haven_setup_if_needed";
2132
+ return {
2133
+ schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
2134
+ outcome: "failed",
2135
+ runtime,
2136
+ topology: "unknown",
2137
+ configuration: { hosted_mcp: false, local_signer: false, local_mcp: false },
2138
+ probe: { result: "not_run" },
2139
+ activation: { restart_required: false, instruction: "Resolve the reported problem before activating Haven tools." },
2140
+ next_action: nextAction2,
2141
+ approval: { required: false, expires_at: null },
2142
+ verification: {
2143
+ tools: ["haven_get_agent", "haven_get_allowances"],
2144
+ instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
2145
+ },
2146
+ error: { code, next_action: nextAction2 }
1837
2147
  };
1838
2148
  }
1839
2149
  function printSetupSummary(setup, log) {
@@ -1846,10 +2156,26 @@ function printSetupSummary(setup, log) {
1846
2156
  );
1847
2157
  }
1848
2158
  }
1849
- log(`Setup challenge expires at ${setup.challenge.expires_at}.`);
2159
+ log(`Setup challenge expires at ${setup.challenge.expires_at}. If it expires, return to Haven for a fresh setup and rerun Connect \u2014 do not reuse or paste credentials.`);
2160
+ }
2161
+ function assertSetupChallengeIsUsable(expiresAt) {
2162
+ const expiresAtMs = Date.parse(expiresAt);
2163
+ if (!Number.isNaN(expiresAtMs) && expiresAtMs > Date.now()) return;
2164
+ throw new Error(
2165
+ "This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
2166
+ );
1850
2167
  }
1851
- function secureLogger(log) {
1852
- return (message) => log(redactSecrets(message));
2168
+ function isExpiredSetupChallenge(err) {
2169
+ return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
2170
+ }
2171
+ function secureLogger(log, redactPaths = false) {
2172
+ return (message) => {
2173
+ let safe = redactSecrets(message);
2174
+ if (redactPaths) {
2175
+ safe = safe.replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
2176
+ }
2177
+ log(safe);
2178
+ };
1853
2179
  }
1854
2180
  function printRuntimeInstall(result, log) {
1855
2181
  for (const message of result.messages) log(message);
@@ -1866,17 +2192,30 @@ function printRuntimeInstall(result, log) {
1866
2192
  log("Local Haven signer still needs runtime setup.");
1867
2193
  }
1868
2194
  }
1869
- function printNextSteps(result, log) {
1870
- log("Next: approve the agent rules in Haven. No Haven tools appear until you approve \u2014 restart or not.");
1871
- if (result.restartRequired) {
1872
- if (runtimeRequiresHardRestart(result.runtime)) {
1873
- log("After you approve, restart this agent so it can load Haven tools.");
1874
- } else {
1875
- log("After you approve, restart this agent \u2014 your current session won't load the Haven tools until you do.");
1876
- }
1877
- } else {
1878
- log("After you approve, the Haven tools should appear in this runtime shortly.");
2195
+ function completionHandoffLines(result) {
2196
+ if (result.errorCode === "manual_runtime_setup_required") {
2197
+ return [
2198
+ "Next steps:",
2199
+ "1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2200
+ "2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
2201
+ `3. ${runtimeVerificationInstruction(result.runtime)}`
2202
+ ];
2203
+ }
2204
+ if (result.errorCode) {
2205
+ return [
2206
+ "Recovery: runtime setup is not complete. Resolve the reported problem, then return to Haven for a fresh connection and run its new Connect command. Do not manually edit runtime config or paste credentials into prompts, logs, or config."
2207
+ ];
1879
2208
  }
2209
+ const profile = runtimeProfile(result.runtime);
2210
+ return [
2211
+ "Next steps:",
2212
+ "1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2213
+ `2. ${profile.activationInstruction}`,
2214
+ `3. ${runtimeVerificationInstruction(result.runtime)}`
2215
+ ];
2216
+ }
2217
+ function printNextSteps(result, log) {
2218
+ for (const line of completionHandoffLines(result)) log(line);
1880
2219
  }
1881
2220
 
1882
2221
  // src/args.ts
@@ -1886,10 +2225,13 @@ function parseArgs(argv, env = process.env) {
1886
2225
  connectorVersion: CONNECTOR_VERSION
1887
2226
  };
1888
2227
  let help = false;
2228
+ let json = false;
1889
2229
  for (let i = 0; i < argv.length; i += 1) {
1890
2230
  const arg = argv[i];
1891
2231
  if (arg === "--help" || arg === "-h") {
1892
2232
  help = true;
2233
+ } else if (arg === "--json") {
2234
+ json = true;
1893
2235
  } else if (arg === "--setup" || arg === "--setup-token") {
1894
2236
  options.setupToken = requireValue(argv, ++i, arg);
1895
2237
  } else if (arg === "--api" || arg === "--api-url") {
@@ -1916,7 +2258,7 @@ function parseArgs(argv, env = process.env) {
1916
2258
  }
1917
2259
  }
1918
2260
  if (help) {
1919
- return { options, help };
2261
+ return { options, help, json };
1920
2262
  }
1921
2263
  if (!options.setupToken) {
1922
2264
  throw new Error("Missing --setup <hv_setup_...> setup token.");
@@ -1925,7 +2267,7 @@ function parseArgs(argv, env = process.env) {
1925
2267
  throw new Error("Missing --api <Haven API URL>.");
1926
2268
  }
1927
2269
  options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
1928
- return { options, help };
2270
+ return { options, help, json };
1929
2271
  }
1930
2272
  function helpText() {
1931
2273
  return [
@@ -1940,16 +2282,17 @@ function helpText() {
1940
2282
  "Options:",
1941
2283
  " --setup <token> Short-lived setup token from Haven.",
1942
2284
  " --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
1943
- " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, or claude-desktop.",
2285
+ " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, claude-desktop, or hermes.",
1944
2286
  " --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
1945
2287
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
1946
2288
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
1947
2289
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
1948
2290
  " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
1949
2291
  " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
2292
+ " --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
1950
2293
  " --help Show this help.",
1951
2294
  "",
1952
- "The connector never prints the private key and never sends it to Haven."
2295
+ "The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
1953
2296
  ].join("\n");
1954
2297
  }
1955
2298
  function requireValue(argv, index, option) {
@@ -1959,12 +2302,63 @@ function requireValue(argv, index, option) {
1959
2302
  }
1960
2303
  return value;
1961
2304
  }
2305
+ async function runCli(argv, io = {
2306
+ stdout: (message) => process.stdout.write(message),
2307
+ stderr: (message) => process.stderr.write(message)
2308
+ }) {
2309
+ const wantsJson = argv.includes("--json");
2310
+ let parsed;
2311
+ try {
2312
+ parsed = parseArgs(argv);
2313
+ } catch (err) {
2314
+ if (wantsJson) {
2315
+ io.stdout(`${JSON.stringify(failedConnectOutcome(void 0, err))}
2316
+ `);
2317
+ } else {
2318
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
2319
+ `);
2320
+ }
2321
+ return 1;
2322
+ }
2323
+ if (parsed.help) {
2324
+ io.stdout(`${helpText()}
2325
+ `);
2326
+ return 0;
2327
+ }
2328
+ try {
2329
+ const result = await runConnect(parsed.options, {
2330
+ log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
2331
+ `),
2332
+ redactPaths: parsed.json
2333
+ });
2334
+ if (parsed.json) io.stdout(`${JSON.stringify(result.outcome)}
2335
+ `);
2336
+ return 0;
2337
+ } catch (err) {
2338
+ if (parsed.json) {
2339
+ io.stdout(`${JSON.stringify(failedConnectOutcome(parsed.options.runtime, err))}
2340
+ `);
2341
+ } else {
2342
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
2343
+ `);
2344
+ }
2345
+ return 1;
2346
+ }
2347
+ }
2348
+ async function main() {
2349
+ const exitCode = await runCli(process.argv.slice(2));
2350
+ if (exitCode !== 0) process.exitCode = exitCode;
2351
+ }
2352
+ if (process.argv[1] && url.pathToFileURL(process.argv[1]).href === (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))) void main();
1962
2353
 
1963
2354
  exports.CONNECTOR_VERSION = CONNECTOR_VERSION;
2355
+ exports.CONNECT_OUTCOME_SCHEMA_VERSION = CONNECT_OUTCOME_SCHEMA_VERSION;
1964
2356
  exports.MCP_RUNTIME_MANIFEST = MCP_RUNTIME_MANIFEST;
2357
+ exports.completionOutcome = completionOutcome;
1965
2358
  exports.createConnectApiClient = createConnectApiClient;
1966
2359
  exports.defaultAgentDirectory = defaultAgentDirectory;
1967
2360
  exports.delegateKeyFromPrivateKey = delegateKeyFromPrivateKey;
2361
+ exports.failedConnectOutcome = failedConnectOutcome;
1968
2362
  exports.generateDelegateKey = generateDelegateKey;
1969
2363
  exports.helpText = helpText;
1970
2364
  exports.installRuntime = installRuntime;
@@ -1973,6 +2367,7 @@ exports.normalizeRuntime = normalizeRuntime;
1973
2367
  exports.parseArgs = parseArgs;
1974
2368
  exports.prepareSignerRuntime = prepareSignerRuntime;
1975
2369
  exports.redactSecrets = redactSecrets;
2370
+ exports.runCli = runCli;
1976
2371
  exports.runConnect = runConnect;
1977
2372
  exports.runtimeInstallCapabilities = runtimeInstallCapabilities;
1978
2373
  exports.runtimeProfile = runtimeProfile;