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