@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/README.md +22 -0
- package/dist/cli.cjs +220 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +221 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +220 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +221 -6
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
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.
|
|
239
|
+
sdkVersion: "0.1.22-alpha.0",
|
|
239
240
|
signerPackage: "@haven_ai/signer",
|
|
240
|
-
signerVersion: "0.1.
|
|
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.
|
|
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,
|
|
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.",
|