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