@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.d.cts CHANGED
@@ -126,7 +126,7 @@ declare function preflightCredentialStorage(input?: PreflightCredentialStorageIn
126
126
  declare function writeCredentialFiles(input: WriteCredentialInput): Promise<StoredCredentialPaths>;
127
127
  declare function defaultAgentDirectory(agentId: string, baseDir?: string): string;
128
128
 
129
- type RuntimeId = 'claude-code' | 'codex-desktop' | 'codex-cli' | 'cursor' | 'vscode' | 'vscode-insiders' | 'claude-desktop' | 'other';
129
+ type RuntimeId = 'claude-code' | 'codex-desktop' | 'codex-cli' | 'cursor' | 'vscode' | 'vscode-insiders' | 'claude-desktop' | 'hermes' | 'other';
130
130
  type RestartMode = 'restart-session' | 'restart-app' | 'hot-reload' | 'manual';
131
131
  interface RuntimeProfile {
132
132
  id: RuntimeId;
@@ -264,7 +264,7 @@ declare function runtimeInstallCapabilities(runtime: string | undefined, env?: N
264
264
  restartRequired: boolean;
265
265
  };
266
266
 
267
- declare const CONNECTOR_VERSION = "0.1.20-alpha.0";
267
+ declare const CONNECTOR_VERSION = "0.1.22-alpha.0";
268
268
  interface ConnectOptions {
269
269
  setupToken: string;
270
270
  apiBaseUrl: string;
@@ -307,11 +307,11 @@ declare function shortAddress(address: string): string;
307
307
 
308
308
  declare const MCP_RUNTIME_MANIFEST: {
309
309
  readonly mcpPackage: "@haven_ai/mcp";
310
- readonly mcpVersion: "0.1.20-alpha.0";
310
+ readonly mcpVersion: "0.1.22-alpha.0";
311
311
  readonly sdkPackage: "@haven_ai/sdk";
312
- readonly sdkVersion: "0.1.20-alpha.0";
312
+ readonly sdkVersion: "0.1.22-alpha.0";
313
313
  readonly signerPackage: "@haven_ai/signer";
314
- readonly signerVersion: "0.1.20-alpha.0";
314
+ readonly signerVersion: "0.1.22-alpha.0";
315
315
  readonly minimumNodeVersion: "24.0.0";
316
316
  readonly supportedClients: readonly ["codex-cli", "codex-desktop", "claude-code"];
317
317
  readonly requiredTools: readonly string[];
package/dist/index.d.ts CHANGED
@@ -126,7 +126,7 @@ declare function preflightCredentialStorage(input?: PreflightCredentialStorageIn
126
126
  declare function writeCredentialFiles(input: WriteCredentialInput): Promise<StoredCredentialPaths>;
127
127
  declare function defaultAgentDirectory(agentId: string, baseDir?: string): string;
128
128
 
129
- type RuntimeId = 'claude-code' | 'codex-desktop' | 'codex-cli' | 'cursor' | 'vscode' | 'vscode-insiders' | 'claude-desktop' | 'other';
129
+ type RuntimeId = 'claude-code' | 'codex-desktop' | 'codex-cli' | 'cursor' | 'vscode' | 'vscode-insiders' | 'claude-desktop' | 'hermes' | 'other';
130
130
  type RestartMode = 'restart-session' | 'restart-app' | 'hot-reload' | 'manual';
131
131
  interface RuntimeProfile {
132
132
  id: RuntimeId;
@@ -264,7 +264,7 @@ declare function runtimeInstallCapabilities(runtime: string | undefined, env?: N
264
264
  restartRequired: boolean;
265
265
  };
266
266
 
267
- declare const CONNECTOR_VERSION = "0.1.20-alpha.0";
267
+ declare const CONNECTOR_VERSION = "0.1.22-alpha.0";
268
268
  interface ConnectOptions {
269
269
  setupToken: string;
270
270
  apiBaseUrl: string;
@@ -307,11 +307,11 @@ declare function shortAddress(address: string): string;
307
307
 
308
308
  declare const MCP_RUNTIME_MANIFEST: {
309
309
  readonly mcpPackage: "@haven_ai/mcp";
310
- readonly mcpVersion: "0.1.20-alpha.0";
310
+ readonly mcpVersion: "0.1.22-alpha.0";
311
311
  readonly sdkPackage: "@haven_ai/sdk";
312
- readonly sdkVersion: "0.1.20-alpha.0";
312
+ readonly sdkVersion: "0.1.22-alpha.0";
313
313
  readonly signerPackage: "@haven_ai/signer";
314
- readonly signerVersion: "0.1.20-alpha.0";
314
+ readonly signerVersion: "0.1.22-alpha.0";
315
315
  readonly minimumNodeVersion: "24.0.0";
316
316
  readonly supportedClients: readonly ["codex-cli", "codex-desktop", "claude-code"];
317
317
  readonly requiredTools: readonly string[];
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import crypto from 'crypto';
2
2
  import { Wallet } from 'ethers';
3
- import { mkdir, rm, writeFile, access, chmod, readFile } from 'fs/promises';
3
+ import { mkdir, rm, writeFile, access, chmod, readFile, unlink } from 'fs/promises';
4
4
  import { homedir, platform } from 'os';
5
5
  import { join, resolve, dirname } from 'path';
6
6
  import { execFile, spawn } from 'child_process';
7
7
  import { promisify } from 'util';
8
+ import { parseDocument, isMap, stringify } from 'yaml';
8
9
  import { registeredToolNames, MCP_VERSION, ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient } from '@haven_ai/mcp';
9
10
  import { HAVEN_MINIMUM_NODE_VERSION, SKILL_FOLDER_NAME, HAVEN_SKILL_MD, isSupportedNodeVersion, unsupportedNodeVersionMessage } from '@haven_ai/sdk';
10
11
  import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
@@ -229,9 +230,9 @@ var MCP_RUNTIME_MANIFEST = {
229
230
  mcpPackage: "@haven_ai/mcp",
230
231
  mcpVersion: MCP_VERSION,
231
232
  sdkPackage: "@haven_ai/sdk",
232
- sdkVersion: "0.1.20-alpha.0",
233
+ sdkVersion: "0.1.22-alpha.0",
233
234
  signerPackage: "@haven_ai/signer",
234
- signerVersion: "0.1.20-alpha.0",
235
+ signerVersion: "0.1.22-alpha.0",
235
236
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
236
237
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
237
238
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -253,13 +254,20 @@ function signerPackageSpec() {
253
254
  }
254
255
 
255
256
  // src/config-writers.ts
257
+ var HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
258
+ var HermesConfigRecoveryError = class extends Error {
259
+ constructor() {
260
+ super("Hermes configuration recovery did not complete");
261
+ this.name = "HermesConfigRecoveryError";
262
+ }
263
+ };
256
264
  var InvalidCodexTomlError = class extends Error {
257
265
  constructor(message) {
258
266
  super(message);
259
267
  this.name = "InvalidCodexTomlError";
260
268
  }
261
269
  };
262
- async function writeRuntimeConfig(input) {
270
+ async function writeRuntimeConfig(input, deps = {}) {
263
271
  switch (input.runtime) {
264
272
  case "codex-cli":
265
273
  case "codex-desktop":
@@ -272,6 +280,8 @@ async function writeRuntimeConfig(input) {
272
280
  return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
273
281
  case "claude-desktop":
274
282
  return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
283
+ case "hermes":
284
+ return writeHermesConfig(input, deps);
275
285
  default:
276
286
  return {
277
287
  hostedConfigured: false,
@@ -325,6 +335,115 @@ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer
325
335
  return `${JSON.stringify(config, null, 2)}
326
336
  `;
327
337
  }
338
+ function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
339
+ if (!existingYaml?.trim()) return renderHermesYaml({ haven: hostedServer, "haven-signer": signerServer });
340
+ const doc = parseDocument(existingYaml, { keepSourceTokens: true });
341
+ if (doc.errors.length > 0 || !isMap(doc.contents)) {
342
+ throw new Error("Hermes config must be a YAML object");
343
+ }
344
+ const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
345
+ const existingServers = mcpPair && isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
346
+ const servers = isRecord(existingServers) ? existingServers : {};
347
+ const mergedServers = {
348
+ ...servers,
349
+ haven: hostedServer,
350
+ "haven-signer": signerServer
351
+ };
352
+ if (!mcpPair) {
353
+ return appendHermesMcpServers(existingYaml, mergedServers);
354
+ }
355
+ if (!mcpPair.value?.range) return replaceEmptyHermesMcpServers(existingYaml, mcpPair.key?.range, mergedServers);
356
+ return replaceHermesMcpServers(existingYaml, mcpPair.key?.range, mcpPair.value.range, mergedServers);
357
+ }
358
+ function mergeHermesEnv(existingEnv, apiKey) {
359
+ if (/[\r\n]/.test(apiKey)) throw new Error("Hermes API key must be a single line");
360
+ const assignment = `${HERMES_API_KEY_ENV}=${apiKey}`;
361
+ if (!existingEnv) return `${assignment}
362
+ `;
363
+ const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
364
+ const hasTrailingNewline = /\r?\n$/.test(existingEnv);
365
+ const lines = existingEnv.split(/\r?\n/);
366
+ if (hasTrailingNewline) lines.pop();
367
+ let found = false;
368
+ const merged = lines.flatMap((line) => {
369
+ if (isHermesEnvAssignment(line)) {
370
+ if (found) return [];
371
+ found = true;
372
+ return [assignment];
373
+ }
374
+ if (isAmbiguousHermesEnvLine(line)) {
375
+ throw new Error("Hermes environment contains an ambiguous managed key");
376
+ }
377
+ return [line];
378
+ });
379
+ if (!found) {
380
+ return `${existingEnv}${hasTrailingNewline ? "" : lineEnding}${assignment}${lineEnding}`;
381
+ }
382
+ return `${merged.join(lineEnding)}${hasTrailingNewline ? lineEnding : ""}`;
383
+ }
384
+ function isHermesEnvAssignment(line) {
385
+ return /^\s*(?:export[ \t]+)?MCP_HAVEN_API_KEY[ \t]*=/.test(line);
386
+ }
387
+ function isAmbiguousHermesEnvLine(line) {
388
+ return /^\s*(?:export[ \t]+)?MCP_HAVEN_API_KEY\b/.test(line);
389
+ }
390
+ function appendHermesMcpServers(source, servers) {
391
+ const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
392
+ if (documentEnd) {
393
+ const markerStart = documentEnd.index + (documentEnd[0].startsWith("\n") ? 1 : 0);
394
+ const beforeMarker = source.slice(0, markerStart);
395
+ return `${beforeMarker}${beforeMarker.endsWith("\n") ? "" : "\n"}${renderHermesMcpServers(servers, "")}
396
+ ${source.slice(markerStart)}`;
397
+ }
398
+ const separator = source.endsWith("\n") ? "" : "\n";
399
+ return `${source}${separator}${renderHermesMcpServers(servers, "")}
400
+ `;
401
+ }
402
+ function replaceHermesMcpServers(source, keyRange, valueRange, servers) {
403
+ const valueStart = valueRange[0];
404
+ const valueEnd = valueRange[1];
405
+ const valueLineStart = source.lastIndexOf("\n", valueStart - 1) + 1;
406
+ const keyStartsOnValueLine = keyRange?.[0] !== void 0 && keyRange[0] >= valueLineStart;
407
+ const nestedIndent = `${source.slice(valueLineStart, keyRange?.[0] ?? valueLineStart)} `;
408
+ if (keyStartsOnValueLine) {
409
+ const keyStart = keyRange[0];
410
+ const keyEnd = keyRange[1];
411
+ const replacement2 = `${source.slice(keyStart, keyEnd)}:
412
+ ${renderHermesMcpServerEntries(servers, nestedIndent)}`;
413
+ const needsTrailingNewline2 = valueEnd < source.length && !source.slice(valueEnd).startsWith("\n");
414
+ return `${source.slice(0, keyStart)}${replacement2}${needsTrailingNewline2 ? "\n" : ""}${source.slice(valueEnd)}`;
415
+ }
416
+ const indent = source.slice(valueLineStart, valueStart);
417
+ const rendered = stringify(servers, { lineWidth: 120 }).trimEnd();
418
+ const replacement = rendered.split("\n").map((line, index) => index === 0 ? line : `${indent}${line}`).join("\n");
419
+ const needsTrailingNewline = valueEnd < source.length && !source.slice(valueEnd).startsWith("\n");
420
+ return `${source.slice(0, valueStart)}${replacement}${needsTrailingNewline ? "\n" : ""}${source.slice(valueEnd)}`;
421
+ }
422
+ function replaceEmptyHermesMcpServers(source, keyRange, servers) {
423
+ if (!keyRange) throw new Error("Hermes config mcp_servers key is invalid");
424
+ const keyStart = keyRange[0];
425
+ const keyEnd = keyRange[1];
426
+ const lineStart = source.lastIndexOf("\n", keyStart - 1) + 1;
427
+ const lineEnd = source.indexOf("\n", keyEnd);
428
+ const nestedIndent = `${source.slice(lineStart, keyStart)} `;
429
+ const replacement = `${source.slice(keyStart, keyEnd)}:
430
+ ${renderHermesMcpServerEntries(servers, nestedIndent)}`;
431
+ return `${source.slice(0, keyStart)}${replacement}${lineEnd === -1 ? "\n" : source.slice(lineEnd)}`;
432
+ }
433
+ function renderHermesYaml(servers) {
434
+ return `${renderHermesMcpServers(servers, "")}
435
+ `;
436
+ }
437
+ function renderHermesMcpServers(servers, indent) {
438
+ return `mcp_servers:
439
+ ${renderHermesMcpServerEntries(servers, `${indent} `)}`;
440
+ }
441
+ function renderHermesMcpServerEntries(servers, indent) {
442
+ return stringify(servers, { lineWidth: 120 }).trimEnd().split("\n").map((line) => `${indent}${line}`).join("\n");
443
+ }
444
+ function isRecord(value) {
445
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
446
+ }
328
447
  function mergeCodexToml(existingToml, localMcpCommand) {
329
448
  let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
330
449
  next = next.trimEnd();
@@ -395,6 +514,82 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
395
514
  };
396
515
  }
397
516
  }
517
+ async function writeHermesConfig(input, deps) {
518
+ const target = hermesConfigPath(input.homeDir);
519
+ const envTarget = hermesEnvPath(input.homeDir);
520
+ try {
521
+ const [existing, existingEnv] = await Promise.all([readOptional(target), readOptional(envTarget)]);
522
+ const hostedServer = {
523
+ ...buildHostedServer(input.hostedMcpUrl, `\${${HERMES_API_KEY_ENV}}`, input.runtime),
524
+ enabled: true
525
+ };
526
+ const signerServer = {
527
+ ...buildSignerServer(resolveSignerLaunchSpec(input), input.runtime),
528
+ enabled: true
529
+ };
530
+ const merged = mergeHermesYaml(
531
+ existing,
532
+ hostedServer,
533
+ signerServer
534
+ );
535
+ const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey);
536
+ const writeText = deps.writeOwnerOnlyText ?? writeOwnerOnlyText;
537
+ await writeText(envTarget, mergedEnv);
538
+ try {
539
+ await writeText(target, merged);
540
+ } catch (err) {
541
+ const recovered = await restoreHermesFiles(target, existing, envTarget, existingEnv, writeText);
542
+ if (!recovered) throw new HermesConfigRecoveryError();
543
+ throw err;
544
+ }
545
+ return {
546
+ hostedConfigured: true,
547
+ signerConfigured: true,
548
+ localMcpConfigured: false,
549
+ runtimeMcpMode: "hosted_plus_signer",
550
+ target: "Hermes Agent config",
551
+ changed: existing !== merged || existingEnv !== mergedEnv,
552
+ restartRequired: true,
553
+ messages: [
554
+ `Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
555
+ "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`.",
556
+ "If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
557
+ ]
558
+ };
559
+ } catch (err) {
560
+ const recoveryIncomplete = err instanceof HermesConfigRecoveryError;
561
+ return {
562
+ hostedConfigured: false,
563
+ signerConfigured: false,
564
+ localMcpConfigured: false,
565
+ runtimeMcpMode: "hosted_plus_signer",
566
+ target: "Hermes Agent config",
567
+ changed: false,
568
+ restartRequired: true,
569
+ 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."],
570
+ errorCode: "runtime_config_write_failed"
571
+ };
572
+ }
573
+ }
574
+ async function restoreHermesFiles(configPath, existingConfig, envPath, existingEnv, writeText) {
575
+ const [envResult, configResult] = await Promise.allSettled([
576
+ restoreOptionalText(envPath, existingEnv, writeText),
577
+ restoreOptionalText(configPath, existingConfig, writeText)
578
+ ]);
579
+ return envResult.status === "fulfilled" && configResult.status === "fulfilled";
580
+ }
581
+ async function restoreOptionalText(path, existing, writeText) {
582
+ if (existing !== null) {
583
+ await writeText(path, existing);
584
+ return;
585
+ }
586
+ try {
587
+ await unlink(path);
588
+ } catch (err) {
589
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
590
+ throw err;
591
+ }
592
+ }
398
593
  async function writeCodexConfig(input) {
399
594
  const target = codexConfigPath(input.homeDir);
400
595
  const local = input.mode === "local";
@@ -685,6 +880,15 @@ function claudeDesktopConfigPath(homeDir = homedir()) {
685
880
  }
686
881
  return resolve(homeDir, ".config", "Claude", "claude_desktop_config.json");
687
882
  }
883
+ function hermesConfigPath(homeDir) {
884
+ return join(hermesHomePath(homeDir), "config.yaml");
885
+ }
886
+ function hermesEnvPath(homeDir) {
887
+ return join(hermesHomePath(homeDir), ".env");
888
+ }
889
+ function hermesHomePath(homeDir) {
890
+ return process.env.HERMES_HOME ?? join(homeDir ?? homedir(), ".hermes");
891
+ }
688
892
  function configTargetLabel(runtime) {
689
893
  switch (runtime) {
690
894
  case "codex-cli":
@@ -1228,6 +1432,12 @@ var RUNTIME_PROFILES = {
1228
1432
  restartMode: "restart-app",
1229
1433
  canWriteRuntimeConfig: true
1230
1434
  },
1435
+ hermes: {
1436
+ id: "hermes",
1437
+ label: "Hermes Agent",
1438
+ restartMode: "restart-session",
1439
+ canWriteRuntimeConfig: true
1440
+ },
1231
1441
  other: {
1232
1442
  id: "other",
1233
1443
  label: "Other agent runtime",
@@ -1265,6 +1475,10 @@ var RUNTIME_ALIASES = {
1265
1475
  "claude_desktop": "claude-desktop",
1266
1476
  claudesktop: "claude-desktop",
1267
1477
  desktop: "claude-desktop",
1478
+ hermes: "hermes",
1479
+ "hermes-agent": "hermes",
1480
+ hermes_agent: "hermes",
1481
+ hermesagent: "hermes",
1268
1482
  other: "other",
1269
1483
  manual: "other"
1270
1484
  };
@@ -1292,6 +1506,7 @@ function detectRuntime(env) {
1292
1506
  if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
1293
1507
  if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
1294
1508
  if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
1509
+ if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
1295
1510
  return null;
1296
1511
  }
1297
1512
  async function acknowledgeLocalSignerConsent(signerPath, log) {
@@ -1701,7 +1916,7 @@ function localRuntimePrepareErrorCode(err) {
1701
1916
  }
1702
1917
 
1703
1918
  // src/runtime.ts
1704
- var CONNECTOR_VERSION = "0.1.20-alpha.0";
1919
+ var CONNECTOR_VERSION = "0.1.22-alpha.0";
1705
1920
  async function runConnect(options, deps = {}) {
1706
1921
  assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
1707
1922
  const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
@@ -1934,7 +2149,7 @@ function helpText() {
1934
2149
  "Options:",
1935
2150
  " --setup <token> Short-lived setup token from Haven.",
1936
2151
  " --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
1937
- " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, or claude-desktop.",
2152
+ " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, claude-desktop, or hermes.",
1938
2153
  " --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
1939
2154
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
1940
2155
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",