@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/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ import { pathToFileURL } from 'url';
2
3
  import crypto from 'crypto';
3
4
  import { Wallet } from 'ethers';
4
- import { mkdir, rm, chmod, access, writeFile, readFile } from 'fs/promises';
5
+ import { mkdir, rm, chmod, access, writeFile, readFile, unlink } from 'fs/promises';
5
6
  import { homedir, platform } from 'os';
6
7
  import { join, resolve, dirname } from 'path';
7
8
  import { execFile, spawn } from 'child_process';
8
9
  import { promisify } from 'util';
10
+ import { parseDocument, isMap, stringify } from 'yaml';
9
11
  import { registeredToolNames, MCP_VERSION, ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient } from '@haven_ai/mcp';
10
12
  import { HAVEN_MINIMUM_NODE_VERSION, isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, HAVEN_SKILL_MD } from '@haven_ai/sdk';
11
13
  import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
@@ -174,11 +176,12 @@ async function writeCredentialFiles(input) {
174
176
  agentPath,
175
177
  {
176
178
  agent_id: input.agentId,
179
+ delegate_address: input.delegateAddress,
177
180
  safe_address: input.safeAddress,
178
181
  chain_id: input.chainId,
179
182
  network: input.network,
180
183
  agent_budget: input.agentBudget,
181
- 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."
184
+ 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."
182
185
  },
183
186
  input.warn
184
187
  );
@@ -230,9 +233,9 @@ var MCP_RUNTIME_MANIFEST = {
230
233
  mcpPackage: "@haven_ai/mcp",
231
234
  mcpVersion: MCP_VERSION,
232
235
  sdkPackage: "@haven_ai/sdk",
233
- sdkVersion: "0.1.21-alpha.0",
236
+ sdkVersion: "0.1.23-alpha.0",
234
237
  signerPackage: "@haven_ai/signer",
235
- signerVersion: "0.1.21-alpha.0",
238
+ signerVersion: "0.1.23-alpha.0",
236
239
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
237
240
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
238
241
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -254,13 +257,20 @@ function signerPackageSpec() {
254
257
  }
255
258
 
256
259
  // src/config-writers.ts
260
+ var HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
261
+ var HermesConfigRecoveryError = class extends Error {
262
+ constructor() {
263
+ super("Hermes configuration recovery did not complete");
264
+ this.name = "HermesConfigRecoveryError";
265
+ }
266
+ };
257
267
  var InvalidCodexTomlError = class extends Error {
258
268
  constructor(message) {
259
269
  super(message);
260
270
  this.name = "InvalidCodexTomlError";
261
271
  }
262
272
  };
263
- async function writeRuntimeConfig(input) {
273
+ async function writeRuntimeConfig(input, deps = {}) {
264
274
  switch (input.runtime) {
265
275
  case "codex-cli":
266
276
  case "codex-desktop":
@@ -273,6 +283,8 @@ async function writeRuntimeConfig(input) {
273
283
  return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
274
284
  case "claude-desktop":
275
285
  return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
286
+ case "hermes":
287
+ return writeHermesConfig(input, deps);
276
288
  default:
277
289
  return {
278
290
  hostedConfigured: false,
@@ -326,6 +338,115 @@ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer
326
338
  return `${JSON.stringify(config, null, 2)}
327
339
  `;
328
340
  }
341
+ function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
342
+ if (!existingYaml?.trim()) return renderHermesYaml({ haven: hostedServer, "haven-signer": signerServer });
343
+ const doc = parseDocument(existingYaml, { keepSourceTokens: true });
344
+ if (doc.errors.length > 0 || !isMap(doc.contents)) {
345
+ throw new Error("Hermes config must be a YAML object");
346
+ }
347
+ const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
348
+ const existingServers = mcpPair && isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
349
+ const servers = isRecord(existingServers) ? existingServers : {};
350
+ const mergedServers = {
351
+ ...servers,
352
+ haven: hostedServer,
353
+ "haven-signer": signerServer
354
+ };
355
+ if (!mcpPair) {
356
+ return appendHermesMcpServers(existingYaml, mergedServers);
357
+ }
358
+ if (!mcpPair.value?.range) return replaceEmptyHermesMcpServers(existingYaml, mcpPair.key?.range, mergedServers);
359
+ return replaceHermesMcpServers(existingYaml, mcpPair.key?.range, mcpPair.value.range, mergedServers);
360
+ }
361
+ function mergeHermesEnv(existingEnv, apiKey) {
362
+ if (/[\r\n]/.test(apiKey)) throw new Error("Hermes API key must be a single line");
363
+ const assignment = `${HERMES_API_KEY_ENV}=${apiKey}`;
364
+ if (!existingEnv) return `${assignment}
365
+ `;
366
+ const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
367
+ const hasTrailingNewline = /\r?\n$/.test(existingEnv);
368
+ const lines = existingEnv.split(/\r?\n/);
369
+ if (hasTrailingNewline) lines.pop();
370
+ let found = false;
371
+ const merged = lines.flatMap((line) => {
372
+ if (isHermesEnvAssignment(line)) {
373
+ if (found) return [];
374
+ found = true;
375
+ return [assignment];
376
+ }
377
+ if (isAmbiguousHermesEnvLine(line)) {
378
+ throw new Error("Hermes environment contains an ambiguous managed key");
379
+ }
380
+ return [line];
381
+ });
382
+ if (!found) {
383
+ return `${existingEnv}${hasTrailingNewline ? "" : lineEnding}${assignment}${lineEnding}`;
384
+ }
385
+ return `${merged.join(lineEnding)}${hasTrailingNewline ? lineEnding : ""}`;
386
+ }
387
+ function isHermesEnvAssignment(line) {
388
+ return /^\s*(?:export[ \t]+)?MCP_HAVEN_API_KEY[ \t]*=/.test(line);
389
+ }
390
+ function isAmbiguousHermesEnvLine(line) {
391
+ return /^\s*(?:export[ \t]+)?MCP_HAVEN_API_KEY\b/.test(line);
392
+ }
393
+ function appendHermesMcpServers(source, servers) {
394
+ const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
395
+ if (documentEnd) {
396
+ const markerStart = documentEnd.index + (documentEnd[0].startsWith("\n") ? 1 : 0);
397
+ const beforeMarker = source.slice(0, markerStart);
398
+ return `${beforeMarker}${beforeMarker.endsWith("\n") ? "" : "\n"}${renderHermesMcpServers(servers, "")}
399
+ ${source.slice(markerStart)}`;
400
+ }
401
+ const separator = source.endsWith("\n") ? "" : "\n";
402
+ return `${source}${separator}${renderHermesMcpServers(servers, "")}
403
+ `;
404
+ }
405
+ function replaceHermesMcpServers(source, keyRange, valueRange, servers) {
406
+ const valueStart = valueRange[0];
407
+ const valueEnd = valueRange[1];
408
+ const valueLineStart = source.lastIndexOf("\n", valueStart - 1) + 1;
409
+ const keyStartsOnValueLine = keyRange?.[0] !== void 0 && keyRange[0] >= valueLineStart;
410
+ const nestedIndent = `${source.slice(valueLineStart, keyRange?.[0] ?? valueLineStart)} `;
411
+ if (keyStartsOnValueLine) {
412
+ const keyStart = keyRange[0];
413
+ const keyEnd = keyRange[1];
414
+ const replacement2 = `${source.slice(keyStart, keyEnd)}:
415
+ ${renderHermesMcpServerEntries(servers, nestedIndent)}`;
416
+ const needsTrailingNewline2 = valueEnd < source.length && !source.slice(valueEnd).startsWith("\n");
417
+ return `${source.slice(0, keyStart)}${replacement2}${needsTrailingNewline2 ? "\n" : ""}${source.slice(valueEnd)}`;
418
+ }
419
+ const indent = source.slice(valueLineStart, valueStart);
420
+ const rendered = stringify(servers, { lineWidth: 120 }).trimEnd();
421
+ const replacement = rendered.split("\n").map((line, index) => index === 0 ? line : `${indent}${line}`).join("\n");
422
+ const needsTrailingNewline = valueEnd < source.length && !source.slice(valueEnd).startsWith("\n");
423
+ return `${source.slice(0, valueStart)}${replacement}${needsTrailingNewline ? "\n" : ""}${source.slice(valueEnd)}`;
424
+ }
425
+ function replaceEmptyHermesMcpServers(source, keyRange, servers) {
426
+ if (!keyRange) throw new Error("Hermes config mcp_servers key is invalid");
427
+ const keyStart = keyRange[0];
428
+ const keyEnd = keyRange[1];
429
+ const lineStart = source.lastIndexOf("\n", keyStart - 1) + 1;
430
+ const lineEnd = source.indexOf("\n", keyEnd);
431
+ const nestedIndent = `${source.slice(lineStart, keyStart)} `;
432
+ const replacement = `${source.slice(keyStart, keyEnd)}:
433
+ ${renderHermesMcpServerEntries(servers, nestedIndent)}`;
434
+ return `${source.slice(0, keyStart)}${replacement}${lineEnd === -1 ? "\n" : source.slice(lineEnd)}`;
435
+ }
436
+ function renderHermesYaml(servers) {
437
+ return `${renderHermesMcpServers(servers, "")}
438
+ `;
439
+ }
440
+ function renderHermesMcpServers(servers, indent) {
441
+ return `mcp_servers:
442
+ ${renderHermesMcpServerEntries(servers, `${indent} `)}`;
443
+ }
444
+ function renderHermesMcpServerEntries(servers, indent) {
445
+ return stringify(servers, { lineWidth: 120 }).trimEnd().split("\n").map((line) => `${indent}${line}`).join("\n");
446
+ }
447
+ function isRecord(value) {
448
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
449
+ }
329
450
  function mergeCodexToml(existingToml, localMcpCommand) {
330
451
  let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
331
452
  next = next.trimEnd();
@@ -396,6 +517,82 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
396
517
  };
397
518
  }
398
519
  }
520
+ async function writeHermesConfig(input, deps) {
521
+ const target = hermesConfigPath(input.homeDir);
522
+ const envTarget = hermesEnvPath(input.homeDir);
523
+ try {
524
+ const [existing, existingEnv] = await Promise.all([readOptional(target), readOptional(envTarget)]);
525
+ const hostedServer = {
526
+ ...buildHostedServer(input.hostedMcpUrl, `\${${HERMES_API_KEY_ENV}}`, input.runtime),
527
+ enabled: true
528
+ };
529
+ const signerServer = {
530
+ ...buildSignerServer(resolveSignerLaunchSpec(input), input.runtime),
531
+ enabled: true
532
+ };
533
+ const merged = mergeHermesYaml(
534
+ existing,
535
+ hostedServer,
536
+ signerServer
537
+ );
538
+ const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey);
539
+ const writeText = deps.writeOwnerOnlyText ?? writeOwnerOnlyText;
540
+ await writeText(envTarget, mergedEnv);
541
+ try {
542
+ await writeText(target, merged);
543
+ } catch (err) {
544
+ const recovered = await restoreHermesFiles(target, existing, envTarget, existingEnv, writeText);
545
+ if (!recovered) throw new HermesConfigRecoveryError();
546
+ throw err;
547
+ }
548
+ return {
549
+ hostedConfigured: true,
550
+ signerConfigured: true,
551
+ localMcpConfigured: false,
552
+ runtimeMcpMode: "hosted_plus_signer",
553
+ target: "Hermes Agent config",
554
+ changed: existing !== merged || existingEnv !== mergedEnv,
555
+ restartRequired: true,
556
+ messages: [
557
+ `Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
558
+ "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`.",
559
+ "If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
560
+ ]
561
+ };
562
+ } catch (err) {
563
+ const recoveryIncomplete = err instanceof HermesConfigRecoveryError;
564
+ return {
565
+ hostedConfigured: false,
566
+ signerConfigured: false,
567
+ localMcpConfigured: false,
568
+ runtimeMcpMode: "hosted_plus_signer",
569
+ target: "Hermes Agent config",
570
+ changed: false,
571
+ restartRequired: true,
572
+ 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."],
573
+ errorCode: "runtime_config_write_failed"
574
+ };
575
+ }
576
+ }
577
+ async function restoreHermesFiles(configPath, existingConfig, envPath, existingEnv, writeText) {
578
+ const [envResult, configResult] = await Promise.allSettled([
579
+ restoreOptionalText(envPath, existingEnv, writeText),
580
+ restoreOptionalText(configPath, existingConfig, writeText)
581
+ ]);
582
+ return envResult.status === "fulfilled" && configResult.status === "fulfilled";
583
+ }
584
+ async function restoreOptionalText(path, existing, writeText) {
585
+ if (existing !== null) {
586
+ await writeText(path, existing);
587
+ return;
588
+ }
589
+ try {
590
+ await unlink(path);
591
+ } catch (err) {
592
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
593
+ throw err;
594
+ }
595
+ }
399
596
  async function writeCodexConfig(input) {
400
597
  const target = codexConfigPath(input.homeDir);
401
598
  const local = input.mode === "local";
@@ -686,6 +883,15 @@ function claudeDesktopConfigPath(homeDir = homedir()) {
686
883
  }
687
884
  return resolve(homeDir, ".config", "Claude", "claude_desktop_config.json");
688
885
  }
886
+ function hermesConfigPath(homeDir) {
887
+ return join(hermesHomePath(homeDir), "config.yaml");
888
+ }
889
+ function hermesEnvPath(homeDir) {
890
+ return join(hermesHomePath(homeDir), ".env");
891
+ }
892
+ function hermesHomePath(homeDir) {
893
+ return process.env.HERMES_HOME ?? join(homeDir ?? homedir(), ".hermes");
894
+ }
689
895
  function configTargetLabel(runtime) {
690
896
  switch (runtime) {
691
897
  case "codex-cli":
@@ -1191,49 +1397,64 @@ var RUNTIME_PROFILES = {
1191
1397
  id: "claude-code",
1192
1398
  label: "Claude Code",
1193
1399
  restartMode: "restart-session",
1194
- canWriteRuntimeConfig: true
1400
+ canWriteRuntimeConfig: true,
1401
+ activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
1195
1402
  },
1196
1403
  "codex-cli": {
1197
1404
  id: "codex-cli",
1198
1405
  label: "Codex CLI",
1199
1406
  restartMode: "restart-session",
1200
- canWriteRuntimeConfig: true
1407
+ canWriteRuntimeConfig: true,
1408
+ activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
1201
1409
  },
1202
1410
  "codex-desktop": {
1203
1411
  id: "codex-desktop",
1204
1412
  label: "Codex Desktop",
1205
1413
  restartMode: "restart-session",
1206
- canWriteRuntimeConfig: true
1414
+ canWriteRuntimeConfig: true,
1415
+ activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
1207
1416
  },
1208
1417
  cursor: {
1209
1418
  id: "cursor",
1210
1419
  label: "Cursor",
1211
1420
  restartMode: "hot-reload",
1212
- canWriteRuntimeConfig: true
1421
+ canWriteRuntimeConfig: true,
1422
+ activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
1213
1423
  },
1214
1424
  vscode: {
1215
1425
  id: "vscode",
1216
1426
  label: "VS Code",
1217
1427
  restartMode: "hot-reload",
1218
- canWriteRuntimeConfig: true
1428
+ canWriteRuntimeConfig: true,
1429
+ activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
1219
1430
  },
1220
1431
  "vscode-insiders": {
1221
1432
  id: "vscode-insiders",
1222
1433
  label: "VS Code Insiders",
1223
1434
  restartMode: "hot-reload",
1224
- canWriteRuntimeConfig: true
1435
+ canWriteRuntimeConfig: true,
1436
+ activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
1225
1437
  },
1226
1438
  "claude-desktop": {
1227
1439
  id: "claude-desktop",
1228
1440
  label: "Claude Desktop",
1229
1441
  restartMode: "restart-app",
1230
- canWriteRuntimeConfig: true
1442
+ canWriteRuntimeConfig: true,
1443
+ activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
1444
+ },
1445
+ hermes: {
1446
+ id: "hermes",
1447
+ label: "Hermes Agent",
1448
+ restartMode: "restart-session",
1449
+ canWriteRuntimeConfig: true,
1450
+ activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
1231
1451
  },
1232
1452
  other: {
1233
1453
  id: "other",
1234
1454
  label: "Other agent runtime",
1235
1455
  restartMode: "manual",
1236
- canWriteRuntimeConfig: false
1456
+ canWriteRuntimeConfig: false,
1457
+ activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
1237
1458
  }
1238
1459
  };
1239
1460
  var RUNTIME_ALIASES = {
@@ -1266,6 +1487,10 @@ var RUNTIME_ALIASES = {
1266
1487
  "claude_desktop": "claude-desktop",
1267
1488
  claudesktop: "claude-desktop",
1268
1489
  desktop: "claude-desktop",
1490
+ hermes: "hermes",
1491
+ "hermes-agent": "hermes",
1492
+ hermes_agent: "hermes",
1493
+ hermesagent: "hermes",
1269
1494
  other: "other",
1270
1495
  manual: "other"
1271
1496
  };
@@ -1281,8 +1506,9 @@ function restartRequiredForRuntime(runtime, env = process.env) {
1281
1506
  const mode = runtimeProfile(runtime, env).restartMode;
1282
1507
  return mode === "restart-session" || mode === "restart-app";
1283
1508
  }
1284
- function runtimeRequiresHardRestart(runtime) {
1285
- return runtime === "claude-desktop" || runtime === "codex-desktop";
1509
+ function runtimeVerificationInstruction(runtime) {
1510
+ const label = RUNTIME_PROFILES[runtime].label;
1511
+ 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.`;
1286
1512
  }
1287
1513
  function normalizeRuntimeName(runtime) {
1288
1514
  const key = runtime?.trim().toLowerCase();
@@ -1293,6 +1519,7 @@ function detectRuntime(env) {
1293
1519
  if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
1294
1520
  if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
1295
1521
  if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
1522
+ if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
1296
1523
  return null;
1297
1524
  }
1298
1525
  async function acknowledgeLocalSignerConsent(signerPath, log) {
@@ -1467,11 +1694,12 @@ async function installRuntime(input, deps = {}) {
1467
1694
  probeLocalSignerCredential(input.signerPath),
1468
1695
  localProbePromise
1469
1696
  ]);
1470
- const hostedOk = configResult.hostedConfigured && hostedProbe.status !== "unauthorized";
1697
+ const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
1471
1698
  const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
1472
1699
  const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
1473
1700
  const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
1474
- const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1701
+ const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent));
1702
+ 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."] : [];
1475
1703
  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."] : [];
1476
1704
  const skillInstall = runtime === "claude-code" && !configResult.errorCode ? await installClaudeSkill(deps.homeDir) : void 0;
1477
1705
  return {
@@ -1490,7 +1718,7 @@ async function installRuntime(input, deps = {}) {
1490
1718
  activationCommand: configResult.activationCommand,
1491
1719
  skillInstalled: skillInstall?.installed,
1492
1720
  signerRuntimePrepared,
1493
- messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1721
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1494
1722
  };
1495
1723
  }
1496
1724
  function runtimeInstallCapabilities(runtime, env = process.env) {
@@ -1651,6 +1879,10 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
1651
1879
  if (!signerConsent?.acknowledged) return "local_signer_ack_required";
1652
1880
  return void 0;
1653
1881
  }
1882
+ function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
1883
+ if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
1884
+ return `hosted_mcp_probe_${hostedProbeStatus}`;
1885
+ }
1654
1886
  function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
1655
1887
  if (!signerCredentialReady) return "local_signer_credential_unavailable";
1656
1888
  if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
@@ -1702,13 +1934,17 @@ function localRuntimePrepareErrorCode(err) {
1702
1934
  }
1703
1935
 
1704
1936
  // src/runtime.ts
1705
- var CONNECTOR_VERSION = "0.1.21-alpha.0";
1937
+ var CONNECTOR_VERSION = "0.1.23-alpha.0";
1938
+ var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
1706
1939
  async function runConnect(options, deps = {}) {
1707
1940
  assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
1708
1941
  const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
1709
1942
  const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
1710
- const log = secureLogger(deps.log ?? ((message) => process.stdout.write(`${message}
1711
- `)));
1943
+ const log = secureLogger(
1944
+ deps.log ?? ((message) => process.stdout.write(`${message}
1945
+ `)),
1946
+ deps.redactPaths === true
1947
+ );
1712
1948
  const writeCredentials = deps.writeCredentials ?? writeCredentialFiles;
1713
1949
  const preflightStorage = deps.preflightStorage ?? preflightCredentialStorage;
1714
1950
  const runRuntimeInstall = deps.installRuntime ?? installRuntime;
@@ -1729,6 +1965,7 @@ async function runConnect(options, deps = {}) {
1729
1965
  connectorVersion,
1730
1966
  runtime: options.runtime
1731
1967
  });
1968
+ assertSetupChallengeIsUsable(setup.challenge.expires_at);
1732
1969
  printSetupSummary(setup, log);
1733
1970
  await preflightStorage({ baseDir: options.credentialsDir, warn: log });
1734
1971
  log("Checked local credential storage \u2014 all clear.");
@@ -1737,21 +1974,31 @@ async function runConnect(options, deps = {}) {
1737
1974
  log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
1738
1975
  const proofSignature = await localKey.signChallenge(setup.challenge.message);
1739
1976
  log("Introducing your agent to Haven\u2026");
1740
- const registration = await api.registerSetup({
1741
- setupToken: options.setupToken,
1742
- connectorVersion,
1743
- runtime: options.runtime,
1744
- challengeId: setup.challenge.id,
1745
- delegateAddress: localKey.address,
1746
- proofSignature,
1747
- apiKeyHash: hashAgentApiKey(localApiKey),
1748
- apiKeyPrefix: agentApiKeyPrefix(localApiKey),
1749
- connectorContext: {
1750
- environment_label: options.environmentLabel ?? "Local workspace",
1751
- config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
1752
- },
1753
- installCapabilities
1754
- });
1977
+ let registration;
1978
+ try {
1979
+ registration = await api.registerSetup({
1980
+ setupToken: options.setupToken,
1981
+ connectorVersion,
1982
+ runtime: options.runtime,
1983
+ challengeId: setup.challenge.id,
1984
+ delegateAddress: localKey.address,
1985
+ proofSignature,
1986
+ apiKeyHash: hashAgentApiKey(localApiKey),
1987
+ apiKeyPrefix: agentApiKeyPrefix(localApiKey),
1988
+ connectorContext: {
1989
+ environment_label: options.environmentLabel ?? "Local workspace",
1990
+ config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
1991
+ },
1992
+ installCapabilities
1993
+ });
1994
+ } catch (err) {
1995
+ if (isExpiredSetupChallenge(err)) {
1996
+ throw new Error(
1997
+ "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."
1998
+ );
1999
+ }
2000
+ throw err;
2001
+ }
1755
2002
  log(`Registered signing address with Haven: ${shortAddress(registration.delegate_address)}.`);
1756
2003
  log("Tucking your credentials away safely on disk\u2026");
1757
2004
  const credentialPaths = await writeCredentials({
@@ -1828,7 +2075,69 @@ async function runConnect(options, deps = {}) {
1828
2075
  setupId: registration.setup_id,
1829
2076
  agentId: registration.agent_id,
1830
2077
  delegateAddress: registration.delegate_address,
1831
- credentialPaths
2078
+ credentialPaths,
2079
+ outcome: completionOutcome({
2080
+ runtimeInstall,
2081
+ delegateAddress: registration.delegate_address,
2082
+ setupChallengeExpiresAt: setup.challenge.expires_at,
2083
+ approvalRequired: registration.agent_status === "pending_approval"
2084
+ })
2085
+ };
2086
+ }
2087
+ function completionOutcome(input) {
2088
+ const { runtimeInstall } = input;
2089
+ const manualSetup = runtimeInstall.errorCode === "manual_runtime_setup_required";
2090
+ const nextAction2 = runtimeInstall.nextUserAction;
2091
+ const outcome = {
2092
+ schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
2093
+ outcome: runtimeInstall.errorCode ? "action_required" : "complete",
2094
+ runtime: runtimeInstall.runtime,
2095
+ topology: runtimeInstall.runtimeMcpMode,
2096
+ configuration: {
2097
+ hosted_mcp: runtimeInstall.hostedMcpConfigured,
2098
+ local_signer: runtimeInstall.localSignerConfigured,
2099
+ local_mcp: runtimeInstall.localMcpConfigured
2100
+ },
2101
+ probe: { result: runtimeInstall.probeResult },
2102
+ activation: {
2103
+ restart_required: runtimeInstall.restartRequired,
2104
+ 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
2105
+ },
2106
+ next_action: nextAction2,
2107
+ approval: { required: input.approvalRequired, expires_at: null },
2108
+ verification: {
2109
+ tools: ["haven_get_agent", "haven_get_allowances"],
2110
+ instruction: runtimeVerificationInstruction(runtimeInstall.runtime)
2111
+ },
2112
+ // The backend contract supplies a 20-byte address. If an unexpected
2113
+ // malformed value arrives, do not echo it into an automation-facing
2114
+ // record; the human log has already been redacted separately.
2115
+ delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
2116
+ ...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
2117
+ ...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
2118
+ };
2119
+ return outcome;
2120
+ }
2121
+ function failedConnectOutcome(runtimeHint, error) {
2122
+ const message = error instanceof Error ? error.message : "";
2123
+ 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";
2124
+ const runtime = normalizeRuntime(runtimeHint);
2125
+ 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";
2126
+ return {
2127
+ schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
2128
+ outcome: "failed",
2129
+ runtime,
2130
+ topology: "unknown",
2131
+ configuration: { hosted_mcp: false, local_signer: false, local_mcp: false },
2132
+ probe: { result: "not_run" },
2133
+ activation: { restart_required: false, instruction: "Resolve the reported problem before activating Haven tools." },
2134
+ next_action: nextAction2,
2135
+ approval: { required: false, expires_at: null },
2136
+ verification: {
2137
+ tools: ["haven_get_agent", "haven_get_allowances"],
2138
+ instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
2139
+ },
2140
+ error: { code, next_action: nextAction2 }
1832
2141
  };
1833
2142
  }
1834
2143
  function printSetupSummary(setup, log) {
@@ -1841,10 +2150,26 @@ function printSetupSummary(setup, log) {
1841
2150
  );
1842
2151
  }
1843
2152
  }
1844
- log(`Setup challenge expires at ${setup.challenge.expires_at}.`);
2153
+ 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.`);
2154
+ }
2155
+ function assertSetupChallengeIsUsable(expiresAt) {
2156
+ const expiresAtMs = Date.parse(expiresAt);
2157
+ if (!Number.isNaN(expiresAtMs) && expiresAtMs > Date.now()) return;
2158
+ throw new Error(
2159
+ "This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
2160
+ );
1845
2161
  }
1846
- function secureLogger(log) {
1847
- return (message) => log(redactSecrets(message));
2162
+ function isExpiredSetupChallenge(err) {
2163
+ return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
2164
+ }
2165
+ function secureLogger(log, redactPaths = false) {
2166
+ return (message) => {
2167
+ let safe = redactSecrets(message);
2168
+ if (redactPaths) {
2169
+ safe = safe.replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
2170
+ }
2171
+ log(safe);
2172
+ };
1848
2173
  }
1849
2174
  function printRuntimeInstall(result, log) {
1850
2175
  for (const message of result.messages) log(message);
@@ -1861,17 +2186,30 @@ function printRuntimeInstall(result, log) {
1861
2186
  log("Local Haven signer still needs runtime setup.");
1862
2187
  }
1863
2188
  }
1864
- function printNextSteps(result, log) {
1865
- log("Next: approve the agent rules in Haven. No Haven tools appear until you approve \u2014 restart or not.");
1866
- if (result.restartRequired) {
1867
- if (runtimeRequiresHardRestart(result.runtime)) {
1868
- log("After you approve, restart this agent so it can load Haven tools.");
1869
- } else {
1870
- log("After you approve, restart this agent \u2014 your current session won't load the Haven tools until you do.");
1871
- }
1872
- } else {
1873
- log("After you approve, the Haven tools should appear in this runtime shortly.");
2189
+ function completionHandoffLines(result) {
2190
+ if (result.errorCode === "manual_runtime_setup_required") {
2191
+ return [
2192
+ "Next steps:",
2193
+ "1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2194
+ "2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
2195
+ `3. ${runtimeVerificationInstruction(result.runtime)}`
2196
+ ];
2197
+ }
2198
+ if (result.errorCode) {
2199
+ return [
2200
+ "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."
2201
+ ];
1874
2202
  }
2203
+ const profile = runtimeProfile(result.runtime);
2204
+ return [
2205
+ "Next steps:",
2206
+ "1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2207
+ `2. ${profile.activationInstruction}`,
2208
+ `3. ${runtimeVerificationInstruction(result.runtime)}`
2209
+ ];
2210
+ }
2211
+ function printNextSteps(result, log) {
2212
+ for (const line of completionHandoffLines(result)) log(line);
1875
2213
  }
1876
2214
 
1877
2215
  // src/args.ts
@@ -1881,10 +2219,13 @@ function parseArgs(argv, env = process.env) {
1881
2219
  connectorVersion: CONNECTOR_VERSION
1882
2220
  };
1883
2221
  let help = false;
2222
+ let json = false;
1884
2223
  for (let i = 0; i < argv.length; i += 1) {
1885
2224
  const arg = argv[i];
1886
2225
  if (arg === "--help" || arg === "-h") {
1887
2226
  help = true;
2227
+ } else if (arg === "--json") {
2228
+ json = true;
1888
2229
  } else if (arg === "--setup" || arg === "--setup-token") {
1889
2230
  options.setupToken = requireValue(argv, ++i, arg);
1890
2231
  } else if (arg === "--api" || arg === "--api-url") {
@@ -1911,7 +2252,7 @@ function parseArgs(argv, env = process.env) {
1911
2252
  }
1912
2253
  }
1913
2254
  if (help) {
1914
- return { options, help };
2255
+ return { options, help, json };
1915
2256
  }
1916
2257
  if (!options.setupToken) {
1917
2258
  throw new Error("Missing --setup <hv_setup_...> setup token.");
@@ -1920,7 +2261,7 @@ function parseArgs(argv, env = process.env) {
1920
2261
  throw new Error("Missing --api <Haven API URL>.");
1921
2262
  }
1922
2263
  options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
1923
- return { options, help };
2264
+ return { options, help, json };
1924
2265
  }
1925
2266
  function helpText() {
1926
2267
  return [
@@ -1935,16 +2276,17 @@ function helpText() {
1935
2276
  "Options:",
1936
2277
  " --setup <token> Short-lived setup token from Haven.",
1937
2278
  " --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
1938
- " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, or claude-desktop.",
2279
+ " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, claude-desktop, or hermes.",
1939
2280
  " --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
1940
2281
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
1941
2282
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
1942
2283
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
1943
2284
  " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
1944
2285
  " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
2286
+ " --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
1945
2287
  " --help Show this help.",
1946
2288
  "",
1947
- "The connector never prints the private key and never sends it to Haven."
2289
+ "The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
1948
2290
  ].join("\n");
1949
2291
  }
1950
2292
  function requireValue(argv, index, option) {
@@ -1956,19 +2298,55 @@ function requireValue(argv, index, option) {
1956
2298
  }
1957
2299
 
1958
2300
  // src/cli.ts
1959
- async function main() {
1960
- const parsed = parseArgs(process.argv.slice(2));
2301
+ async function runCli(argv, io = {
2302
+ stdout: (message) => process.stdout.write(message),
2303
+ stderr: (message) => process.stderr.write(message)
2304
+ }) {
2305
+ const wantsJson = argv.includes("--json");
2306
+ let parsed;
2307
+ try {
2308
+ parsed = parseArgs(argv);
2309
+ } catch (err) {
2310
+ if (wantsJson) {
2311
+ io.stdout(`${JSON.stringify(failedConnectOutcome(void 0, err))}
2312
+ `);
2313
+ } else {
2314
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
2315
+ `);
2316
+ }
2317
+ return 1;
2318
+ }
1961
2319
  if (parsed.help) {
1962
- process.stdout.write(`${helpText()}
2320
+ io.stdout(`${helpText()}
1963
2321
  `);
1964
- return;
2322
+ return 0;
1965
2323
  }
1966
- await runConnect(parsed.options);
1967
- }
1968
- main().catch((err) => {
1969
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}
2324
+ try {
2325
+ const result = await runConnect(parsed.options, {
2326
+ log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
2327
+ `),
2328
+ redactPaths: parsed.json
2329
+ });
2330
+ if (parsed.json) io.stdout(`${JSON.stringify(result.outcome)}
2331
+ `);
2332
+ return 0;
2333
+ } catch (err) {
2334
+ if (parsed.json) {
2335
+ io.stdout(`${JSON.stringify(failedConnectOutcome(parsed.options.runtime, err))}
1970
2336
  `);
1971
- process.exit(1);
1972
- });
2337
+ } else {
2338
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
2339
+ `);
2340
+ }
2341
+ return 1;
2342
+ }
2343
+ }
2344
+ async function main() {
2345
+ const exitCode = await runCli(process.argv.slice(2));
2346
+ if (exitCode !== 0) process.exitCode = exitCode;
2347
+ }
2348
+ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) void main();
2349
+
2350
+ export { runCli };
1973
2351
  //# sourceMappingURL=cli.js.map
1974
2352
  //# sourceMappingURL=cli.js.map