@haven_ai/connect 0.1.21-alpha.0 → 0.1.22-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/README.md CHANGED
@@ -25,3 +25,25 @@ Do not point it at a project repository, shared folder, or cloud-synced folder.
25
25
  Use `--ack-local-tools` with Haven-generated setup prompts. It prepares the
26
26
  local Haven tools acknowledgement during setup so Codex and Claude Code can load
27
27
  Haven after a normal restart.
28
+
29
+ ## Supported runtimes
30
+
31
+ The default setup writes the hosted Haven MCP (using the agent API key for
32
+ identity) plus a separate local signer. The API key identifies the agent; the
33
+ locally held signer key and the user's approved Haven wallet rules remain the
34
+ spending authority.
35
+
36
+ | Runtime | Configuration written by setup | Reload behaviour |
37
+ | --- | --- | --- |
38
+ | Claude Code | User MCP registry | Start a new session |
39
+ | Codex CLI / Codex Desktop | `~/.codex/config.toml` | Start a new session / restart the app |
40
+ | Cursor | Cursor MCP configuration | Reloads automatically |
41
+ | VS Code / VS Code Insiders | VS Code MCP configuration | Reloads automatically |
42
+ | Claude Desktop | Claude Desktop MCP configuration | Restart the app |
43
+ | Hermes Agent | `$HERMES_HOME/config.yaml` + `.env`, or `~/.hermes/config.yaml` + `.env` | Start a new session; gateway users run `/restart` |
44
+
45
+ For Hermes, Connect stores the hosted-MCP API key in the matching owner-only
46
+ `.env` file and keeps only `Bearer ${MCP_HAVEN_API_KEY}` in `config.yaml`.
47
+ Hermes requires its Python MCP SDK support to be installed. If Haven tools do
48
+ not appear after restart, run `pip install mcp` in the Hermes environment, then
49
+ restart Hermes and check `hermes mcp list`.
package/dist/cli.cjs CHANGED
@@ -8,6 +8,7 @@ var os = require('os');
8
8
  var path = require('path');
9
9
  var child_process = require('child_process');
10
10
  var util = require('util');
11
+ var yaml = require('yaml');
11
12
  var mcp = require('@haven_ai/mcp');
12
13
  var sdk = require('@haven_ai/sdk');
13
14
  var signer = require('@haven_ai/signer');
@@ -236,9 +237,9 @@ var MCP_RUNTIME_MANIFEST = {
236
237
  mcpPackage: "@haven_ai/mcp",
237
238
  mcpVersion: mcp.MCP_VERSION,
238
239
  sdkPackage: "@haven_ai/sdk",
239
- sdkVersion: "0.1.21-alpha.0",
240
+ sdkVersion: "0.1.22-alpha.0",
240
241
  signerPackage: "@haven_ai/signer",
241
- signerVersion: "0.1.21-alpha.0",
242
+ signerVersion: "0.1.22-alpha.0",
242
243
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
243
244
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
244
245
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -260,13 +261,20 @@ function signerPackageSpec() {
260
261
  }
261
262
 
262
263
  // src/config-writers.ts
264
+ var HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
265
+ var HermesConfigRecoveryError = class extends Error {
266
+ constructor() {
267
+ super("Hermes configuration recovery did not complete");
268
+ this.name = "HermesConfigRecoveryError";
269
+ }
270
+ };
263
271
  var InvalidCodexTomlError = class extends Error {
264
272
  constructor(message) {
265
273
  super(message);
266
274
  this.name = "InvalidCodexTomlError";
267
275
  }
268
276
  };
269
- async function writeRuntimeConfig(input) {
277
+ async function writeRuntimeConfig(input, deps = {}) {
270
278
  switch (input.runtime) {
271
279
  case "codex-cli":
272
280
  case "codex-desktop":
@@ -279,6 +287,8 @@ async function writeRuntimeConfig(input) {
279
287
  return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
280
288
  case "claude-desktop":
281
289
  return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
290
+ case "hermes":
291
+ return writeHermesConfig(input, deps);
282
292
  default:
283
293
  return {
284
294
  hostedConfigured: false,
@@ -332,6 +342,115 @@ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer
332
342
  return `${JSON.stringify(config, null, 2)}
333
343
  `;
334
344
  }
345
+ function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
346
+ if (!existingYaml?.trim()) return renderHermesYaml({ haven: hostedServer, "haven-signer": signerServer });
347
+ const doc = yaml.parseDocument(existingYaml, { keepSourceTokens: true });
348
+ if (doc.errors.length > 0 || !yaml.isMap(doc.contents)) {
349
+ throw new Error("Hermes config must be a YAML object");
350
+ }
351
+ const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
352
+ const existingServers = mcpPair && yaml.isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
353
+ const servers = isRecord(existingServers) ? existingServers : {};
354
+ const mergedServers = {
355
+ ...servers,
356
+ haven: hostedServer,
357
+ "haven-signer": signerServer
358
+ };
359
+ if (!mcpPair) {
360
+ return appendHermesMcpServers(existingYaml, mergedServers);
361
+ }
362
+ if (!mcpPair.value?.range) return replaceEmptyHermesMcpServers(existingYaml, mcpPair.key?.range, mergedServers);
363
+ return replaceHermesMcpServers(existingYaml, mcpPair.key?.range, mcpPair.value.range, mergedServers);
364
+ }
365
+ function mergeHermesEnv(existingEnv, apiKey) {
366
+ if (/[\r\n]/.test(apiKey)) throw new Error("Hermes API key must be a single line");
367
+ const assignment = `${HERMES_API_KEY_ENV}=${apiKey}`;
368
+ if (!existingEnv) return `${assignment}
369
+ `;
370
+ const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
371
+ const hasTrailingNewline = /\r?\n$/.test(existingEnv);
372
+ const lines = existingEnv.split(/\r?\n/);
373
+ if (hasTrailingNewline) lines.pop();
374
+ let found = false;
375
+ const merged = lines.flatMap((line) => {
376
+ if (isHermesEnvAssignment(line)) {
377
+ if (found) return [];
378
+ found = true;
379
+ return [assignment];
380
+ }
381
+ if (isAmbiguousHermesEnvLine(line)) {
382
+ throw new Error("Hermes environment contains an ambiguous managed key");
383
+ }
384
+ return [line];
385
+ });
386
+ if (!found) {
387
+ return `${existingEnv}${hasTrailingNewline ? "" : lineEnding}${assignment}${lineEnding}`;
388
+ }
389
+ return `${merged.join(lineEnding)}${hasTrailingNewline ? lineEnding : ""}`;
390
+ }
391
+ function isHermesEnvAssignment(line) {
392
+ return /^\s*(?:export[ \t]+)?MCP_HAVEN_API_KEY[ \t]*=/.test(line);
393
+ }
394
+ function isAmbiguousHermesEnvLine(line) {
395
+ return /^\s*(?:export[ \t]+)?MCP_HAVEN_API_KEY\b/.test(line);
396
+ }
397
+ function appendHermesMcpServers(source, servers) {
398
+ const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
399
+ if (documentEnd) {
400
+ const markerStart = documentEnd.index + (documentEnd[0].startsWith("\n") ? 1 : 0);
401
+ const beforeMarker = source.slice(0, markerStart);
402
+ return `${beforeMarker}${beforeMarker.endsWith("\n") ? "" : "\n"}${renderHermesMcpServers(servers, "")}
403
+ ${source.slice(markerStart)}`;
404
+ }
405
+ const separator = source.endsWith("\n") ? "" : "\n";
406
+ return `${source}${separator}${renderHermesMcpServers(servers, "")}
407
+ `;
408
+ }
409
+ function replaceHermesMcpServers(source, keyRange, valueRange, servers) {
410
+ const valueStart = valueRange[0];
411
+ const valueEnd = valueRange[1];
412
+ const valueLineStart = source.lastIndexOf("\n", valueStart - 1) + 1;
413
+ const keyStartsOnValueLine = keyRange?.[0] !== void 0 && keyRange[0] >= valueLineStart;
414
+ const nestedIndent = `${source.slice(valueLineStart, keyRange?.[0] ?? valueLineStart)} `;
415
+ if (keyStartsOnValueLine) {
416
+ const keyStart = keyRange[0];
417
+ const keyEnd = keyRange[1];
418
+ const replacement2 = `${source.slice(keyStart, keyEnd)}:
419
+ ${renderHermesMcpServerEntries(servers, nestedIndent)}`;
420
+ const needsTrailingNewline2 = valueEnd < source.length && !source.slice(valueEnd).startsWith("\n");
421
+ return `${source.slice(0, keyStart)}${replacement2}${needsTrailingNewline2 ? "\n" : ""}${source.slice(valueEnd)}`;
422
+ }
423
+ const indent = source.slice(valueLineStart, valueStart);
424
+ const rendered = yaml.stringify(servers, { lineWidth: 120 }).trimEnd();
425
+ const replacement = rendered.split("\n").map((line, index) => index === 0 ? line : `${indent}${line}`).join("\n");
426
+ const needsTrailingNewline = valueEnd < source.length && !source.slice(valueEnd).startsWith("\n");
427
+ return `${source.slice(0, valueStart)}${replacement}${needsTrailingNewline ? "\n" : ""}${source.slice(valueEnd)}`;
428
+ }
429
+ function replaceEmptyHermesMcpServers(source, keyRange, servers) {
430
+ if (!keyRange) throw new Error("Hermes config mcp_servers key is invalid");
431
+ const keyStart = keyRange[0];
432
+ const keyEnd = keyRange[1];
433
+ const lineStart = source.lastIndexOf("\n", keyStart - 1) + 1;
434
+ const lineEnd = source.indexOf("\n", keyEnd);
435
+ const nestedIndent = `${source.slice(lineStart, keyStart)} `;
436
+ const replacement = `${source.slice(keyStart, keyEnd)}:
437
+ ${renderHermesMcpServerEntries(servers, nestedIndent)}`;
438
+ return `${source.slice(0, keyStart)}${replacement}${lineEnd === -1 ? "\n" : source.slice(lineEnd)}`;
439
+ }
440
+ function renderHermesYaml(servers) {
441
+ return `${renderHermesMcpServers(servers, "")}
442
+ `;
443
+ }
444
+ function renderHermesMcpServers(servers, indent) {
445
+ return `mcp_servers:
446
+ ${renderHermesMcpServerEntries(servers, `${indent} `)}`;
447
+ }
448
+ function renderHermesMcpServerEntries(servers, indent) {
449
+ return yaml.stringify(servers, { lineWidth: 120 }).trimEnd().split("\n").map((line) => `${indent}${line}`).join("\n");
450
+ }
451
+ function isRecord(value) {
452
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
453
+ }
335
454
  function mergeCodexToml(existingToml, localMcpCommand) {
336
455
  let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
337
456
  next = next.trimEnd();
@@ -402,6 +521,82 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
402
521
  };
403
522
  }
404
523
  }
524
+ async function writeHermesConfig(input, deps) {
525
+ const target = hermesConfigPath(input.homeDir);
526
+ const envTarget = hermesEnvPath(input.homeDir);
527
+ try {
528
+ const [existing, existingEnv] = await Promise.all([readOptional(target), readOptional(envTarget)]);
529
+ const hostedServer = {
530
+ ...buildHostedServer(input.hostedMcpUrl, `\${${HERMES_API_KEY_ENV}}`, input.runtime),
531
+ enabled: true
532
+ };
533
+ const signerServer = {
534
+ ...buildSignerServer(resolveSignerLaunchSpec(input), input.runtime),
535
+ enabled: true
536
+ };
537
+ const merged = mergeHermesYaml(
538
+ existing,
539
+ hostedServer,
540
+ signerServer
541
+ );
542
+ const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey);
543
+ const writeText = deps.writeOwnerOnlyText ?? writeOwnerOnlyText;
544
+ await writeText(envTarget, mergedEnv);
545
+ try {
546
+ await writeText(target, merged);
547
+ } catch (err) {
548
+ const recovered = await restoreHermesFiles(target, existing, envTarget, existingEnv, writeText);
549
+ if (!recovered) throw new HermesConfigRecoveryError();
550
+ throw err;
551
+ }
552
+ return {
553
+ hostedConfigured: true,
554
+ signerConfigured: true,
555
+ localMcpConfigured: false,
556
+ runtimeMcpMode: "hosted_plus_signer",
557
+ target: "Hermes Agent config",
558
+ changed: existing !== merged || existingEnv !== mergedEnv,
559
+ restartRequired: true,
560
+ messages: [
561
+ `Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
562
+ "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`.",
563
+ "If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
564
+ ]
565
+ };
566
+ } catch (err) {
567
+ const recoveryIncomplete = err instanceof HermesConfigRecoveryError;
568
+ return {
569
+ hostedConfigured: false,
570
+ signerConfigured: false,
571
+ localMcpConfigured: false,
572
+ runtimeMcpMode: "hosted_plus_signer",
573
+ target: "Hermes Agent config",
574
+ changed: false,
575
+ restartRequired: true,
576
+ 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."],
577
+ errorCode: "runtime_config_write_failed"
578
+ };
579
+ }
580
+ }
581
+ async function restoreHermesFiles(configPath, existingConfig, envPath, existingEnv, writeText) {
582
+ const [envResult, configResult] = await Promise.allSettled([
583
+ restoreOptionalText(envPath, existingEnv, writeText),
584
+ restoreOptionalText(configPath, existingConfig, writeText)
585
+ ]);
586
+ return envResult.status === "fulfilled" && configResult.status === "fulfilled";
587
+ }
588
+ async function restoreOptionalText(path, existing, writeText) {
589
+ if (existing !== null) {
590
+ await writeText(path, existing);
591
+ return;
592
+ }
593
+ try {
594
+ await promises.unlink(path);
595
+ } catch (err) {
596
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
597
+ throw err;
598
+ }
599
+ }
405
600
  async function writeCodexConfig(input) {
406
601
  const target = codexConfigPath(input.homeDir);
407
602
  const local = input.mode === "local";
@@ -692,6 +887,15 @@ function claudeDesktopConfigPath(homeDir = os.homedir()) {
692
887
  }
693
888
  return path.resolve(homeDir, ".config", "Claude", "claude_desktop_config.json");
694
889
  }
890
+ function hermesConfigPath(homeDir) {
891
+ return path.join(hermesHomePath(homeDir), "config.yaml");
892
+ }
893
+ function hermesEnvPath(homeDir) {
894
+ return path.join(hermesHomePath(homeDir), ".env");
895
+ }
896
+ function hermesHomePath(homeDir) {
897
+ return process.env.HERMES_HOME ?? path.join(homeDir ?? os.homedir(), ".hermes");
898
+ }
695
899
  function configTargetLabel(runtime) {
696
900
  switch (runtime) {
697
901
  case "codex-cli":
@@ -1235,6 +1439,12 @@ var RUNTIME_PROFILES = {
1235
1439
  restartMode: "restart-app",
1236
1440
  canWriteRuntimeConfig: true
1237
1441
  },
1442
+ hermes: {
1443
+ id: "hermes",
1444
+ label: "Hermes Agent",
1445
+ restartMode: "restart-session",
1446
+ canWriteRuntimeConfig: true
1447
+ },
1238
1448
  other: {
1239
1449
  id: "other",
1240
1450
  label: "Other agent runtime",
@@ -1272,6 +1482,10 @@ var RUNTIME_ALIASES = {
1272
1482
  "claude_desktop": "claude-desktop",
1273
1483
  claudesktop: "claude-desktop",
1274
1484
  desktop: "claude-desktop",
1485
+ hermes: "hermes",
1486
+ "hermes-agent": "hermes",
1487
+ hermes_agent: "hermes",
1488
+ hermesagent: "hermes",
1275
1489
  other: "other",
1276
1490
  manual: "other"
1277
1491
  };
@@ -1299,6 +1513,7 @@ function detectRuntime(env) {
1299
1513
  if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
1300
1514
  if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
1301
1515
  if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
1516
+ if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
1302
1517
  return null;
1303
1518
  }
1304
1519
  async function acknowledgeLocalSignerConsent(signerPath, log) {
@@ -1708,7 +1923,7 @@ function localRuntimePrepareErrorCode(err) {
1708
1923
  }
1709
1924
 
1710
1925
  // src/runtime.ts
1711
- var CONNECTOR_VERSION = "0.1.21-alpha.0";
1926
+ var CONNECTOR_VERSION = "0.1.22-alpha.0";
1712
1927
  async function runConnect(options, deps = {}) {
1713
1928
  assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
1714
1929
  const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
@@ -1941,7 +2156,7 @@ function helpText() {
1941
2156
  "Options:",
1942
2157
  " --setup <token> Short-lived setup token from Haven.",
1943
2158
  " --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
1944
- " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, or claude-desktop.",
2159
+ " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, claude-desktop, or hermes.",
1945
2160
  " --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
1946
2161
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
1947
2162
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",