@haven_ai/connect 0.1.28-alpha.0 → 0.1.29-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 +68 -0
- package/dist/cli.cjs +997 -188
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +999 -190
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1009 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +195 -5
- package/dist/index.d.ts +195 -5
- package/dist/index.js +1007 -191
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
|
|
2
2
|
import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
|
|
3
3
|
import { isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD, HAVEN_MINIMUM_NODE_VERSION } from '@haven_ai/sdk';
|
|
4
|
-
import { mkdir, rm,
|
|
4
|
+
import { mkdir, rm, stat, readdir, readFile, chmod, access, writeFile, unlink } from 'fs/promises';
|
|
5
5
|
import { homedir, platform } from 'os';
|
|
6
|
-
import { join, resolve, dirname } from 'path';
|
|
6
|
+
import { join, resolve, dirname, basename } from 'path';
|
|
7
7
|
import { parseDocument, isMap, stringify } from 'yaml';
|
|
8
8
|
import { execFile, spawn } from 'child_process';
|
|
9
9
|
import { promisify } from 'util';
|
|
10
10
|
import crypto from 'crypto';
|
|
11
11
|
import { Wallet } from 'ethers';
|
|
12
|
+
import { createInterface } from 'readline';
|
|
12
13
|
import { realpathSync } from 'fs';
|
|
13
14
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
14
15
|
|
|
@@ -21,6 +22,69 @@ var __export = (target, all) => {
|
|
|
21
22
|
for (var name in all)
|
|
22
23
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
24
|
};
|
|
25
|
+
|
|
26
|
+
// src/redact.ts
|
|
27
|
+
function redactSecrets(value) {
|
|
28
|
+
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
29
|
+
}
|
|
30
|
+
function shortAddress(address) {
|
|
31
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
32
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
33
|
+
}
|
|
34
|
+
var API_KEY_RE, PRIVATE_KEY_RE;
|
|
35
|
+
var init_redact = __esm({
|
|
36
|
+
"src/redact.ts"() {
|
|
37
|
+
API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
|
|
38
|
+
PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// src/server-names.ts
|
|
43
|
+
function assertValidServerSlug(slug) {
|
|
44
|
+
if (slug.length === 0 || slug.length > 32 || !SLUG_RE.test(slug)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Invalid server name ${JSON.stringify(slug)}: use 1-32 lowercase letters, digits, and single hyphens (e.g. "research").`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (slug === "haven" || slug === "haven-signer") {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Invalid server name ${JSON.stringify(slug)}: "haven" and "haven-signer" are the unnamed pair's own names \u2014 omit --name for the bare pair.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (slug === "signer" || slug.startsWith("signer-")) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Invalid server name ${JSON.stringify(slug)}: "signer" and "signer-*" are reserved \u2014 they would collide with another pair's haven-signer-* entry.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function serverNamesFor(slug) {
|
|
61
|
+
if (slug === void 0) {
|
|
62
|
+
return {
|
|
63
|
+
hosted: "haven",
|
|
64
|
+
signer: "haven-signer",
|
|
65
|
+
// Historical Codex table names — every wired Codex host has these.
|
|
66
|
+
codexHosted: "haven",
|
|
67
|
+
codexSigner: "haven_signer",
|
|
68
|
+
hermesEnvKey: "MCP_HAVEN_API_KEY"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
assertValidServerSlug(slug);
|
|
72
|
+
return {
|
|
73
|
+
hosted: `haven-${slug}`,
|
|
74
|
+
signer: `haven-signer-${slug}`,
|
|
75
|
+
// Hyphens are valid TOML bare keys, so named Codex tables match the
|
|
76
|
+
// JSON/YAML names instead of inheriting the legacy underscore.
|
|
77
|
+
codexHosted: `haven-${slug}`,
|
|
78
|
+
codexSigner: `haven-signer-${slug}`,
|
|
79
|
+
hermesEnvKey: `MCP_HAVEN_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
var SLUG_RE;
|
|
83
|
+
var init_server_names = __esm({
|
|
84
|
+
"src/server-names.ts"() {
|
|
85
|
+
SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
24
88
|
function mcpPackageSpec() {
|
|
25
89
|
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
26
90
|
}
|
|
@@ -37,9 +101,9 @@ var init_runtime_manifest = __esm({
|
|
|
37
101
|
mcpPackage: "@haven_ai/mcp",
|
|
38
102
|
mcpVersion: MCP_VERSION,
|
|
39
103
|
sdkPackage: "@haven_ai/sdk",
|
|
40
|
-
sdkVersion: "0.1.
|
|
104
|
+
sdkVersion: "0.1.29-alpha.0",
|
|
41
105
|
signerPackage: "@haven_ai/signer",
|
|
42
|
-
signerVersion: "0.1.
|
|
106
|
+
signerVersion: "0.1.29-alpha.0",
|
|
43
107
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
44
108
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
45
109
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -115,31 +179,33 @@ function buildSignerServer(spec, runtime) {
|
|
|
115
179
|
if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
|
|
116
180
|
return server;
|
|
117
181
|
}
|
|
118
|
-
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer) {
|
|
119
|
-
const config = existingJson?.trim() ? parseJsonObject(existingJson) : {};
|
|
182
|
+
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
183
|
+
const config = existingJson?.trim() ? parseJsonObject(existingJson, configPath) : {};
|
|
120
184
|
const existingRoot = config[serverRoot];
|
|
121
185
|
const servers = existingRoot && typeof existingRoot === "object" && !Array.isArray(existingRoot) ? existingRoot : {};
|
|
122
186
|
config[serverRoot] = {
|
|
123
187
|
...servers,
|
|
124
|
-
|
|
125
|
-
|
|
188
|
+
[names.hosted]: hostedServer,
|
|
189
|
+
[names.signer]: signerServer
|
|
126
190
|
};
|
|
127
191
|
return `${JSON.stringify(config, null, 2)}
|
|
128
192
|
`;
|
|
129
193
|
}
|
|
130
|
-
function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
|
|
131
|
-
if (!existingYaml?.trim())
|
|
194
|
+
function mergeHermesYaml(existingYaml, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
195
|
+
if (!existingYaml?.trim()) {
|
|
196
|
+
return renderHermesYaml({ [names.hosted]: hostedServer, [names.signer]: signerServer });
|
|
197
|
+
}
|
|
132
198
|
const doc = parseDocument(existingYaml, { keepSourceTokens: true });
|
|
133
199
|
if (doc.errors.length > 0 || !isMap(doc.contents)) {
|
|
134
|
-
throw new
|
|
200
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
|
|
135
201
|
}
|
|
136
202
|
const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
137
203
|
const existingServers = mcpPair && isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
|
|
138
204
|
const servers = isRecord(existingServers) ? existingServers : {};
|
|
139
205
|
const mergedServers = {
|
|
140
206
|
...servers,
|
|
141
|
-
|
|
142
|
-
|
|
207
|
+
[names.hosted]: hostedServer,
|
|
208
|
+
[names.signer]: signerServer
|
|
143
209
|
};
|
|
144
210
|
if (!mcpPair) {
|
|
145
211
|
return appendHermesMcpServers(existingYaml, mergedServers);
|
|
@@ -147,9 +213,9 @@ function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
|
|
|
147
213
|
if (!mcpPair.value?.range) return replaceEmptyHermesMcpServers(existingYaml, mcpPair.key?.range, mergedServers);
|
|
148
214
|
return replaceHermesMcpServers(existingYaml, mcpPair.key?.range, mcpPair.value.range, mergedServers);
|
|
149
215
|
}
|
|
150
|
-
function mergeHermesEnv(existingEnv, apiKey) {
|
|
216
|
+
function mergeHermesEnv(existingEnv, apiKey, envKey = HERMES_API_KEY_ENV) {
|
|
151
217
|
if (/[\r\n]/.test(apiKey)) throw new Error("Hermes API key must be a single line");
|
|
152
|
-
const assignment = `${
|
|
218
|
+
const assignment = `${envKey}=${apiKey}`;
|
|
153
219
|
if (!existingEnv) return `${assignment}
|
|
154
220
|
`;
|
|
155
221
|
const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -158,12 +224,12 @@ function mergeHermesEnv(existingEnv, apiKey) {
|
|
|
158
224
|
if (hasTrailingNewline) lines.pop();
|
|
159
225
|
let found = false;
|
|
160
226
|
const merged = lines.flatMap((line) => {
|
|
161
|
-
if (isHermesEnvAssignment(line)) {
|
|
227
|
+
if (isHermesEnvAssignment(line, envKey)) {
|
|
162
228
|
if (found) return [];
|
|
163
229
|
found = true;
|
|
164
230
|
return [assignment];
|
|
165
231
|
}
|
|
166
|
-
if (isAmbiguousHermesEnvLine(line)) {
|
|
232
|
+
if (isAmbiguousHermesEnvLine(line, envKey)) {
|
|
167
233
|
throw new Error("Hermes environment contains an ambiguous managed key");
|
|
168
234
|
}
|
|
169
235
|
return [line];
|
|
@@ -173,11 +239,11 @@ function mergeHermesEnv(existingEnv, apiKey) {
|
|
|
173
239
|
}
|
|
174
240
|
return `${merged.join(lineEnding)}${hasTrailingNewline ? lineEnding : ""}`;
|
|
175
241
|
}
|
|
176
|
-
function isHermesEnvAssignment(line) {
|
|
177
|
-
return
|
|
242
|
+
function isHermesEnvAssignment(line, envKey) {
|
|
243
|
+
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}[ \\t]*=`).test(line);
|
|
178
244
|
}
|
|
179
|
-
function isAmbiguousHermesEnvLine(line) {
|
|
180
|
-
return
|
|
245
|
+
function isAmbiguousHermesEnvLine(line, envKey) {
|
|
246
|
+
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}\\b`).test(line);
|
|
181
247
|
}
|
|
182
248
|
function appendHermesMcpServers(source, servers) {
|
|
183
249
|
const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
|
|
@@ -236,11 +302,14 @@ function renderHermesMcpServerEntries(servers, indent) {
|
|
|
236
302
|
function isRecord(value) {
|
|
237
303
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
238
304
|
}
|
|
239
|
-
function mergeCodexToml(existingToml, localMcpCommand) {
|
|
240
|
-
let next = removeTomlTableTree(
|
|
305
|
+
function mergeCodexToml(existingToml, localMcpCommand, names = serverNamesFor()) {
|
|
306
|
+
let next = removeTomlTableTree(
|
|
307
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
308
|
+
`mcp_servers.${names.codexSigner}`
|
|
309
|
+
);
|
|
241
310
|
next = next.trimEnd();
|
|
242
311
|
const block = [
|
|
243
|
-
|
|
312
|
+
`[mcp_servers.${names.codexHosted}]`,
|
|
244
313
|
`command = ${tomlString(localMcpCommand)}`,
|
|
245
314
|
"args = []",
|
|
246
315
|
"startup_timeout_sec = 120"
|
|
@@ -252,15 +321,18 @@ function mergeCodexToml(existingToml, localMcpCommand) {
|
|
|
252
321
|
`;
|
|
253
322
|
return merged;
|
|
254
323
|
}
|
|
255
|
-
function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec) {
|
|
256
|
-
let next = removeTomlTableTree(
|
|
324
|
+
function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec, names = serverNamesFor()) {
|
|
325
|
+
let next = removeTomlTableTree(
|
|
326
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
327
|
+
`mcp_servers.${names.codexSigner}`
|
|
328
|
+
);
|
|
257
329
|
next = next.trimEnd();
|
|
258
330
|
const block = [
|
|
259
|
-
|
|
331
|
+
`[mcp_servers.${names.codexHosted}]`,
|
|
260
332
|
`url = ${tomlString(hostedMcpUrl)}`,
|
|
261
333
|
`http_headers = { "Authorization" = ${tomlString(`Bearer ${apiKey}`)} }`,
|
|
262
334
|
"",
|
|
263
|
-
|
|
335
|
+
`[mcp_servers.${names.codexSigner}]`,
|
|
264
336
|
`command = ${tomlString(signerSpec.command)}`,
|
|
265
337
|
`args = [${signerSpec.args.map((arg) => tomlString(arg)).join(", ")}]`,
|
|
266
338
|
"startup_timeout_sec = 120"
|
|
@@ -279,7 +351,9 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
279
351
|
existing,
|
|
280
352
|
serverRoot,
|
|
281
353
|
buildHostedServer(input.hostedMcpUrl, input.apiKey, input.runtime),
|
|
282
|
-
buildSignerServer(resolveSignerLaunchSpec(input), input.runtime)
|
|
354
|
+
buildSignerServer(resolveSignerLaunchSpec(input), input.runtime),
|
|
355
|
+
serverNamesFor(input.serverName),
|
|
356
|
+
target
|
|
283
357
|
);
|
|
284
358
|
await writeOwnerOnlyText(target, merged);
|
|
285
359
|
return {
|
|
@@ -293,6 +367,7 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
293
367
|
messages: [`Updated Haven MCP entries in ${configTargetLabel(input.runtime)}.`]
|
|
294
368
|
};
|
|
295
369
|
} catch (err) {
|
|
370
|
+
const unreadable = err instanceof UnreadableRuntimeConfigError;
|
|
296
371
|
return {
|
|
297
372
|
hostedConfigured: false,
|
|
298
373
|
signerConfigured: false,
|
|
@@ -301,8 +376,11 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
301
376
|
target: configTargetLabel(input.runtime),
|
|
302
377
|
changed: false,
|
|
303
378
|
restartRequired: true,
|
|
304
|
-
messages:
|
|
305
|
-
|
|
379
|
+
messages: unreadable ? [
|
|
380
|
+
`Could not update ${configTargetLabel(input.runtime)}: ${err.message}.`,
|
|
381
|
+
`Nothing was written to ${err.configPath}. Fix the JSON there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} ${input.runtime}\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the setup command: its token is already used.`
|
|
382
|
+
] : [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
|
|
383
|
+
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
306
384
|
};
|
|
307
385
|
}
|
|
308
386
|
}
|
|
@@ -311,8 +389,9 @@ async function writeHermesConfig(input, deps) {
|
|
|
311
389
|
const envTarget = hermesEnvPath(input.homeDir);
|
|
312
390
|
try {
|
|
313
391
|
const [existing, existingEnv] = await Promise.all([readOptional(target), readOptional(envTarget)]);
|
|
392
|
+
const names = serverNamesFor(input.serverName);
|
|
314
393
|
const hostedServer = {
|
|
315
|
-
...buildHostedServer(input.hostedMcpUrl, `\${${
|
|
394
|
+
...buildHostedServer(input.hostedMcpUrl, `\${${names.hermesEnvKey}}`, input.runtime),
|
|
316
395
|
enabled: true
|
|
317
396
|
};
|
|
318
397
|
const signerServer = {
|
|
@@ -322,9 +401,11 @@ async function writeHermesConfig(input, deps) {
|
|
|
322
401
|
const merged = mergeHermesYaml(
|
|
323
402
|
existing,
|
|
324
403
|
hostedServer,
|
|
325
|
-
signerServer
|
|
404
|
+
signerServer,
|
|
405
|
+
names,
|
|
406
|
+
target
|
|
326
407
|
);
|
|
327
|
-
const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey);
|
|
408
|
+
const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey, names.hermesEnvKey);
|
|
328
409
|
const writeText = deps.writeOwnerOnlyText ?? writeOwnerOnlyText;
|
|
329
410
|
await writeText(envTarget, mergedEnv);
|
|
330
411
|
try {
|
|
@@ -345,11 +426,13 @@ async function writeHermesConfig(input, deps) {
|
|
|
345
426
|
messages: [
|
|
346
427
|
`Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
|
|
347
428
|
"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`.",
|
|
429
|
+
"If several long-lived Hermes processes are running (a gateway plus TUI workers), restart EVERY one: each loads its MCP wiring at startup, so a process started before this setup keeps using its old snapshot.",
|
|
348
430
|
"If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
|
|
349
431
|
]
|
|
350
432
|
};
|
|
351
433
|
} catch (err) {
|
|
352
434
|
const recoveryIncomplete = err instanceof HermesConfigRecoveryError;
|
|
435
|
+
const unreadable = err instanceof UnreadableRuntimeConfigError;
|
|
353
436
|
return {
|
|
354
437
|
hostedConfigured: false,
|
|
355
438
|
signerConfigured: false,
|
|
@@ -358,8 +441,11 @@ async function writeHermesConfig(input, deps) {
|
|
|
358
441
|
target: "Hermes Agent config",
|
|
359
442
|
changed: false,
|
|
360
443
|
restartRequired: true,
|
|
361
|
-
messages:
|
|
362
|
-
|
|
444
|
+
messages: unreadable ? [
|
|
445
|
+
`Could not update Hermes Agent config: ${err.message}.`,
|
|
446
|
+
`Nothing was written to ${err.configPath}. Fix the YAML there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} hermes\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the setup command: its token is already used.`
|
|
447
|
+
] : [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."],
|
|
448
|
+
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
363
449
|
};
|
|
364
450
|
}
|
|
365
451
|
}
|
|
@@ -391,7 +477,7 @@ async function writeCodexConfig(input) {
|
|
|
391
477
|
if (!input.localMcpCommand) {
|
|
392
478
|
throw new Error("local MCP wrapper command is required");
|
|
393
479
|
}
|
|
394
|
-
const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand);
|
|
480
|
+
const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand, serverNamesFor(input.serverName));
|
|
395
481
|
await writeOwnerOnlyText(target, merged2);
|
|
396
482
|
return {
|
|
397
483
|
hostedConfigured: false,
|
|
@@ -406,7 +492,13 @@ async function writeCodexConfig(input) {
|
|
|
406
492
|
]
|
|
407
493
|
};
|
|
408
494
|
}
|
|
409
|
-
const merged = mergeCodexTomlHosted(
|
|
495
|
+
const merged = mergeCodexTomlHosted(
|
|
496
|
+
existing ?? "",
|
|
497
|
+
input.hostedMcpUrl,
|
|
498
|
+
input.apiKey,
|
|
499
|
+
resolveSignerLaunchSpec(input),
|
|
500
|
+
serverNamesFor(input.serverName)
|
|
501
|
+
);
|
|
410
502
|
await writeOwnerOnlyText(target, merged);
|
|
411
503
|
return {
|
|
412
504
|
hostedConfigured: true,
|
|
@@ -448,10 +540,15 @@ async function writeOwnerOnlyText(path, value) {
|
|
|
448
540
|
await writeFile(path, value, { mode: 384 });
|
|
449
541
|
await chmod(path, 384).catch(() => void 0);
|
|
450
542
|
}
|
|
451
|
-
function parseJsonObject(value) {
|
|
452
|
-
|
|
543
|
+
function parseJsonObject(value, configPath) {
|
|
544
|
+
let parsed;
|
|
545
|
+
try {
|
|
546
|
+
parsed = JSON.parse(value);
|
|
547
|
+
} catch {
|
|
548
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "it is not valid JSON");
|
|
549
|
+
}
|
|
453
550
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
454
|
-
throw new
|
|
551
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "the top level is not a JSON object");
|
|
455
552
|
}
|
|
456
553
|
return parsed;
|
|
457
554
|
}
|
|
@@ -718,10 +815,11 @@ function configTargetLabel(runtime) {
|
|
|
718
815
|
return "runtime MCP config";
|
|
719
816
|
}
|
|
720
817
|
}
|
|
721
|
-
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError;
|
|
818
|
+
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError, REPAIR_COMMAND_PREFIX, UnreadableRuntimeConfigError;
|
|
722
819
|
var init_config_writers = __esm({
|
|
723
820
|
"src/config-writers.ts"() {
|
|
724
821
|
init_runtime_manifest();
|
|
822
|
+
init_server_names();
|
|
725
823
|
HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
|
|
726
824
|
HermesConfigRecoveryError = class extends Error {
|
|
727
825
|
constructor() {
|
|
@@ -735,8 +833,37 @@ var init_config_writers = __esm({
|
|
|
735
833
|
this.name = "InvalidCodexTomlError";
|
|
736
834
|
}
|
|
737
835
|
};
|
|
836
|
+
REPAIR_COMMAND_PREFIX = "npx @haven_ai/connect@alpha --doctor --repair --runtime";
|
|
837
|
+
UnreadableRuntimeConfigError = class extends Error {
|
|
838
|
+
configPath;
|
|
839
|
+
constructor(configPath, detail) {
|
|
840
|
+
super(`${configPath} is not a config Haven can merge into (${detail})`);
|
|
841
|
+
this.name = "UnreadableRuntimeConfigError";
|
|
842
|
+
this.configPath = configPath;
|
|
843
|
+
}
|
|
844
|
+
};
|
|
738
845
|
}
|
|
739
846
|
});
|
|
847
|
+
async function probeHostedAgentIdentity(apiKey, apiUrl, fetchImpl = fetch) {
|
|
848
|
+
let response;
|
|
849
|
+
try {
|
|
850
|
+
response = await fetchWithTimeout(fetchImpl, `${apiUrl.replace(/\/+$/, "")}/machine-payments/agent`, {
|
|
851
|
+
method: "GET",
|
|
852
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
|
|
853
|
+
});
|
|
854
|
+
} catch {
|
|
855
|
+
return { status: "network_error" };
|
|
856
|
+
}
|
|
857
|
+
if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
|
|
858
|
+
if (!response.ok) return { status: "bad_response" };
|
|
859
|
+
try {
|
|
860
|
+
const payload = JSON.parse(await response.text());
|
|
861
|
+
if (typeof payload?.delegate_address !== "string") return { status: "bad_response" };
|
|
862
|
+
return { status: "ok", agentId: payload.id, delegateAddress: payload.delegate_address };
|
|
863
|
+
} catch {
|
|
864
|
+
return { status: "bad_response" };
|
|
865
|
+
}
|
|
866
|
+
}
|
|
740
867
|
async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
|
|
741
868
|
let response;
|
|
742
869
|
try {
|
|
@@ -774,7 +901,7 @@ async function probeLocalSignerCredential(signerPath) {
|
|
|
774
901
|
}
|
|
775
902
|
}
|
|
776
903
|
async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
|
|
777
|
-
return new Promise((
|
|
904
|
+
return new Promise((resolve9) => {
|
|
778
905
|
const child = spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
779
906
|
let stdout = "";
|
|
780
907
|
let settled = false;
|
|
@@ -786,7 +913,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
786
913
|
settled = true;
|
|
787
914
|
clearTimeout(timeout);
|
|
788
915
|
child.kill();
|
|
789
|
-
|
|
916
|
+
resolve9(result);
|
|
790
917
|
};
|
|
791
918
|
const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
|
|
792
919
|
child.on("error", () => finish({ status: "process_error" }));
|
|
@@ -900,7 +1027,8 @@ async function prepareSignerRuntime(input, deps = {}) {
|
|
|
900
1027
|
wrapperPath,
|
|
901
1028
|
runtimeDirectory,
|
|
902
1029
|
npmCacheDirectory,
|
|
903
|
-
cliPath
|
|
1030
|
+
cliPath,
|
|
1031
|
+
serverName: input.serverName
|
|
904
1032
|
});
|
|
905
1033
|
messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
|
|
906
1034
|
return {
|
|
@@ -1001,6 +1129,7 @@ async function readRuntimeSidecar(credentialDirectory) {
|
|
|
1001
1129
|
}
|
|
1002
1130
|
async function writeRuntimeSidecar(input) {
|
|
1003
1131
|
const value = {
|
|
1132
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1004
1133
|
signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
|
|
1005
1134
|
signer_version: MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1006
1135
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
@@ -1031,6 +1160,26 @@ var init_signer_runtime = __esm({
|
|
|
1031
1160
|
}
|
|
1032
1161
|
});
|
|
1033
1162
|
|
|
1163
|
+
// src/connect-error.ts
|
|
1164
|
+
function isConnectError(err) {
|
|
1165
|
+
return err instanceof ConnectError;
|
|
1166
|
+
}
|
|
1167
|
+
var ConnectError;
|
|
1168
|
+
var init_connect_error = __esm({
|
|
1169
|
+
"src/connect-error.ts"() {
|
|
1170
|
+
ConnectError = class extends Error {
|
|
1171
|
+
code;
|
|
1172
|
+
nextAction;
|
|
1173
|
+
constructor(code, message, nextAction2) {
|
|
1174
|
+
super(message);
|
|
1175
|
+
this.name = "ConnectError";
|
|
1176
|
+
this.code = code;
|
|
1177
|
+
this.nextAction = nextAction2;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1034
1183
|
// src/runtime-registry.ts
|
|
1035
1184
|
function runtimeProfile(runtime, env = process.env) {
|
|
1036
1185
|
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
@@ -1040,6 +1189,42 @@ function normalizeRuntime(runtime, env = process.env) {
|
|
|
1040
1189
|
if (explicit) return explicit;
|
|
1041
1190
|
return detectRuntime(env) ?? "other";
|
|
1042
1191
|
}
|
|
1192
|
+
async function resolveRuntimeSelection(explicit, force, options = {}) {
|
|
1193
|
+
const env = options.env ?? process.env;
|
|
1194
|
+
if (force !== void 0) {
|
|
1195
|
+
const forced = normalizeRuntimeName(force);
|
|
1196
|
+
if (!forced) {
|
|
1197
|
+
throw new ConnectError(
|
|
1198
|
+
"runtime_force_unrecognized",
|
|
1199
|
+
`Unknown --runtime-force value "${force}". Valid values: ${RUNTIME_FLAG_VALUES}.`,
|
|
1200
|
+
"rerun_connect_with_a_valid_runtime_name"
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
return { runtime: forced, source: "force" };
|
|
1204
|
+
}
|
|
1205
|
+
const detected = detectRuntime(env);
|
|
1206
|
+
const supplied = explicit?.trim() || options.selfReported?.trim() || void 0;
|
|
1207
|
+
const hint = normalizeRuntimeName(supplied);
|
|
1208
|
+
if (supplied && !hint) {
|
|
1209
|
+
if (!detected) {
|
|
1210
|
+
throw new ConnectError(
|
|
1211
|
+
"runtime_unrecognized",
|
|
1212
|
+
`"${supplied}" is not an agent runtime Haven knows. Valid values: ${RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Re-run with one of those, or --runtime other to store credentials and finish the MCP setup by hand. Nothing was written and the Haven setup token is still unused.`,
|
|
1213
|
+
"rerun_connect_with_a_valid_runtime_name"
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
return { runtime: detected, source: "detected", discardedHint: supplied };
|
|
1217
|
+
}
|
|
1218
|
+
if (detected && hint && detected !== hint) {
|
|
1219
|
+
return { runtime: detected, source: "detected", overrodeHint: hint };
|
|
1220
|
+
}
|
|
1221
|
+
if (hint) return { runtime: hint, source: "explicit" };
|
|
1222
|
+
if (detected) return { runtime: detected, source: "detected" };
|
|
1223
|
+
if (options.promptForRuntime) {
|
|
1224
|
+
return { runtime: await options.promptForRuntime(), source: "prompted" };
|
|
1225
|
+
}
|
|
1226
|
+
return { runtime: null, source: "none" };
|
|
1227
|
+
}
|
|
1043
1228
|
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
1044
1229
|
const mode = runtimeProfile(runtime, env).restartMode;
|
|
1045
1230
|
return mode === "restart-session" || mode === "restart-app";
|
|
@@ -1060,9 +1245,10 @@ function detectRuntime(env) {
|
|
|
1060
1245
|
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
1061
1246
|
return null;
|
|
1062
1247
|
}
|
|
1063
|
-
var RUNTIME_PROFILES, RUNTIME_ALIASES;
|
|
1248
|
+
var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUES;
|
|
1064
1249
|
var init_runtime_registry = __esm({
|
|
1065
1250
|
"src/runtime-registry.ts"() {
|
|
1251
|
+
init_connect_error();
|
|
1066
1252
|
RUNTIME_PROFILES = {
|
|
1067
1253
|
"claude-code": {
|
|
1068
1254
|
id: "claude-code",
|
|
@@ -1133,6 +1319,11 @@ var init_runtime_registry = __esm({
|
|
|
1133
1319
|
"claude-code": "claude-code",
|
|
1134
1320
|
claudecode: "claude-code",
|
|
1135
1321
|
"claude_code": "claude-code",
|
|
1322
|
+
// #1682: Cowork runs Claude Code's config, so it resolves to the same
|
|
1323
|
+
// profile. The dashboard's command carries no --runtime for it (it is a
|
|
1324
|
+
// detected, command-path runtime), but an explicit `--runtime cowork` typed
|
|
1325
|
+
// by hand must not fall through to the no-runtime refusal.
|
|
1326
|
+
cowork: "claude-code",
|
|
1136
1327
|
codex: "codex-cli",
|
|
1137
1328
|
"codex-cli": "codex-cli",
|
|
1138
1329
|
codexcli: "codex-cli",
|
|
@@ -1162,9 +1353,18 @@ var init_runtime_registry = __esm({
|
|
|
1162
1353
|
"hermes-agent": "hermes",
|
|
1163
1354
|
hermes_agent: "hermes",
|
|
1164
1355
|
hermesagent: "hermes",
|
|
1356
|
+
// #1682: OpenClaw is a SNIPPET target — the user pastes an mcpServers entry
|
|
1357
|
+
// into ~/.openclaw/openclaw.json and restarts the gateway. That is exactly
|
|
1358
|
+
// the 'other' profile's behaviour (credentials written to disk, no config
|
|
1359
|
+
// auto-written, manual finish), so it resolves there rather than earning a
|
|
1360
|
+
// profile whose only distinguishing feature would be its label.
|
|
1361
|
+
openclaw: "other",
|
|
1362
|
+
"open-claw": "other",
|
|
1363
|
+
"open_claw": "other",
|
|
1165
1364
|
other: "other",
|
|
1166
1365
|
manual: "other"
|
|
1167
1366
|
};
|
|
1367
|
+
RUNTIME_FLAG_VALUES = "claude-code, codex-cli, codex-desktop, cursor, vscode, vscode-insiders, claude-desktop, hermes, other";
|
|
1168
1368
|
}
|
|
1169
1369
|
});
|
|
1170
1370
|
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
@@ -1241,6 +1441,82 @@ var init_signer_consent = __esm({
|
|
|
1241
1441
|
}
|
|
1242
1442
|
});
|
|
1243
1443
|
|
|
1444
|
+
// src/tombstone.ts
|
|
1445
|
+
var tombstone_exports = {};
|
|
1446
|
+
__export(tombstone_exports, {
|
|
1447
|
+
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
1448
|
+
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
1449
|
+
readAgentTombstone: () => readAgentTombstone,
|
|
1450
|
+
writeAgentTombstone: () => writeAgentTombstone
|
|
1451
|
+
});
|
|
1452
|
+
function tombstoneScript(info) {
|
|
1453
|
+
const lines = [
|
|
1454
|
+
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
1455
|
+
"",
|
|
1456
|
+
` agent: ${info.agent_id}`,
|
|
1457
|
+
` retired at: ${info.retired_at}`,
|
|
1458
|
+
` reason: ${info.reason}`,
|
|
1459
|
+
...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
|
|
1460
|
+
"",
|
|
1461
|
+
"This process is running with a wiring snapshot that predates the",
|
|
1462
|
+
"retirement \u2014 it loaded its MCP config at startup and has kept it since.",
|
|
1463
|
+
"Restart THIS host to pick up the current wiring. If several long-lived",
|
|
1464
|
+
"hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
|
|
1465
|
+
"one of them: each holds the snapshot from its own start time, so after",
|
|
1466
|
+
"a chain of recreations each can be parked on a DIFFERENT old agent.",
|
|
1467
|
+
"",
|
|
1468
|
+
"Then verify with: npx @haven_ai/connect@alpha --doctor --runtime <runtime>"
|
|
1469
|
+
];
|
|
1470
|
+
return [
|
|
1471
|
+
"#!/usr/bin/env node",
|
|
1472
|
+
`// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
|
|
1473
|
+
"// once every long-lived MCP host on this machine has been restarted.",
|
|
1474
|
+
`process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
|
|
1475
|
+
"process.exit(1)",
|
|
1476
|
+
""
|
|
1477
|
+
].join("\n");
|
|
1478
|
+
}
|
|
1479
|
+
async function writeAgentTombstone(input) {
|
|
1480
|
+
const dirStat = await stat(input.directory).catch(() => null);
|
|
1481
|
+
if (!dirStat?.isDirectory()) {
|
|
1482
|
+
throw new Error(`Not a directory: ${input.directory} \u2014 nothing to tombstone.`);
|
|
1483
|
+
}
|
|
1484
|
+
const info = {
|
|
1485
|
+
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
1486
|
+
// MCP stderr log on EVERY stale probe, potentially for months — redact
|
|
1487
|
+
// like every other output path, at the write layer so any future caller
|
|
1488
|
+
// inherits it. (#1681 review, finding 1)
|
|
1489
|
+
agent_id: input.agentId,
|
|
1490
|
+
retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1491
|
+
reason: redactSecrets(input.reason),
|
|
1492
|
+
...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
|
|
1493
|
+
};
|
|
1494
|
+
const binDir = join(input.directory, "bin");
|
|
1495
|
+
await mkdir(binDir, { recursive: true });
|
|
1496
|
+
const wrapperPath = join(binDir, "haven-signer.mjs");
|
|
1497
|
+
await writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
1498
|
+
await chmod(wrapperPath, 493);
|
|
1499
|
+
await writeFile(join(input.directory, TOMBSTONE_FILENAME), JSON.stringify(info, null, 2) + "\n", "utf8");
|
|
1500
|
+
return info;
|
|
1501
|
+
}
|
|
1502
|
+
async function readAgentTombstone(directory) {
|
|
1503
|
+
try {
|
|
1504
|
+
const parsed = JSON.parse(await readFile(join(directory, TOMBSTONE_FILENAME), "utf8"));
|
|
1505
|
+
if (typeof parsed?.agent_id !== "string") return null;
|
|
1506
|
+
return parsed;
|
|
1507
|
+
} catch {
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
1512
|
+
var init_tombstone = __esm({
|
|
1513
|
+
"src/tombstone.ts"() {
|
|
1514
|
+
init_redact();
|
|
1515
|
+
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
1516
|
+
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1244
1520
|
// src/doctor.ts
|
|
1245
1521
|
var doctor_exports = {};
|
|
1246
1522
|
__export(doctor_exports, {
|
|
@@ -1248,129 +1524,101 @@ __export(doctor_exports, {
|
|
|
1248
1524
|
runRepair: () => runRepair
|
|
1249
1525
|
});
|
|
1250
1526
|
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
1251
|
-
|
|
1252
|
-
const root = join(homeDir, ".haven", "agents");
|
|
1527
|
+
const root = explicit ? dirname(explicit) : join(homeDir, ".haven", "agents");
|
|
1253
1528
|
let entries = [];
|
|
1254
1529
|
try {
|
|
1255
1530
|
entries = await readdir(root);
|
|
1256
1531
|
} catch {
|
|
1257
|
-
return {};
|
|
1532
|
+
return explicit ? { directory: explicit, others: [] } : { others: [] };
|
|
1258
1533
|
}
|
|
1259
1534
|
const candidates = [];
|
|
1535
|
+
const tombstonedOnly = [];
|
|
1260
1536
|
for (const entry of entries) {
|
|
1261
1537
|
const directory = join(root, entry);
|
|
1262
1538
|
try {
|
|
1263
1539
|
const s = await stat(join(directory, "identity.json"));
|
|
1264
1540
|
candidates.push({ directory, mtimeMs: s.mtimeMs });
|
|
1265
1541
|
} catch {
|
|
1542
|
+
try {
|
|
1543
|
+
await stat(join(directory, TOMBSTONE_FILENAME));
|
|
1544
|
+
tombstonedOnly.push(directory);
|
|
1545
|
+
} catch {
|
|
1546
|
+
}
|
|
1266
1547
|
}
|
|
1267
1548
|
}
|
|
1268
|
-
if (candidates.length === 0) return {};
|
|
1269
1549
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1550
|
+
if (explicit) {
|
|
1551
|
+
return {
|
|
1552
|
+
directory: explicit,
|
|
1553
|
+
others: [...candidates.map((c) => c.directory), ...tombstonedOnly].filter((d) => d !== explicit)
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
if (candidates.length === 0 && tombstonedOnly.length === 0) return { others: [] };
|
|
1270
1557
|
return {
|
|
1271
|
-
directory: candidates[0]
|
|
1272
|
-
|
|
1558
|
+
directory: candidates[0]?.directory,
|
|
1559
|
+
others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly]
|
|
1273
1560
|
};
|
|
1274
1561
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1562
|
+
function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
|
|
1563
|
+
if (configText === null) return isPrimary;
|
|
1564
|
+
if (slug) {
|
|
1565
|
+
for (const name of [names.hosted, names.codexHosted, names.signer, names.codexSigner]) {
|
|
1566
|
+
if (new RegExp(`(^|[."'\\s\\[])${name}(["'\\]:\\s]|$)`, "m").test(configText)) return true;
|
|
1567
|
+
}
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
|
|
1571
|
+
if (bareOwnerExists) return false;
|
|
1572
|
+
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
1573
|
+
}
|
|
1574
|
+
async function readIdentity(directory) {
|
|
1575
|
+
try {
|
|
1576
|
+
return JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
1577
|
+
} catch {
|
|
1578
|
+
return void 0;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async function checksForAgent(entry, input, deps) {
|
|
1582
|
+
const { directory, identity, sidecar } = entry;
|
|
1277
1583
|
const checks = [];
|
|
1278
1584
|
let signerCapabilities;
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1585
|
+
let signerFile;
|
|
1586
|
+
try {
|
|
1587
|
+
const parsed = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
|
|
1588
|
+
signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
1589
|
+
} catch {
|
|
1590
|
+
signerFile = void 0;
|
|
1591
|
+
}
|
|
1592
|
+
const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
|
|
1593
|
+
checks.push({
|
|
1594
|
+
id: "credentials",
|
|
1595
|
+
label: "Agent credentials",
|
|
1596
|
+
ok: credentialsOk,
|
|
1597
|
+
detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})` : "identity.json or signer.json is missing or unparseable.",
|
|
1598
|
+
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
1599
|
+
});
|
|
1600
|
+
if (!sidecar) {
|
|
1283
1601
|
checks.push({
|
|
1284
|
-
id: "
|
|
1285
|
-
label: "
|
|
1602
|
+
id: "signer_runtime",
|
|
1603
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
1286
1604
|
ok: false,
|
|
1287
|
-
detail: "No
|
|
1288
|
-
repair: `Run
|
|
1605
|
+
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
1606
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1289
1607
|
});
|
|
1290
1608
|
} else {
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
identity = void 0;
|
|
1295
|
-
}
|
|
1296
|
-
try {
|
|
1297
|
-
const signer = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
|
|
1298
|
-
signerParses = typeof signer === "object" && signer !== null;
|
|
1299
|
-
} catch {
|
|
1300
|
-
signerParses = false;
|
|
1301
|
-
}
|
|
1302
|
-
const ok = Boolean(identity?.api_key) && signerParses;
|
|
1609
|
+
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
1610
|
+
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
1611
|
+
const ok = matches && versionOk;
|
|
1303
1612
|
checks.push({
|
|
1304
|
-
id: "
|
|
1305
|
-
label: "
|
|
1613
|
+
id: "signer_runtime",
|
|
1614
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
1306
1615
|
ok,
|
|
1307
|
-
detail: ok ? `
|
|
1308
|
-
...ok ? {} : { repair: `
|
|
1616
|
+
detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : matches ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
1617
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1309
1618
|
});
|
|
1310
1619
|
}
|
|
1311
|
-
|
|
1312
|
-
if (
|
|
1313
|
-
sidecar = await readRuntimeSidecar(directory);
|
|
1314
|
-
if (!sidecar) {
|
|
1315
|
-
checks.push({
|
|
1316
|
-
id: "signer_runtime",
|
|
1317
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
1318
|
-
ok: false,
|
|
1319
|
-
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
1320
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1321
|
-
});
|
|
1322
|
-
} else {
|
|
1323
|
-
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
1324
|
-
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
1325
|
-
const ok = matches && versionOk;
|
|
1326
|
-
checks.push({
|
|
1327
|
-
id: "signer_runtime",
|
|
1328
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
1329
|
-
ok,
|
|
1330
|
-
detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : matches ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
1331
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1336
|
-
if (configPath === null) {
|
|
1337
|
-
checks.push({
|
|
1338
|
-
id: "runtime_config",
|
|
1339
|
-
label: "Runtime MCP config",
|
|
1340
|
-
ok: true,
|
|
1341
|
-
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1342
|
-
});
|
|
1343
|
-
} else {
|
|
1344
|
-
let configText = null;
|
|
1345
|
-
try {
|
|
1346
|
-
configText = await readFile(configPath, "utf8");
|
|
1347
|
-
} catch {
|
|
1348
|
-
configText = null;
|
|
1349
|
-
}
|
|
1350
|
-
if (configText === null) {
|
|
1351
|
-
checks.push({
|
|
1352
|
-
id: "runtime_config",
|
|
1353
|
-
label: "Runtime MCP config",
|
|
1354
|
-
ok: false,
|
|
1355
|
-
detail: `No runtime config at ${configPath}.`,
|
|
1356
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1357
|
-
});
|
|
1358
|
-
} else {
|
|
1359
|
-
const hasHaven = identity?.hosted_mcp_url ? configText.includes(identity.hosted_mcp_url) : configText.includes("haven");
|
|
1360
|
-
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1361
|
-
const wrapperReferenced = sidecar ? configText.includes(sidecar.wrapper_path) : false;
|
|
1362
|
-
const ok = hasHaven && !signerViaNpx && (sidecar ? wrapperReferenced : true);
|
|
1363
|
-
checks.push({
|
|
1364
|
-
id: "runtime_config",
|
|
1365
|
-
label: "Runtime MCP config",
|
|
1366
|
-
ok,
|
|
1367
|
-
detail: ok ? `Config at ${configPath} references the hosted server and the prepared signer wrapper.` : signerViaNpx ? `Config at ${configPath} still launches the signer via npx \u2014 the pre-#1586 shape that cannot start under a 120s startup timeout.` : `Config at ${configPath} is missing the Haven entries${sidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
|
|
1368
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1369
|
-
});
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
if (identity?.api_key && (identity.hosted_mcp_url || identity.api_url)) {
|
|
1373
|
-
const hostedUrl = identity.hosted_mcp_url ?? `${identity.api_url}/mcp`;
|
|
1620
|
+
const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
1621
|
+
if (identity?.api_key && hostedUrl) {
|
|
1374
1622
|
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
1375
1623
|
checks.push({
|
|
1376
1624
|
id: "hosted_mcp",
|
|
@@ -1390,7 +1638,43 @@ async function runDoctor(input, deps = {}) {
|
|
|
1390
1638
|
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
1391
1639
|
});
|
|
1392
1640
|
}
|
|
1393
|
-
|
|
1641
|
+
const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
|
|
1642
|
+
if (identity?.api_key && identity.api_url) {
|
|
1643
|
+
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
1644
|
+
identity.api_key,
|
|
1645
|
+
identity.api_url,
|
|
1646
|
+
deps.fetch
|
|
1647
|
+
);
|
|
1648
|
+
if (probe.status !== "ok") {
|
|
1649
|
+
checks.push({
|
|
1650
|
+
id: "identity_match",
|
|
1651
|
+
label: "Hosted identity matches the local signing key",
|
|
1652
|
+
ok: false,
|
|
1653
|
+
detail: probe.status === "unauthorized" ? "The stored API key was rejected, so the agent it authenticates as cannot be compared with the local signing key." : `Could not read the hosted identity (${probe.status}) \u2014 the comparison did not happen, so it cannot be reported as a match.`,
|
|
1654
|
+
repair: probe.status === "unauthorized" ? `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : `Restore network access to the Haven API, then re-run: ${RERUN} --doctor --runtime ${input.runtime}`
|
|
1655
|
+
});
|
|
1656
|
+
} else if (!localDelegate) {
|
|
1657
|
+
checks.push({
|
|
1658
|
+
id: "identity_match",
|
|
1659
|
+
label: "Hosted identity matches the local signing key",
|
|
1660
|
+
ok: false,
|
|
1661
|
+
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
1662
|
+
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
1663
|
+
});
|
|
1664
|
+
} else {
|
|
1665
|
+
const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
|
|
1666
|
+
checks.push({
|
|
1667
|
+
id: "identity_match",
|
|
1668
|
+
label: "Hosted identity matches the local signing key",
|
|
1669
|
+
ok: same,
|
|
1670
|
+
detail: same ? `The stored API key authenticates as the agent whose signing key is in this directory (${shortAddress(localDelegate)}).` : `MISMATCH: the stored API key authenticates as agent ${probe.agentId ?? "unknown"} with delegate ${shortAddress(probe.delegateAddress ?? "unknown")}, but signer.json here holds ${shortAddress(localDelegate)}. This runtime would quote as one agent and sign as another.`,
|
|
1671
|
+
...same ? {} : {
|
|
1672
|
+
repair: `Re-run setup for this agent so its API key and signing key come from one run: ${RERUN} --setup <token>. Do not hand-edit either file.`
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (sidecar) {
|
|
1394
1678
|
const consent = await getLocalSignerConsentStatus(join(directory, "signer.json"));
|
|
1395
1679
|
if (!consent.acknowledged) {
|
|
1396
1680
|
checks.push({
|
|
@@ -1418,7 +1702,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
1418
1702
|
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1419
1703
|
});
|
|
1420
1704
|
}
|
|
1421
|
-
} else
|
|
1705
|
+
} else {
|
|
1422
1706
|
checks.push({
|
|
1423
1707
|
id: "signer_process",
|
|
1424
1708
|
label: "Signer stdio handshake",
|
|
@@ -1427,6 +1711,171 @@ async function runDoctor(input, deps = {}) {
|
|
|
1427
1711
|
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1428
1712
|
});
|
|
1429
1713
|
}
|
|
1714
|
+
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
1715
|
+
}
|
|
1716
|
+
async function runDoctor(input, deps = {}) {
|
|
1717
|
+
const homeDir = deps.homeDir ?? homedir();
|
|
1718
|
+
const checks = [];
|
|
1719
|
+
let signerCapabilities;
|
|
1720
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1721
|
+
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1722
|
+
let configText = null;
|
|
1723
|
+
if (configPath !== null) {
|
|
1724
|
+
try {
|
|
1725
|
+
configText = await readFile(configPath, "utf8");
|
|
1726
|
+
} catch {
|
|
1727
|
+
configText = null;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const allDirectories = directory ? [directory, ...others] : others;
|
|
1731
|
+
let bareOwnerExists = false;
|
|
1732
|
+
for (const dir of allDirectories) {
|
|
1733
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
1734
|
+
if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
|
|
1735
|
+
bareOwnerExists = true;
|
|
1736
|
+
break;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
const inventory = [];
|
|
1740
|
+
const capabilitiesByDirectory = /* @__PURE__ */ new Map();
|
|
1741
|
+
const primaryChecksById = /* @__PURE__ */ new Map();
|
|
1742
|
+
for (const dir of allDirectories) {
|
|
1743
|
+
const identity = await readIdentity(dir);
|
|
1744
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
1745
|
+
const tombstone = await readAgentTombstone(dir);
|
|
1746
|
+
const slug = sidecar?.server_name;
|
|
1747
|
+
const names = serverNamesFor(slug);
|
|
1748
|
+
if (!identity?.api_key) {
|
|
1749
|
+
inventory.push({
|
|
1750
|
+
...slug ? { slug } : {},
|
|
1751
|
+
...tombstone?.agent_id ? { agentId: tombstone.agent_id } : {},
|
|
1752
|
+
directory: dir,
|
|
1753
|
+
classification: tombstone ? "retired" : "orphaned",
|
|
1754
|
+
checks: []
|
|
1755
|
+
});
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
|
|
1759
|
+
const entry = {
|
|
1760
|
+
...slug ? { slug } : {},
|
|
1761
|
+
...identity.agent_id ? { agentId: identity.agent_id } : {},
|
|
1762
|
+
directory: dir,
|
|
1763
|
+
classification: wired ? "wired" : "superseded",
|
|
1764
|
+
checks: []
|
|
1765
|
+
};
|
|
1766
|
+
if (wired) {
|
|
1767
|
+
const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
|
|
1768
|
+
entry.checks = result.checks;
|
|
1769
|
+
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
1770
|
+
}
|
|
1771
|
+
inventory.push(entry);
|
|
1772
|
+
}
|
|
1773
|
+
const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
|
|
1774
|
+
const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
|
|
1775
|
+
if (primaryDirectory) {
|
|
1776
|
+
const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
|
|
1777
|
+
signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
|
|
1778
|
+
for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
|
|
1779
|
+
}
|
|
1780
|
+
if (!primaryDirectory) {
|
|
1781
|
+
checks.push({
|
|
1782
|
+
id: "credentials",
|
|
1783
|
+
label: "Agent credentials",
|
|
1784
|
+
ok: false,
|
|
1785
|
+
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
1786
|
+
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
1787
|
+
});
|
|
1788
|
+
} else {
|
|
1789
|
+
const primaryIdentity = await readIdentity(primaryDirectory);
|
|
1790
|
+
const primarySidecar = await readRuntimeSidecar(primaryDirectory);
|
|
1791
|
+
if (primaryChecksById.size === 0) {
|
|
1792
|
+
const result = await checksForAgent(
|
|
1793
|
+
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
1794
|
+
input,
|
|
1795
|
+
deps
|
|
1796
|
+
);
|
|
1797
|
+
signerCapabilities = result.signerCapabilities;
|
|
1798
|
+
for (const check of result.checks) primaryChecksById.set(check.id, check);
|
|
1799
|
+
}
|
|
1800
|
+
for (const id of ["credentials", "signer_runtime"]) {
|
|
1801
|
+
const check = primaryChecksById.get(id);
|
|
1802
|
+
if (check) checks.push(check);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
if (configPath === null) {
|
|
1806
|
+
checks.push({
|
|
1807
|
+
id: "runtime_config",
|
|
1808
|
+
label: "Runtime MCP config",
|
|
1809
|
+
ok: true,
|
|
1810
|
+
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1811
|
+
});
|
|
1812
|
+
} else if (configText === null) {
|
|
1813
|
+
checks.push({
|
|
1814
|
+
id: "runtime_config",
|
|
1815
|
+
label: "Runtime MCP config",
|
|
1816
|
+
ok: false,
|
|
1817
|
+
detail: `No runtime config at ${configPath}.`,
|
|
1818
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1819
|
+
});
|
|
1820
|
+
} else {
|
|
1821
|
+
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
1822
|
+
const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
|
|
1823
|
+
const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
|
|
1824
|
+
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1825
|
+
const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
|
|
1826
|
+
const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
|
|
1827
|
+
checks.push({
|
|
1828
|
+
id: "runtime_config",
|
|
1829
|
+
label: "Runtime MCP config",
|
|
1830
|
+
ok,
|
|
1831
|
+
detail: ok ? `Config at ${configPath} references the hosted server and the prepared signer wrapper.` : signerViaNpx ? `Config at ${configPath} still launches the signer via npx \u2014 the pre-#1586 shape that cannot start under a 120s startup timeout.` : `Config at ${configPath} is missing the Haven entries${primarySidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
|
|
1832
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
for (const id of ["hosted_mcp", "identity_match"]) {
|
|
1836
|
+
const check = primaryChecksById.get(id);
|
|
1837
|
+
if (check) checks.push(check);
|
|
1838
|
+
}
|
|
1839
|
+
const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
|
|
1840
|
+
if (otherEntries.length > 0) {
|
|
1841
|
+
const live = [];
|
|
1842
|
+
const revoked = [];
|
|
1843
|
+
const unverifiable = [];
|
|
1844
|
+
const retired = [];
|
|
1845
|
+
for (const entry of otherEntries) {
|
|
1846
|
+
const identity = await readIdentity(entry.directory);
|
|
1847
|
+
const tombstone = await readAgentTombstone(entry.directory);
|
|
1848
|
+
const otherAgent = identity?.agent_id ?? tombstone?.agent_id ?? basename(entry.directory);
|
|
1849
|
+
const otherUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
1850
|
+
if (!identity?.api_key || !otherUrl) {
|
|
1851
|
+
if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
|
|
1852
|
+
else unverifiable.push(`${otherAgent} (no stored key/URL to probe)`);
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
|
|
1856
|
+
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, otherUrl, deps.fetch);
|
|
1857
|
+
if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
|
|
1858
|
+
else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
|
|
1859
|
+
else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
|
|
1860
|
+
}
|
|
1861
|
+
const parts = [];
|
|
1862
|
+
if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
|
|
1863
|
+
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
1864
|
+
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
1865
|
+
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
1866
|
+
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
1867
|
+
checks.push({
|
|
1868
|
+
id: "superseded_agents",
|
|
1869
|
+
label: "Superseded agent credentials",
|
|
1870
|
+
ok: supersededLive.length === 0,
|
|
1871
|
+
detail: supersededLive.length > 0 ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
|
|
1872
|
+
...supersededLive.length > 0 ? {
|
|
1873
|
+
repair: `Revoke ${supersededLive.join(", ")} on the Haven agent page, then remove the old director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
|
|
1874
|
+
} : {}
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
const signerProcess = primaryChecksById.get("signer_process");
|
|
1878
|
+
if (signerProcess) checks.push(signerProcess);
|
|
1430
1879
|
const restart = restartRequiredForRuntime(input.runtime, deps.env);
|
|
1431
1880
|
checks.push({
|
|
1432
1881
|
id: "restart",
|
|
@@ -1434,20 +1883,24 @@ async function runDoctor(input, deps = {}) {
|
|
|
1434
1883
|
ok: true,
|
|
1435
1884
|
detail: restart ? "This runtime loads MCP config at startup \u2014 restart it after any repair before expecting the tools to appear." : "No restart requirement known for this runtime."
|
|
1436
1885
|
});
|
|
1886
|
+
const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
|
|
1437
1887
|
return {
|
|
1438
1888
|
version: 1,
|
|
1439
|
-
ok: checks.every((check) => check.ok),
|
|
1889
|
+
ok: checks.every((check) => check.ok) && wiredOk,
|
|
1440
1890
|
runtime: input.runtime,
|
|
1441
|
-
credentialDirectory:
|
|
1891
|
+
credentialDirectory: primaryDirectory,
|
|
1442
1892
|
checks,
|
|
1893
|
+
agents: inventory,
|
|
1443
1894
|
...signerCapabilities ? { signerCapabilities } : {}
|
|
1444
1895
|
};
|
|
1445
1896
|
}
|
|
1446
1897
|
async function runRepair(input, deps = {}) {
|
|
1447
1898
|
const homeDir = deps.homeDir ?? homedir();
|
|
1448
1899
|
const messages = [];
|
|
1449
|
-
const { directory,
|
|
1450
|
-
if (
|
|
1900
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1901
|
+
if (others.length > 0) {
|
|
1902
|
+
messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
|
|
1903
|
+
}
|
|
1451
1904
|
if (!directory) {
|
|
1452
1905
|
return {
|
|
1453
1906
|
ok: false,
|
|
@@ -1509,6 +1962,9 @@ var init_doctor = __esm({
|
|
|
1509
1962
|
init_config_writers();
|
|
1510
1963
|
init_runtime_registry();
|
|
1511
1964
|
init_signer_consent();
|
|
1965
|
+
init_tombstone();
|
|
1966
|
+
init_server_names();
|
|
1967
|
+
init_redact();
|
|
1512
1968
|
RERUN = "npx @haven_ai/connect@alpha";
|
|
1513
1969
|
}
|
|
1514
1970
|
});
|
|
@@ -1618,16 +2074,9 @@ function agentApiKeyPrefix(apiKey) {
|
|
|
1618
2074
|
return apiKey.slice(0, 12);
|
|
1619
2075
|
}
|
|
1620
2076
|
|
|
1621
|
-
// src/
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
function redactSecrets(value) {
|
|
1625
|
-
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
1626
|
-
}
|
|
1627
|
-
function shortAddress(address) {
|
|
1628
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
1629
|
-
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1630
|
-
}
|
|
2077
|
+
// src/runtime.ts
|
|
2078
|
+
init_redact();
|
|
2079
|
+
init_server_names();
|
|
1631
2080
|
async function preflightCredentialStorage(input = {}) {
|
|
1632
2081
|
const directory = defaultCredentialRoot(input.baseDir);
|
|
1633
2082
|
await mkdir(directory, { recursive: true, mode: 448 });
|
|
@@ -1641,7 +2090,7 @@ async function preflightCredentialStorage(input = {}) {
|
|
|
1641
2090
|
return directory;
|
|
1642
2091
|
}
|
|
1643
2092
|
async function writeCredentialFiles(input) {
|
|
1644
|
-
const directory = defaultAgentDirectory(input.agentId, input.baseDir);
|
|
2093
|
+
const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
|
|
1645
2094
|
await mkdir(directory, { recursive: true, mode: 448 });
|
|
1646
2095
|
await restrictPermissions(directory, 448, input.warn);
|
|
1647
2096
|
const identityPath = join(directory, "identity.json");
|
|
@@ -1706,6 +2155,17 @@ async function writeCredentialFiles(input) {
|
|
|
1706
2155
|
}
|
|
1707
2156
|
return { directory, identityPath, signerPath, agentPath };
|
|
1708
2157
|
}
|
|
2158
|
+
async function assertServerSlugAvailable(serverName, baseDir) {
|
|
2159
|
+
const directory = defaultAgentDirectory(serverName, baseDir);
|
|
2160
|
+
try {
|
|
2161
|
+
await stat(join(directory, "identity.json"));
|
|
2162
|
+
} catch {
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
throw new Error(
|
|
2166
|
+
`The name "${serverName}" is already wired on this machine (${directory} holds credentials). Pick a different --name, or revoke and remove that agent first \u2014 connect never overwrites credentials.`
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
1709
2169
|
function defaultAgentDirectory(agentId, baseDir = join(homedir(), ".haven", "agents")) {
|
|
1710
2170
|
return resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
1711
2171
|
}
|
|
@@ -1745,6 +2205,7 @@ async function restrictPermissions(path, mode, warn) {
|
|
|
1745
2205
|
|
|
1746
2206
|
// src/runtime-install.ts
|
|
1747
2207
|
init_config_writers();
|
|
2208
|
+
init_server_names();
|
|
1748
2209
|
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
1749
2210
|
try {
|
|
1750
2211
|
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
@@ -1870,7 +2331,8 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
|
1870
2331
|
wrapperPath,
|
|
1871
2332
|
runtimeDirectory,
|
|
1872
2333
|
npmCacheDirectory,
|
|
1873
|
-
cliPath
|
|
2334
|
+
cliPath,
|
|
2335
|
+
serverName: input.serverName
|
|
1874
2336
|
});
|
|
1875
2337
|
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1876
2338
|
return {
|
|
@@ -1966,6 +2428,7 @@ async function writeWrapper2(input) {
|
|
|
1966
2428
|
}
|
|
1967
2429
|
async function writeRuntimeSidecar2(input) {
|
|
1968
2430
|
const value = {
|
|
2431
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1969
2432
|
mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
1970
2433
|
mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
1971
2434
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
@@ -2199,12 +2662,13 @@ async function installRuntime(input, deps = {}) {
|
|
|
2199
2662
|
}
|
|
2200
2663
|
const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
|
|
2201
2664
|
progress("Setting up your Haven tools\u2026");
|
|
2202
|
-
const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await configureClaudeCodeHosted(deps, input, signerCommand) : await writeRuntimeConfig({
|
|
2665
|
+
const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "", input.serverName) : await configureClaudeCodeHosted(deps, input, signerCommand) : await writeRuntimeConfig({
|
|
2203
2666
|
runtime,
|
|
2204
2667
|
hostedMcpUrl: input.hostedMcpUrl,
|
|
2205
2668
|
apiKey: input.apiKey,
|
|
2206
2669
|
identityPath: input.identityPath,
|
|
2207
2670
|
signerPath: input.signerPath,
|
|
2671
|
+
serverName: input.serverName,
|
|
2208
2672
|
credentialDirectory: input.credentialDirectory,
|
|
2209
2673
|
localMcpCommand: localRuntimeInstall?.command,
|
|
2210
2674
|
signerCommand,
|
|
@@ -2284,7 +2748,7 @@ function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
|
2284
2748
|
restartRequired: restartRequiredForRuntime(runtime, env)
|
|
2285
2749
|
};
|
|
2286
2750
|
}
|
|
2287
|
-
async function configureClaudeCode(deps, localMcpCommand) {
|
|
2751
|
+
async function configureClaudeCode(deps, localMcpCommand, serverName) {
|
|
2288
2752
|
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
2289
2753
|
const serverJson = JSON.stringify({
|
|
2290
2754
|
type: "stdio",
|
|
@@ -2294,12 +2758,13 @@ async function configureClaudeCode(deps, localMcpCommand) {
|
|
|
2294
2758
|
});
|
|
2295
2759
|
try {
|
|
2296
2760
|
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
2297
|
-
|
|
2298
|
-
await runCommand("claude", ["mcp", "remove",
|
|
2299
|
-
await runCommand("claude", ["mcp", "
|
|
2300
|
-
|
|
2761
|
+
const names = serverNamesFor(serverName);
|
|
2762
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2763
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2764
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, serverJson, "--scope", "user"]).catch(async () => {
|
|
2765
|
+
await runCommand("claude", ["mcp", "add", names.hosted, "--scope", "user", "--", localMcpCommand]);
|
|
2301
2766
|
});
|
|
2302
|
-
const verified = await runCommand("claude", ["mcp", "get",
|
|
2767
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2303
2768
|
return {
|
|
2304
2769
|
hostedConfigured: false,
|
|
2305
2770
|
signerConfigured: true,
|
|
@@ -2344,11 +2809,12 @@ async function configureClaudeCodeHosted(deps, input, signerCommand) {
|
|
|
2344
2809
|
env: {}
|
|
2345
2810
|
});
|
|
2346
2811
|
try {
|
|
2347
|
-
|
|
2348
|
-
await runCommand("claude", ["mcp", "remove",
|
|
2349
|
-
await runCommand("claude", ["mcp", "
|
|
2350
|
-
await runCommand("claude", ["mcp", "add-json",
|
|
2351
|
-
|
|
2812
|
+
const names = serverNamesFor(input.serverName);
|
|
2813
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2814
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2815
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, hostedJson, "--scope", "user"]);
|
|
2816
|
+
await runCommand("claude", ["mcp", "add-json", names.signer, signerJson, "--scope", "user"]);
|
|
2817
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2352
2818
|
return {
|
|
2353
2819
|
hostedConfigured: true,
|
|
2354
2820
|
signerConfigured: true,
|
|
@@ -2452,7 +2918,8 @@ async function prepareRuntimeForLocalMcp(input, deps) {
|
|
|
2452
2918
|
credentialDirectory: input.credentialDirectory,
|
|
2453
2919
|
identityPath: input.identityPath,
|
|
2454
2920
|
signerPath: input.signerPath,
|
|
2455
|
-
homeDir: deps.homeDir
|
|
2921
|
+
homeDir: deps.homeDir,
|
|
2922
|
+
serverName: input.serverName
|
|
2456
2923
|
});
|
|
2457
2924
|
}
|
|
2458
2925
|
async function prepareSignerForRuntime(input, deps) {
|
|
@@ -2466,7 +2933,8 @@ async function prepareSignerForRuntime(input, deps) {
|
|
|
2466
2933
|
return prepare({
|
|
2467
2934
|
credentialDirectory: input.credentialDirectory,
|
|
2468
2935
|
signerPath: input.signerPath,
|
|
2469
|
-
homeDir: deps.homeDir
|
|
2936
|
+
homeDir: deps.homeDir,
|
|
2937
|
+
serverName: input.serverName
|
|
2470
2938
|
});
|
|
2471
2939
|
}
|
|
2472
2940
|
async function runLocalMcpProbe(runtimeInstall, deps) {
|
|
@@ -2486,8 +2954,179 @@ function localRuntimePrepareErrorCode(err) {
|
|
|
2486
2954
|
|
|
2487
2955
|
// src/runtime.ts
|
|
2488
2956
|
init_runtime_registry();
|
|
2957
|
+
init_connect_error();
|
|
2958
|
+
|
|
2959
|
+
// src/installed-clients.ts
|
|
2960
|
+
init_connect_error();
|
|
2961
|
+
init_config_writers();
|
|
2962
|
+
init_runtime_registry();
|
|
2963
|
+
var SCAN_ORDER = [
|
|
2964
|
+
"claude-code",
|
|
2965
|
+
"codex-cli",
|
|
2966
|
+
"cursor",
|
|
2967
|
+
"vscode",
|
|
2968
|
+
"vscode-insiders",
|
|
2969
|
+
"claude-desktop",
|
|
2970
|
+
"hermes"
|
|
2971
|
+
];
|
|
2972
|
+
function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env = process.env) {
|
|
2973
|
+
const targets = [
|
|
2974
|
+
{
|
|
2975
|
+
runtime: "claude-code",
|
|
2976
|
+
label: "Claude Code",
|
|
2977
|
+
// Claude Code is configured through its own CLI (`claude mcp add-json`),
|
|
2978
|
+
// not by writing a file this module owns — so its evidence is the
|
|
2979
|
+
// client directory, never a config path.
|
|
2980
|
+
configPath: null,
|
|
2981
|
+
markers: [join(homeDir, ".claude"), join(homeDir, ".claude.json")]
|
|
2982
|
+
},
|
|
2983
|
+
{
|
|
2984
|
+
runtime: "codex-cli",
|
|
2985
|
+
// Both Codex surfaces write the same ~/.codex/config.toml, so they are
|
|
2986
|
+
// ONE candidate. Splitting them would ask the user to answer a question
|
|
2987
|
+
// whose answers are the same write.
|
|
2988
|
+
label: "Codex (CLI or Desktop)",
|
|
2989
|
+
configPath: runtimeConfigPathFor("codex-cli", homeDir),
|
|
2990
|
+
markers: [join(homeDir, ".codex")]
|
|
2991
|
+
},
|
|
2992
|
+
{
|
|
2993
|
+
runtime: "cursor",
|
|
2994
|
+
label: "Cursor",
|
|
2995
|
+
configPath: runtimeConfigPathFor("cursor", homeDir),
|
|
2996
|
+
markers: [join(homeDir, ".cursor")]
|
|
2997
|
+
},
|
|
2998
|
+
{
|
|
2999
|
+
runtime: "vscode",
|
|
3000
|
+
label: "VS Code",
|
|
3001
|
+
configPath: runtimeConfigPathFor("vscode", homeDir),
|
|
3002
|
+
markers: [resolve(cwd, ".vscode")]
|
|
3003
|
+
},
|
|
3004
|
+
{
|
|
3005
|
+
runtime: "vscode-insiders",
|
|
3006
|
+
label: "VS Code Insiders",
|
|
3007
|
+
configPath: runtimeConfigPathFor("vscode-insiders", homeDir),
|
|
3008
|
+
markers: []
|
|
3009
|
+
},
|
|
3010
|
+
{
|
|
3011
|
+
runtime: "claude-desktop",
|
|
3012
|
+
label: "Claude Desktop",
|
|
3013
|
+
configPath: runtimeConfigPathFor("claude-desktop", homeDir),
|
|
3014
|
+
markers: []
|
|
3015
|
+
},
|
|
3016
|
+
{
|
|
3017
|
+
runtime: "hermes",
|
|
3018
|
+
label: "Hermes Agent",
|
|
3019
|
+
configPath: runtimeConfigPathFor("hermes", homeDir),
|
|
3020
|
+
markers: [env.HERMES_HOME ?? join(homeDir, ".hermes")]
|
|
3021
|
+
}
|
|
3022
|
+
];
|
|
3023
|
+
return targets.filter((target) => runtimeProfile(target.runtime, {}).canWriteRuntimeConfig);
|
|
3024
|
+
}
|
|
3025
|
+
async function scanInstalledClients(options = {}) {
|
|
3026
|
+
const exists = options.exists ?? pathExists;
|
|
3027
|
+
const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
|
|
3028
|
+
const found = [];
|
|
3029
|
+
for (const target of targets) {
|
|
3030
|
+
if (target.configPath && await exists(target.configPath)) {
|
|
3031
|
+
found.push({
|
|
3032
|
+
runtime: target.runtime,
|
|
3033
|
+
label: target.label,
|
|
3034
|
+
detail: `MCP config found at ${target.configPath}`,
|
|
3035
|
+
configPath: target.configPath,
|
|
3036
|
+
evidence: "config-file"
|
|
3037
|
+
});
|
|
3038
|
+
continue;
|
|
3039
|
+
}
|
|
3040
|
+
for (const marker of target.markers) {
|
|
3041
|
+
if (!await exists(marker)) continue;
|
|
3042
|
+
found.push({
|
|
3043
|
+
runtime: target.runtime,
|
|
3044
|
+
label: target.label,
|
|
3045
|
+
detail: `installed (${marker})`,
|
|
3046
|
+
configPath: target.configPath,
|
|
3047
|
+
evidence: "client-directory"
|
|
3048
|
+
});
|
|
3049
|
+
break;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
return found.sort((a, b) => {
|
|
3053
|
+
if (a.evidence !== b.evidence) return a.evidence === "config-file" ? -1 : 1;
|
|
3054
|
+
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
var MAX_PROMPT_ATTEMPTS = 3;
|
|
3058
|
+
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3059
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3060
|
+
io.write("Haven could not detect which agent runtime this is.\n");
|
|
3061
|
+
io.write("These agent clients are installed on this machine:\n");
|
|
3062
|
+
candidates.forEach((candidate, index) => {
|
|
3063
|
+
io.write(` ${index + 1}) ${candidate.label} \u2014 ${candidate.detail}
|
|
3064
|
+
`);
|
|
3065
|
+
});
|
|
3066
|
+
io.write("Haven writes an API key and a signing key into the client you pick, so pick the one your agent actually runs in.\n");
|
|
3067
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
3068
|
+
const answer = await io.question(`Which one? [1-${candidates.length}] (default 1 \u2014 ${candidates[0].label}): `);
|
|
3069
|
+
if (answer === null) throw promptAbortedError("the prompt was cancelled");
|
|
3070
|
+
const trimmed = answer.trim();
|
|
3071
|
+
if (trimmed === "") return candidates[0].runtime;
|
|
3072
|
+
const picked = Number.parseInt(trimmed, 10);
|
|
3073
|
+
if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
|
|
3074
|
+
return candidates[picked - 1].runtime;
|
|
3075
|
+
}
|
|
3076
|
+
io.write(`"${trimmed}" is not one of 1-${candidates.length}.
|
|
3077
|
+
`);
|
|
3078
|
+
}
|
|
3079
|
+
throw promptAbortedError(`no valid choice after ${MAX_PROMPT_ATTEMPTS} attempts`);
|
|
3080
|
+
}
|
|
3081
|
+
async function resolveRuntimeByInstalledClientPrompt(options = {}) {
|
|
3082
|
+
const candidates = await scanInstalledClients(options);
|
|
3083
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3084
|
+
return promptForInstalledClient(candidates, options.io ?? defaultPromptIo());
|
|
3085
|
+
}
|
|
3086
|
+
function noInstalledClientsError() {
|
|
3087
|
+
return new ConnectError(
|
|
3088
|
+
"runtime_no_installed_clients",
|
|
3089
|
+
"Could not determine the agent runtime: nothing was detected in this environment, and no agent client Haven can configure is installed on this machine. Re-run with --runtime <name> naming the client you want configured, or --runtime other to store credentials and finish the MCP setup by hand.",
|
|
3090
|
+
"rerun_connect_with_explicit_runtime"
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
3093
|
+
function promptAbortedError(reason) {
|
|
3094
|
+
return new ConnectError(
|
|
3095
|
+
"runtime_prompt_aborted",
|
|
3096
|
+
`Runtime not chosen (${reason}). Nothing was written: no agent was created, no credentials were stored, and the Haven setup token is still unused. Run the setup command again, or pass --runtime <name> to skip the prompt.`,
|
|
3097
|
+
"rerun_connect_and_choose_a_runtime"
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
async function pathExists(path) {
|
|
3101
|
+
try {
|
|
3102
|
+
await access(path);
|
|
3103
|
+
return true;
|
|
3104
|
+
} catch {
|
|
3105
|
+
return false;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
function defaultPromptIo() {
|
|
3109
|
+
return {
|
|
3110
|
+
write: (text) => process.stdout.write(text),
|
|
3111
|
+
question: (query) => new Promise((resolvePromise) => {
|
|
3112
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3113
|
+
let settled = false;
|
|
3114
|
+
const settle = (value) => {
|
|
3115
|
+
if (settled) return;
|
|
3116
|
+
settled = true;
|
|
3117
|
+
rl.close();
|
|
3118
|
+
resolvePromise(value);
|
|
3119
|
+
};
|
|
3120
|
+
rl.once("SIGINT", () => settle(null));
|
|
3121
|
+
rl.once("close", () => settle(null));
|
|
3122
|
+
rl.question(query, (answer) => settle(answer));
|
|
3123
|
+
})
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// src/runtime.ts
|
|
2489
3128
|
init_runtime_manifest();
|
|
2490
|
-
var CONNECTOR_VERSION = "0.1.
|
|
3129
|
+
var CONNECTOR_VERSION = "0.1.29-alpha.0";
|
|
2491
3130
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
2492
3131
|
async function runConnect(options, deps = {}) {
|
|
2493
3132
|
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
@@ -2503,24 +3142,49 @@ async function runConnect(options, deps = {}) {
|
|
|
2503
3142
|
const runRuntimeInstall = deps.installRuntime ?? installRuntime;
|
|
2504
3143
|
const generateKey = deps.generateKey ?? generateDelegateKey;
|
|
2505
3144
|
const generateLocalApiKey = deps.generateApiKey ?? generateAgentApiKey;
|
|
2506
|
-
const
|
|
3145
|
+
const selection = await resolveRuntimeSelection(options.runtime, options.runtimeForce, {
|
|
3146
|
+
env: deps.env ?? process.env,
|
|
3147
|
+
selfReported: options.runtimeSelfReport,
|
|
3148
|
+
promptForRuntime: runtimeSelectionPrompt(options, deps)
|
|
3149
|
+
});
|
|
3150
|
+
if (!selection.runtime) {
|
|
3151
|
+
throw new ConnectError(
|
|
3152
|
+
"runtime_undetermined",
|
|
3153
|
+
`Could not determine the agent runtime: nothing was detected in this environment and no --runtime was given. If you are an AI agent running this command: re-run it once, unchanged except for adding --runtime <name>, naming the harness you are running in \u2014 one of: ${RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Do not guess: if your harness is not one of those, use --runtime other, which stores the credentials and prints the manual MCP steps. Nothing was written and the Haven setup token is still unused.`,
|
|
3154
|
+
"rerun_connect_with_explicit_runtime"
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
const runtime = selection.runtime;
|
|
3158
|
+
const installCapabilities = runtimeInstallCapabilities(runtime);
|
|
2507
3159
|
if (options.localMcp) {
|
|
2508
|
-
|
|
2509
|
-
if (!supportsLocalMcp(resolvedRuntime)) {
|
|
3160
|
+
if (!supportsLocalMcp(runtime)) {
|
|
2510
3161
|
throw new Error(
|
|
2511
|
-
`--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(
|
|
3162
|
+
`--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(runtime).label}. Re-run without --local to use the default hosted MCP + local signer setup.`
|
|
2512
3163
|
);
|
|
2513
3164
|
}
|
|
2514
3165
|
}
|
|
3166
|
+
if (selection.overrodeHint) {
|
|
3167
|
+
log(`runtime: ${runtime} (detected; ignoring the ${selection.overrodeHint} hint \u2014 pass --runtime-force ${selection.overrodeHint} to override)`);
|
|
3168
|
+
}
|
|
3169
|
+
if (selection.discardedHint) {
|
|
3170
|
+
log(`runtime: ${runtime} (detected; "${selection.discardedHint}" is not a runtime Haven knows \u2014 valid values: ${RUNTIME_FLAG_VALUES})`);
|
|
3171
|
+
}
|
|
3172
|
+
if (selection.source === "prompted") {
|
|
3173
|
+
log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
|
|
3174
|
+
}
|
|
2515
3175
|
log("Warming up your connection to Haven\u2026");
|
|
2516
3176
|
const setup = await api.resolveSetup({
|
|
2517
3177
|
setupToken: options.setupToken,
|
|
2518
3178
|
connectorVersion,
|
|
2519
|
-
runtime
|
|
3179
|
+
runtime
|
|
2520
3180
|
});
|
|
2521
3181
|
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
2522
3182
|
printSetupSummary(setup, log);
|
|
2523
3183
|
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
3184
|
+
if (options.serverName) {
|
|
3185
|
+
assertValidServerSlug(options.serverName);
|
|
3186
|
+
await assertServerSlugAvailable(options.serverName, options.credentialsDir);
|
|
3187
|
+
}
|
|
2524
3188
|
log("Checked local credential storage \u2014 all clear.");
|
|
2525
3189
|
const localKey = generateKey();
|
|
2526
3190
|
const localApiKey = generateLocalApiKey();
|
|
@@ -2532,7 +3196,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2532
3196
|
registration = await api.registerSetup({
|
|
2533
3197
|
setupToken: options.setupToken,
|
|
2534
3198
|
connectorVersion,
|
|
2535
|
-
runtime
|
|
3199
|
+
runtime,
|
|
2536
3200
|
challengeId: setup.challenge.id,
|
|
2537
3201
|
delegateAddress: localKey.address,
|
|
2538
3202
|
proofSignature,
|
|
@@ -2557,6 +3221,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2557
3221
|
const credentialPaths = await writeCredentials({
|
|
2558
3222
|
baseDir: options.credentialsDir,
|
|
2559
3223
|
agentId: registration.agent_id,
|
|
3224
|
+
serverName: options.serverName,
|
|
2560
3225
|
apiKey: localApiKey,
|
|
2561
3226
|
delegateKey: localKey.privateKey,
|
|
2562
3227
|
delegateAddress: localKey.address,
|
|
@@ -2584,7 +3249,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2584
3249
|
);
|
|
2585
3250
|
}
|
|
2586
3251
|
const runtimeInstall = await runRuntimeInstall({
|
|
2587
|
-
runtime
|
|
3252
|
+
runtime,
|
|
2588
3253
|
hostedMcpUrl: registration.hosted_mcp_url,
|
|
2589
3254
|
apiKey: localApiKey,
|
|
2590
3255
|
signerPath: credentialPaths.signerPath,
|
|
@@ -2593,7 +3258,8 @@ async function runConnect(options, deps = {}) {
|
|
|
2593
3258
|
environmentLabel: options.environmentLabel ?? "Local workspace",
|
|
2594
3259
|
ackSigner: options.ackSigner,
|
|
2595
3260
|
ackLocalTools: options.ackLocalTools,
|
|
2596
|
-
localMcp: options.localMcp
|
|
3261
|
+
localMcp: options.localMcp,
|
|
3262
|
+
serverName: options.serverName
|
|
2597
3263
|
}, {
|
|
2598
3264
|
onProgress: log,
|
|
2599
3265
|
// #1543: report "runtime configured" the moment the config write settles,
|
|
@@ -2629,6 +3295,19 @@ async function runConnect(options, deps = {}) {
|
|
|
2629
3295
|
} else {
|
|
2630
3296
|
log("Haven setup on this machine is complete.");
|
|
2631
3297
|
}
|
|
3298
|
+
try {
|
|
3299
|
+
const supersededIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
3300
|
+
if (supersededIds.length > 0) {
|
|
3301
|
+
log("");
|
|
3302
|
+
log(
|
|
3303
|
+
`Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${supersededIds.join(", ")} \u2014 still exist with their own keys, and any host that was already running keeps acting as them.`
|
|
3304
|
+
);
|
|
3305
|
+
log(
|
|
3306
|
+
`If you meant to replace them: revoke them on the Haven agent page, then restart EVERY long-lived host (gateways, TUI workers, editors) \u2014 each holds the MCP wiring snapshot from its own start time, so after repeated setups each can be stuck on a DIFFERENT old agent. Then remove their directories under ~/.haven/agents (or ${RERUN_HINT} --tombstone <dir> to leave a diagnostic in their place). Run ${RERUN_HINT} --doctor to check whether their keys are still live.`
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
} catch {
|
|
3310
|
+
}
|
|
2632
3311
|
try {
|
|
2633
3312
|
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
2634
3313
|
runtime: runtimeInstall.runtime,
|
|
@@ -2703,11 +3382,16 @@ function completionOutcome(input) {
|
|
|
2703
3382
|
};
|
|
2704
3383
|
return outcome;
|
|
2705
3384
|
}
|
|
3385
|
+
function runtimeSelectionPrompt(options, deps) {
|
|
3386
|
+
if (options.interactive !== true) return void 0;
|
|
3387
|
+
if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
|
|
3388
|
+
return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
|
|
3389
|
+
}
|
|
2706
3390
|
function failedConnectOutcome(runtimeHint, error) {
|
|
2707
3391
|
const message = error instanceof Error ? error.message : "";
|
|
2708
|
-
const code = /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
|
|
3392
|
+
const code = error instanceof ConnectError ? error.code : /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
|
|
2709
3393
|
const runtime = normalizeRuntime(runtimeHint);
|
|
2710
|
-
const nextAction2 = code === "setup_challenge_expired_or_invalid" ? "return_to_haven_for_fresh_setup" : code === "unsupported_node_version" ? "install_supported_node_and_rerun_connect" : code === "local_mcp_unsupported_runtime" ? "rerun_without_local_mcp" : "review_the_safe_error_output_and_start_a_fresh_haven_setup_if_needed";
|
|
3394
|
+
const nextAction2 = error instanceof ConnectError ? error.nextAction : code === "setup_challenge_expired_or_invalid" ? "return_to_haven_for_fresh_setup" : code === "unsupported_node_version" ? "install_supported_node_and_rerun_connect" : code === "local_mcp_unsupported_runtime" ? "rerun_without_local_mcp" : "review_the_safe_error_output_and_start_a_fresh_haven_setup_if_needed";
|
|
2711
3395
|
return {
|
|
2712
3396
|
schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
|
|
2713
3397
|
outcome: "failed",
|
|
@@ -2793,7 +3477,7 @@ function describeApprovedBudget(budget) {
|
|
|
2793
3477
|
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
2794
3478
|
const intervalMs = options.intervalMs ?? 5e3;
|
|
2795
3479
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
2796
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
3480
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve9) => setTimeout(resolve9, ms)));
|
|
2797
3481
|
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
2798
3482
|
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
2799
3483
|
let waitingAnnounced = false;
|
|
@@ -2879,11 +3563,39 @@ function activationInstructionWithWhy(profile) {
|
|
|
2879
3563
|
}
|
|
2880
3564
|
return profile.activationInstruction;
|
|
2881
3565
|
}
|
|
3566
|
+
var RERUN_HINT = "npx @haven_ai/connect@alpha";
|
|
3567
|
+
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
3568
|
+
const root = defaultCredentialRoot(baseDir);
|
|
3569
|
+
let entries = [];
|
|
3570
|
+
try {
|
|
3571
|
+
entries = await readdir(root);
|
|
3572
|
+
} catch {
|
|
3573
|
+
return [];
|
|
3574
|
+
}
|
|
3575
|
+
const ids = [];
|
|
3576
|
+
for (const entry of entries) {
|
|
3577
|
+
if (join(root, entry) === currentDirectory) continue;
|
|
3578
|
+
const identityPath = join(root, entry, "identity.json");
|
|
3579
|
+
try {
|
|
3580
|
+
await stat(identityPath);
|
|
3581
|
+
} catch {
|
|
3582
|
+
continue;
|
|
3583
|
+
}
|
|
3584
|
+
try {
|
|
3585
|
+
const identity = JSON.parse(await readFile(identityPath, "utf8"));
|
|
3586
|
+
ids.push(identity.agent_id ?? entry);
|
|
3587
|
+
} catch {
|
|
3588
|
+
ids.push(entry);
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
return ids;
|
|
3592
|
+
}
|
|
2882
3593
|
function printNextSteps(result, log, approval) {
|
|
2883
3594
|
for (const line of completionHandoffLines(result, approval)) log(line);
|
|
2884
3595
|
}
|
|
2885
3596
|
|
|
2886
3597
|
// src/args.ts
|
|
3598
|
+
init_server_names();
|
|
2887
3599
|
function parseArgs(argv, env = process.env) {
|
|
2888
3600
|
const options = {
|
|
2889
3601
|
apiBaseUrl: env.HAVEN_API_URL ?? "http://localhost:3001",
|
|
@@ -2893,6 +3605,9 @@ function parseArgs(argv, env = process.env) {
|
|
|
2893
3605
|
let json = false;
|
|
2894
3606
|
let doctor = false;
|
|
2895
3607
|
let repair = false;
|
|
3608
|
+
let tombstoneDir;
|
|
3609
|
+
let tombstoneReason;
|
|
3610
|
+
let tombstoneReplacedBy;
|
|
2896
3611
|
for (let i = 0; i < argv.length; i += 1) {
|
|
2897
3612
|
const arg = argv[i];
|
|
2898
3613
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -2903,14 +3618,25 @@ function parseArgs(argv, env = process.env) {
|
|
|
2903
3618
|
doctor = true;
|
|
2904
3619
|
} else if (arg === "--repair") {
|
|
2905
3620
|
repair = true;
|
|
3621
|
+
} else if (arg === "--tombstone") {
|
|
3622
|
+
tombstoneDir = requireValue(argv, ++i, arg);
|
|
3623
|
+
} else if (arg === "--reason") {
|
|
3624
|
+
tombstoneReason = requireValue(argv, ++i, arg);
|
|
3625
|
+
} else if (arg === "--replaced-by") {
|
|
3626
|
+
tombstoneReplacedBy = requireValue(argv, ++i, arg);
|
|
2906
3627
|
} else if (arg === "--setup" || arg === "--setup-token") {
|
|
2907
3628
|
options.setupToken = requireValue(argv, ++i, arg);
|
|
2908
3629
|
} else if (arg === "--api" || arg === "--api-url") {
|
|
2909
3630
|
options.apiBaseUrl = requireValue(argv, ++i, arg);
|
|
2910
3631
|
} else if (arg === "--runtime") {
|
|
2911
3632
|
options.runtime = requireValue(argv, ++i, arg);
|
|
3633
|
+
} else if (arg === "--runtime-force") {
|
|
3634
|
+
options.runtimeForce = requireValue(argv, ++i, arg);
|
|
2912
3635
|
} else if (arg === "--credentials-dir") {
|
|
2913
3636
|
options.credentialsDir = requireValue(argv, ++i, arg);
|
|
3637
|
+
} else if (arg === "--name") {
|
|
3638
|
+
options.serverName = requireValue(argv, ++i, arg);
|
|
3639
|
+
assertValidServerSlug(options.serverName);
|
|
2914
3640
|
} else if (arg === "--environment-label") {
|
|
2915
3641
|
options.environmentLabel = requireValue(argv, ++i, arg);
|
|
2916
3642
|
} else if (arg === "--ack-local-tools") {
|
|
@@ -2928,14 +3654,21 @@ function parseArgs(argv, env = process.env) {
|
|
|
2928
3654
|
throw new Error(`Unknown option: ${arg}`);
|
|
2929
3655
|
}
|
|
2930
3656
|
}
|
|
3657
|
+
const tombstone = tombstoneDir ? { directory: tombstoneDir, reason: tombstoneReason, replacedBy: tombstoneReplacedBy } : void 0;
|
|
2931
3658
|
if (help) {
|
|
2932
|
-
return { options, help, json, doctor, repair };
|
|
3659
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
3660
|
+
}
|
|
3661
|
+
if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
3662
|
+
throw new Error("--reason and --replaced-by require --tombstone <dir>.");
|
|
3663
|
+
}
|
|
3664
|
+
if (tombstone) {
|
|
3665
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2933
3666
|
}
|
|
2934
3667
|
if (doctor || repair) {
|
|
2935
3668
|
if (!options.runtime) {
|
|
2936
3669
|
throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
|
|
2937
3670
|
}
|
|
2938
|
-
return { options, help, json, doctor, repair };
|
|
3671
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2939
3672
|
}
|
|
2940
3673
|
if (!options.setupToken) {
|
|
2941
3674
|
throw new Error("Missing --setup <hv_setup_...> setup token.");
|
|
@@ -2944,7 +3677,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
2944
3677
|
throw new Error("Missing --api <Haven API URL>.");
|
|
2945
3678
|
}
|
|
2946
3679
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
2947
|
-
return { options, help, json, doctor, repair };
|
|
3680
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2948
3681
|
}
|
|
2949
3682
|
function helpText() {
|
|
2950
3683
|
return [
|
|
@@ -2954,14 +3687,23 @@ function helpText() {
|
|
|
2954
3687
|
"sends Haven only the public signing address plus a proof signature.",
|
|
2955
3688
|
"",
|
|
2956
3689
|
"Usage:",
|
|
2957
|
-
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools
|
|
3690
|
+
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools",
|
|
2958
3691
|
"",
|
|
2959
3692
|
"Options:",
|
|
2960
3693
|
" --setup <token> Short-lived setup token from Haven.",
|
|
2961
3694
|
" --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
|
|
2962
3695
|
" --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, claude-desktop, or hermes.",
|
|
3696
|
+
" Usually unnecessary: the connector detects the runtime it runs inside, and a detection",
|
|
3697
|
+
" that contradicts this hint wins (with a printed notice). When nothing is detected, an",
|
|
3698
|
+
" interactive terminal is offered the agent clients installed on this machine; this flag is",
|
|
3699
|
+
" how an agent, or a non-interactive run, answers instead. An unknown name is refused, never guessed.",
|
|
3700
|
+
" --runtime-force <name> Escape hatch: use exactly this runtime, ignoring environment detection.",
|
|
2963
3701
|
" --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
|
|
2964
3702
|
" --environment-label <text> Non-sensitive label shown in Haven setup review.",
|
|
3703
|
+
" --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
|
|
3704
|
+
" MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
|
|
3705
|
+
" agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
|
|
3706
|
+
" single hyphens; immutable once wired. Omit for the bare haven / haven-signer pair.",
|
|
2965
3707
|
" --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
|
|
2966
3708
|
" --ack-signer Backward-compatible alias for --ack-local-tools.",
|
|
2967
3709
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
@@ -2972,6 +3714,11 @@ function helpText() {
|
|
|
2972
3714
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
2973
3715
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
2974
3716
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
3717
|
+
" --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
|
|
3718
|
+
" wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
|
|
3719
|
+
" writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
|
|
3720
|
+
" --reason <text> Reason recorded in the tombstone (with --tombstone).",
|
|
3721
|
+
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone).",
|
|
2975
3722
|
" --help Show this help.",
|
|
2976
3723
|
"",
|
|
2977
3724
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -2984,6 +3731,10 @@ function requireValue(argv, index, option) {
|
|
|
2984
3731
|
}
|
|
2985
3732
|
return value;
|
|
2986
3733
|
}
|
|
3734
|
+
|
|
3735
|
+
// src/index.ts
|
|
3736
|
+
init_redact();
|
|
3737
|
+
init_redact();
|
|
2987
3738
|
async function runCli(argv, io = {
|
|
2988
3739
|
stdout: (message) => process.stdout.write(message),
|
|
2989
3740
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -3007,6 +3758,45 @@ async function runCli(argv, io = {
|
|
|
3007
3758
|
`);
|
|
3008
3759
|
return 0;
|
|
3009
3760
|
}
|
|
3761
|
+
if (parsed.tombstone) {
|
|
3762
|
+
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
3763
|
+
const { readFile: readFile11 } = await import('fs/promises');
|
|
3764
|
+
const { join: join10 } = await import('path');
|
|
3765
|
+
try {
|
|
3766
|
+
let agentId = "unknown";
|
|
3767
|
+
try {
|
|
3768
|
+
const identity = JSON.parse(
|
|
3769
|
+
await readFile11(join10(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
3770
|
+
);
|
|
3771
|
+
agentId = identity.agent_id ?? "unknown";
|
|
3772
|
+
} catch {
|
|
3773
|
+
}
|
|
3774
|
+
const info = await writeAgentTombstone2({
|
|
3775
|
+
directory: parsed.tombstone.directory,
|
|
3776
|
+
agentId,
|
|
3777
|
+
reason: parsed.tombstone.reason ?? "retired by operator via --tombstone",
|
|
3778
|
+
replacedBy: parsed.tombstone.replacedBy
|
|
3779
|
+
});
|
|
3780
|
+
if (parsed.json) {
|
|
3781
|
+
io.stdout(`${redactSecrets(JSON.stringify({ tombstoned: true, ...info }))}
|
|
3782
|
+
`);
|
|
3783
|
+
} else {
|
|
3784
|
+
io.stdout(redactSecrets(`Tombstoned agent ${info.agent_id} at ${parsed.tombstone.directory}.
|
|
3785
|
+
`));
|
|
3786
|
+
io.stdout(
|
|
3787
|
+
"Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
|
|
3788
|
+
);
|
|
3789
|
+
io.stdout(
|
|
3790
|
+
"Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot from its own start time, and the tombstone only speaks when a stale host next probes the old path.\n"
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
return 0;
|
|
3794
|
+
} catch (err) {
|
|
3795
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
3796
|
+
`);
|
|
3797
|
+
return 1;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3010
3800
|
if (parsed.doctor || parsed.repair) {
|
|
3011
3801
|
const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
3012
3802
|
const runtime = parsed.options.runtime ?? "";
|
|
@@ -3029,6 +3819,23 @@ async function runCli(argv, io = {
|
|
|
3029
3819
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3030
3820
|
`));
|
|
3031
3821
|
}
|
|
3822
|
+
const otherAgents = report.agents.filter((agent) => agent.directory !== report.credentialDirectory);
|
|
3823
|
+
if (otherAgents.length > 0) {
|
|
3824
|
+
io.stdout("\nOther agents on this machine:\n");
|
|
3825
|
+
for (const agent of otherAgents) {
|
|
3826
|
+
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
3827
|
+
const failed = agent.checks.filter((check) => !check.ok);
|
|
3828
|
+
const verdict = agent.classification === "wired" ? failed.length === 0 ? "wired, all checks passed" : `wired, ${failed.length} check(s) FAILED` : agent.classification;
|
|
3829
|
+
io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : "\u2022"} ${name}: ${verdict}
|
|
3830
|
+
`));
|
|
3831
|
+
for (const check of failed) {
|
|
3832
|
+
io.stdout(redactSecrets(` \u2717 ${check.label}: ${check.detail}
|
|
3833
|
+
`));
|
|
3834
|
+
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3835
|
+
`));
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3032
3839
|
io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
|
|
3033
3840
|
}
|
|
3034
3841
|
return report.ok ? 0 : 1;
|
|
@@ -3040,7 +3847,15 @@ async function runCli(argv, io = {
|
|
|
3040
3847
|
}
|
|
3041
3848
|
try {
|
|
3042
3849
|
const result = await runConnect(
|
|
3043
|
-
{
|
|
3850
|
+
{
|
|
3851
|
+
...parsed.options,
|
|
3852
|
+
waitForApproval: !parsed.json,
|
|
3853
|
+
// #1719: only a human-facing run may be asked which installed client to
|
|
3854
|
+
// configure. --json is the automation contract — it must fail with a
|
|
3855
|
+
// machine-readable code, never block on stdin. runConnect additionally
|
|
3856
|
+
// requires a real TTY before it prompts.
|
|
3857
|
+
interactive: !parsed.json
|
|
3858
|
+
},
|
|
3044
3859
|
{
|
|
3045
3860
|
log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
|
|
3046
3861
|
`),
|
|
@@ -3078,8 +3893,9 @@ if (isCliEntrypoint()) void main();
|
|
|
3078
3893
|
// src/index.ts
|
|
3079
3894
|
init_signer_runtime();
|
|
3080
3895
|
init_runtime_registry();
|
|
3896
|
+
init_connect_error();
|
|
3081
3897
|
init_runtime_manifest();
|
|
3082
3898
|
|
|
3083
|
-
export { CONNECTOR_VERSION, CONNECT_OUTCOME_SCHEMA_VERSION, MCP_RUNTIME_MANIFEST, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, generateDelegateKey, helpText, installRuntime, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, redactSecrets, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, sdkPackageSpec, shortAddress, signerPackageSpec, writeCredentialFiles };
|
|
3899
|
+
export { CONNECTOR_VERSION, CONNECT_OUTCOME_SCHEMA_VERSION, ConnectError, MCP_RUNTIME_MANIFEST, RUNTIME_FLAG_VALUES, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, generateDelegateKey, helpText, installRuntime, isConnectError, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, promptForInstalledClient, redactSecrets, resolveRuntimeByInstalledClientPrompt, resolveRuntimeSelection, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, scanInstalledClients, sdkPackageSpec, shortAddress, signerPackageSpec, writeCredentialFiles };
|
|
3084
3900
|
//# sourceMappingURL=index.js.map
|
|
3085
3901
|
//# sourceMappingURL=index.js.map
|