@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.cjs
CHANGED
|
@@ -11,6 +11,7 @@ var child_process = require('child_process');
|
|
|
11
11
|
var util = require('util');
|
|
12
12
|
var crypto = require('crypto');
|
|
13
13
|
var ethers = require('ethers');
|
|
14
|
+
var readline = require('readline');
|
|
14
15
|
var fs = require('fs');
|
|
15
16
|
var url = require('url');
|
|
16
17
|
|
|
@@ -28,6 +29,69 @@ var __export = (target, all) => {
|
|
|
28
29
|
for (var name in all)
|
|
29
30
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
30
31
|
};
|
|
32
|
+
|
|
33
|
+
// src/redact.ts
|
|
34
|
+
function redactSecrets(value) {
|
|
35
|
+
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
36
|
+
}
|
|
37
|
+
function shortAddress(address) {
|
|
38
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
39
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
40
|
+
}
|
|
41
|
+
var API_KEY_RE, PRIVATE_KEY_RE;
|
|
42
|
+
var init_redact = __esm({
|
|
43
|
+
"src/redact.ts"() {
|
|
44
|
+
API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
|
|
45
|
+
PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/server-names.ts
|
|
50
|
+
function assertValidServerSlug(slug) {
|
|
51
|
+
if (slug.length === 0 || slug.length > 32 || !SLUG_RE.test(slug)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Invalid server name ${JSON.stringify(slug)}: use 1-32 lowercase letters, digits, and single hyphens (e.g. "research").`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (slug === "haven" || slug === "haven-signer") {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Invalid server name ${JSON.stringify(slug)}: "haven" and "haven-signer" are the unnamed pair's own names \u2014 omit --name for the bare pair.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (slug === "signer" || slug.startsWith("signer-")) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Invalid server name ${JSON.stringify(slug)}: "signer" and "signer-*" are reserved \u2014 they would collide with another pair's haven-signer-* entry.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function serverNamesFor(slug) {
|
|
68
|
+
if (slug === void 0) {
|
|
69
|
+
return {
|
|
70
|
+
hosted: "haven",
|
|
71
|
+
signer: "haven-signer",
|
|
72
|
+
// Historical Codex table names — every wired Codex host has these.
|
|
73
|
+
codexHosted: "haven",
|
|
74
|
+
codexSigner: "haven_signer",
|
|
75
|
+
hermesEnvKey: "MCP_HAVEN_API_KEY"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
assertValidServerSlug(slug);
|
|
79
|
+
return {
|
|
80
|
+
hosted: `haven-${slug}`,
|
|
81
|
+
signer: `haven-signer-${slug}`,
|
|
82
|
+
// Hyphens are valid TOML bare keys, so named Codex tables match the
|
|
83
|
+
// JSON/YAML names instead of inheriting the legacy underscore.
|
|
84
|
+
codexHosted: `haven-${slug}`,
|
|
85
|
+
codexSigner: `haven-signer-${slug}`,
|
|
86
|
+
hermesEnvKey: `MCP_HAVEN_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
var SLUG_RE;
|
|
90
|
+
var init_server_names = __esm({
|
|
91
|
+
"src/server-names.ts"() {
|
|
92
|
+
SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
31
95
|
function mcpPackageSpec() {
|
|
32
96
|
return `${exports.MCP_RUNTIME_MANIFEST.mcpPackage}@${exports.MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
33
97
|
}
|
|
@@ -44,9 +108,9 @@ var init_runtime_manifest = __esm({
|
|
|
44
108
|
mcpPackage: "@haven_ai/mcp",
|
|
45
109
|
mcpVersion: mcp.MCP_VERSION,
|
|
46
110
|
sdkPackage: "@haven_ai/sdk",
|
|
47
|
-
sdkVersion: "0.1.
|
|
111
|
+
sdkVersion: "0.1.29-alpha.0",
|
|
48
112
|
signerPackage: "@haven_ai/signer",
|
|
49
|
-
signerVersion: "0.1.
|
|
113
|
+
signerVersion: "0.1.29-alpha.0",
|
|
50
114
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
51
115
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
52
116
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -122,31 +186,33 @@ function buildSignerServer(spec, runtime) {
|
|
|
122
186
|
if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
|
|
123
187
|
return server;
|
|
124
188
|
}
|
|
125
|
-
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer) {
|
|
126
|
-
const config = existingJson?.trim() ? parseJsonObject(existingJson) : {};
|
|
189
|
+
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
190
|
+
const config = existingJson?.trim() ? parseJsonObject(existingJson, configPath) : {};
|
|
127
191
|
const existingRoot = config[serverRoot];
|
|
128
192
|
const servers = existingRoot && typeof existingRoot === "object" && !Array.isArray(existingRoot) ? existingRoot : {};
|
|
129
193
|
config[serverRoot] = {
|
|
130
194
|
...servers,
|
|
131
|
-
|
|
132
|
-
|
|
195
|
+
[names.hosted]: hostedServer,
|
|
196
|
+
[names.signer]: signerServer
|
|
133
197
|
};
|
|
134
198
|
return `${JSON.stringify(config, null, 2)}
|
|
135
199
|
`;
|
|
136
200
|
}
|
|
137
|
-
function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
|
|
138
|
-
if (!existingYaml?.trim())
|
|
201
|
+
function mergeHermesYaml(existingYaml, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
202
|
+
if (!existingYaml?.trim()) {
|
|
203
|
+
return renderHermesYaml({ [names.hosted]: hostedServer, [names.signer]: signerServer });
|
|
204
|
+
}
|
|
139
205
|
const doc = yaml.parseDocument(existingYaml, { keepSourceTokens: true });
|
|
140
206
|
if (doc.errors.length > 0 || !yaml.isMap(doc.contents)) {
|
|
141
|
-
throw new
|
|
207
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
|
|
142
208
|
}
|
|
143
209
|
const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
144
210
|
const existingServers = mcpPair && yaml.isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
|
|
145
211
|
const servers = isRecord(existingServers) ? existingServers : {};
|
|
146
212
|
const mergedServers = {
|
|
147
213
|
...servers,
|
|
148
|
-
|
|
149
|
-
|
|
214
|
+
[names.hosted]: hostedServer,
|
|
215
|
+
[names.signer]: signerServer
|
|
150
216
|
};
|
|
151
217
|
if (!mcpPair) {
|
|
152
218
|
return appendHermesMcpServers(existingYaml, mergedServers);
|
|
@@ -154,9 +220,9 @@ function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
|
|
|
154
220
|
if (!mcpPair.value?.range) return replaceEmptyHermesMcpServers(existingYaml, mcpPair.key?.range, mergedServers);
|
|
155
221
|
return replaceHermesMcpServers(existingYaml, mcpPair.key?.range, mcpPair.value.range, mergedServers);
|
|
156
222
|
}
|
|
157
|
-
function mergeHermesEnv(existingEnv, apiKey) {
|
|
223
|
+
function mergeHermesEnv(existingEnv, apiKey, envKey = HERMES_API_KEY_ENV) {
|
|
158
224
|
if (/[\r\n]/.test(apiKey)) throw new Error("Hermes API key must be a single line");
|
|
159
|
-
const assignment = `${
|
|
225
|
+
const assignment = `${envKey}=${apiKey}`;
|
|
160
226
|
if (!existingEnv) return `${assignment}
|
|
161
227
|
`;
|
|
162
228
|
const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -165,12 +231,12 @@ function mergeHermesEnv(existingEnv, apiKey) {
|
|
|
165
231
|
if (hasTrailingNewline) lines.pop();
|
|
166
232
|
let found = false;
|
|
167
233
|
const merged = lines.flatMap((line) => {
|
|
168
|
-
if (isHermesEnvAssignment(line)) {
|
|
234
|
+
if (isHermesEnvAssignment(line, envKey)) {
|
|
169
235
|
if (found) return [];
|
|
170
236
|
found = true;
|
|
171
237
|
return [assignment];
|
|
172
238
|
}
|
|
173
|
-
if (isAmbiguousHermesEnvLine(line)) {
|
|
239
|
+
if (isAmbiguousHermesEnvLine(line, envKey)) {
|
|
174
240
|
throw new Error("Hermes environment contains an ambiguous managed key");
|
|
175
241
|
}
|
|
176
242
|
return [line];
|
|
@@ -180,11 +246,11 @@ function mergeHermesEnv(existingEnv, apiKey) {
|
|
|
180
246
|
}
|
|
181
247
|
return `${merged.join(lineEnding)}${hasTrailingNewline ? lineEnding : ""}`;
|
|
182
248
|
}
|
|
183
|
-
function isHermesEnvAssignment(line) {
|
|
184
|
-
return
|
|
249
|
+
function isHermesEnvAssignment(line, envKey) {
|
|
250
|
+
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}[ \\t]*=`).test(line);
|
|
185
251
|
}
|
|
186
|
-
function isAmbiguousHermesEnvLine(line) {
|
|
187
|
-
return
|
|
252
|
+
function isAmbiguousHermesEnvLine(line, envKey) {
|
|
253
|
+
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}\\b`).test(line);
|
|
188
254
|
}
|
|
189
255
|
function appendHermesMcpServers(source, servers) {
|
|
190
256
|
const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
|
|
@@ -243,11 +309,14 @@ function renderHermesMcpServerEntries(servers, indent) {
|
|
|
243
309
|
function isRecord(value) {
|
|
244
310
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
245
311
|
}
|
|
246
|
-
function mergeCodexToml(existingToml, localMcpCommand) {
|
|
247
|
-
let next = removeTomlTableTree(
|
|
312
|
+
function mergeCodexToml(existingToml, localMcpCommand, names = serverNamesFor()) {
|
|
313
|
+
let next = removeTomlTableTree(
|
|
314
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
315
|
+
`mcp_servers.${names.codexSigner}`
|
|
316
|
+
);
|
|
248
317
|
next = next.trimEnd();
|
|
249
318
|
const block = [
|
|
250
|
-
|
|
319
|
+
`[mcp_servers.${names.codexHosted}]`,
|
|
251
320
|
`command = ${tomlString(localMcpCommand)}`,
|
|
252
321
|
"args = []",
|
|
253
322
|
"startup_timeout_sec = 120"
|
|
@@ -259,15 +328,18 @@ function mergeCodexToml(existingToml, localMcpCommand) {
|
|
|
259
328
|
`;
|
|
260
329
|
return merged;
|
|
261
330
|
}
|
|
262
|
-
function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec) {
|
|
263
|
-
let next = removeTomlTableTree(
|
|
331
|
+
function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec, names = serverNamesFor()) {
|
|
332
|
+
let next = removeTomlTableTree(
|
|
333
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
334
|
+
`mcp_servers.${names.codexSigner}`
|
|
335
|
+
);
|
|
264
336
|
next = next.trimEnd();
|
|
265
337
|
const block = [
|
|
266
|
-
|
|
338
|
+
`[mcp_servers.${names.codexHosted}]`,
|
|
267
339
|
`url = ${tomlString(hostedMcpUrl)}`,
|
|
268
340
|
`http_headers = { "Authorization" = ${tomlString(`Bearer ${apiKey}`)} }`,
|
|
269
341
|
"",
|
|
270
|
-
|
|
342
|
+
`[mcp_servers.${names.codexSigner}]`,
|
|
271
343
|
`command = ${tomlString(signerSpec.command)}`,
|
|
272
344
|
`args = [${signerSpec.args.map((arg) => tomlString(arg)).join(", ")}]`,
|
|
273
345
|
"startup_timeout_sec = 120"
|
|
@@ -286,7 +358,9 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
286
358
|
existing,
|
|
287
359
|
serverRoot,
|
|
288
360
|
buildHostedServer(input.hostedMcpUrl, input.apiKey, input.runtime),
|
|
289
|
-
buildSignerServer(resolveSignerLaunchSpec(input), input.runtime)
|
|
361
|
+
buildSignerServer(resolveSignerLaunchSpec(input), input.runtime),
|
|
362
|
+
serverNamesFor(input.serverName),
|
|
363
|
+
target
|
|
290
364
|
);
|
|
291
365
|
await writeOwnerOnlyText(target, merged);
|
|
292
366
|
return {
|
|
@@ -300,6 +374,7 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
300
374
|
messages: [`Updated Haven MCP entries in ${configTargetLabel(input.runtime)}.`]
|
|
301
375
|
};
|
|
302
376
|
} catch (err) {
|
|
377
|
+
const unreadable = err instanceof UnreadableRuntimeConfigError;
|
|
303
378
|
return {
|
|
304
379
|
hostedConfigured: false,
|
|
305
380
|
signerConfigured: false,
|
|
@@ -308,8 +383,11 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
308
383
|
target: configTargetLabel(input.runtime),
|
|
309
384
|
changed: false,
|
|
310
385
|
restartRequired: true,
|
|
311
|
-
messages:
|
|
312
|
-
|
|
386
|
+
messages: unreadable ? [
|
|
387
|
+
`Could not update ${configTargetLabel(input.runtime)}: ${err.message}.`,
|
|
388
|
+
`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.`
|
|
389
|
+
] : [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
|
|
390
|
+
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
313
391
|
};
|
|
314
392
|
}
|
|
315
393
|
}
|
|
@@ -318,8 +396,9 @@ async function writeHermesConfig(input, deps) {
|
|
|
318
396
|
const envTarget = hermesEnvPath(input.homeDir);
|
|
319
397
|
try {
|
|
320
398
|
const [existing, existingEnv] = await Promise.all([readOptional(target), readOptional(envTarget)]);
|
|
399
|
+
const names = serverNamesFor(input.serverName);
|
|
321
400
|
const hostedServer = {
|
|
322
|
-
...buildHostedServer(input.hostedMcpUrl, `\${${
|
|
401
|
+
...buildHostedServer(input.hostedMcpUrl, `\${${names.hermesEnvKey}}`, input.runtime),
|
|
323
402
|
enabled: true
|
|
324
403
|
};
|
|
325
404
|
const signerServer = {
|
|
@@ -329,9 +408,11 @@ async function writeHermesConfig(input, deps) {
|
|
|
329
408
|
const merged = mergeHermesYaml(
|
|
330
409
|
existing,
|
|
331
410
|
hostedServer,
|
|
332
|
-
signerServer
|
|
411
|
+
signerServer,
|
|
412
|
+
names,
|
|
413
|
+
target
|
|
333
414
|
);
|
|
334
|
-
const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey);
|
|
415
|
+
const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey, names.hermesEnvKey);
|
|
335
416
|
const writeText = deps.writeOwnerOnlyText ?? writeOwnerOnlyText;
|
|
336
417
|
await writeText(envTarget, mergedEnv);
|
|
337
418
|
try {
|
|
@@ -352,11 +433,13 @@ async function writeHermesConfig(input, deps) {
|
|
|
352
433
|
messages: [
|
|
353
434
|
`Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
|
|
354
435
|
"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`.",
|
|
436
|
+
"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.",
|
|
355
437
|
"If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
|
|
356
438
|
]
|
|
357
439
|
};
|
|
358
440
|
} catch (err) {
|
|
359
441
|
const recoveryIncomplete = err instanceof HermesConfigRecoveryError;
|
|
442
|
+
const unreadable = err instanceof UnreadableRuntimeConfigError;
|
|
360
443
|
return {
|
|
361
444
|
hostedConfigured: false,
|
|
362
445
|
signerConfigured: false,
|
|
@@ -365,8 +448,11 @@ async function writeHermesConfig(input, deps) {
|
|
|
365
448
|
target: "Hermes Agent config",
|
|
366
449
|
changed: false,
|
|
367
450
|
restartRequired: true,
|
|
368
|
-
messages:
|
|
369
|
-
|
|
451
|
+
messages: unreadable ? [
|
|
452
|
+
`Could not update Hermes Agent config: ${err.message}.`,
|
|
453
|
+
`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.`
|
|
454
|
+
] : [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."],
|
|
455
|
+
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
370
456
|
};
|
|
371
457
|
}
|
|
372
458
|
}
|
|
@@ -398,7 +484,7 @@ async function writeCodexConfig(input) {
|
|
|
398
484
|
if (!input.localMcpCommand) {
|
|
399
485
|
throw new Error("local MCP wrapper command is required");
|
|
400
486
|
}
|
|
401
|
-
const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand);
|
|
487
|
+
const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand, serverNamesFor(input.serverName));
|
|
402
488
|
await writeOwnerOnlyText(target, merged2);
|
|
403
489
|
return {
|
|
404
490
|
hostedConfigured: false,
|
|
@@ -413,7 +499,13 @@ async function writeCodexConfig(input) {
|
|
|
413
499
|
]
|
|
414
500
|
};
|
|
415
501
|
}
|
|
416
|
-
const merged = mergeCodexTomlHosted(
|
|
502
|
+
const merged = mergeCodexTomlHosted(
|
|
503
|
+
existing ?? "",
|
|
504
|
+
input.hostedMcpUrl,
|
|
505
|
+
input.apiKey,
|
|
506
|
+
resolveSignerLaunchSpec(input),
|
|
507
|
+
serverNamesFor(input.serverName)
|
|
508
|
+
);
|
|
417
509
|
await writeOwnerOnlyText(target, merged);
|
|
418
510
|
return {
|
|
419
511
|
hostedConfigured: true,
|
|
@@ -455,10 +547,15 @@ async function writeOwnerOnlyText(path$1, value) {
|
|
|
455
547
|
await promises.writeFile(path$1, value, { mode: 384 });
|
|
456
548
|
await promises.chmod(path$1, 384).catch(() => void 0);
|
|
457
549
|
}
|
|
458
|
-
function parseJsonObject(value) {
|
|
459
|
-
|
|
550
|
+
function parseJsonObject(value, configPath) {
|
|
551
|
+
let parsed;
|
|
552
|
+
try {
|
|
553
|
+
parsed = JSON.parse(value);
|
|
554
|
+
} catch {
|
|
555
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "it is not valid JSON");
|
|
556
|
+
}
|
|
460
557
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
461
|
-
throw new
|
|
558
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "the top level is not a JSON object");
|
|
462
559
|
}
|
|
463
560
|
return parsed;
|
|
464
561
|
}
|
|
@@ -725,10 +822,11 @@ function configTargetLabel(runtime) {
|
|
|
725
822
|
return "runtime MCP config";
|
|
726
823
|
}
|
|
727
824
|
}
|
|
728
|
-
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError;
|
|
825
|
+
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError, REPAIR_COMMAND_PREFIX, UnreadableRuntimeConfigError;
|
|
729
826
|
var init_config_writers = __esm({
|
|
730
827
|
"src/config-writers.ts"() {
|
|
731
828
|
init_runtime_manifest();
|
|
829
|
+
init_server_names();
|
|
732
830
|
HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
|
|
733
831
|
HermesConfigRecoveryError = class extends Error {
|
|
734
832
|
constructor() {
|
|
@@ -742,8 +840,37 @@ var init_config_writers = __esm({
|
|
|
742
840
|
this.name = "InvalidCodexTomlError";
|
|
743
841
|
}
|
|
744
842
|
};
|
|
843
|
+
REPAIR_COMMAND_PREFIX = "npx @haven_ai/connect@alpha --doctor --repair --runtime";
|
|
844
|
+
UnreadableRuntimeConfigError = class extends Error {
|
|
845
|
+
configPath;
|
|
846
|
+
constructor(configPath, detail) {
|
|
847
|
+
super(`${configPath} is not a config Haven can merge into (${detail})`);
|
|
848
|
+
this.name = "UnreadableRuntimeConfigError";
|
|
849
|
+
this.configPath = configPath;
|
|
850
|
+
}
|
|
851
|
+
};
|
|
745
852
|
}
|
|
746
853
|
});
|
|
854
|
+
async function probeHostedAgentIdentity(apiKey, apiUrl, fetchImpl = fetch) {
|
|
855
|
+
let response;
|
|
856
|
+
try {
|
|
857
|
+
response = await fetchWithTimeout(fetchImpl, `${apiUrl.replace(/\/+$/, "")}/machine-payments/agent`, {
|
|
858
|
+
method: "GET",
|
|
859
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
|
|
860
|
+
});
|
|
861
|
+
} catch {
|
|
862
|
+
return { status: "network_error" };
|
|
863
|
+
}
|
|
864
|
+
if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
|
|
865
|
+
if (!response.ok) return { status: "bad_response" };
|
|
866
|
+
try {
|
|
867
|
+
const payload = JSON.parse(await response.text());
|
|
868
|
+
if (typeof payload?.delegate_address !== "string") return { status: "bad_response" };
|
|
869
|
+
return { status: "ok", agentId: payload.id, delegateAddress: payload.delegate_address };
|
|
870
|
+
} catch {
|
|
871
|
+
return { status: "bad_response" };
|
|
872
|
+
}
|
|
873
|
+
}
|
|
747
874
|
async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
|
|
748
875
|
let response;
|
|
749
876
|
try {
|
|
@@ -781,7 +908,7 @@ async function probeLocalSignerCredential(signerPath) {
|
|
|
781
908
|
}
|
|
782
909
|
}
|
|
783
910
|
async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
|
|
784
|
-
return new Promise((
|
|
911
|
+
return new Promise((resolve9) => {
|
|
785
912
|
const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
786
913
|
let stdout = "";
|
|
787
914
|
let settled = false;
|
|
@@ -793,7 +920,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
793
920
|
settled = true;
|
|
794
921
|
clearTimeout(timeout);
|
|
795
922
|
child.kill();
|
|
796
|
-
|
|
923
|
+
resolve9(result);
|
|
797
924
|
};
|
|
798
925
|
const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
|
|
799
926
|
child.on("error", () => finish({ status: "process_error" }));
|
|
@@ -907,7 +1034,8 @@ async function prepareSignerRuntime(input, deps = {}) {
|
|
|
907
1034
|
wrapperPath,
|
|
908
1035
|
runtimeDirectory,
|
|
909
1036
|
npmCacheDirectory,
|
|
910
|
-
cliPath
|
|
1037
|
+
cliPath,
|
|
1038
|
+
serverName: input.serverName
|
|
911
1039
|
});
|
|
912
1040
|
messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
|
|
913
1041
|
return {
|
|
@@ -1008,6 +1136,7 @@ async function readRuntimeSidecar(credentialDirectory) {
|
|
|
1008
1136
|
}
|
|
1009
1137
|
async function writeRuntimeSidecar(input) {
|
|
1010
1138
|
const value = {
|
|
1139
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1011
1140
|
signer_package: exports.MCP_RUNTIME_MANIFEST.signerPackage,
|
|
1012
1141
|
signer_version: exports.MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1013
1142
|
sdk_package: exports.MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
@@ -1038,6 +1167,26 @@ var init_signer_runtime = __esm({
|
|
|
1038
1167
|
}
|
|
1039
1168
|
});
|
|
1040
1169
|
|
|
1170
|
+
// src/connect-error.ts
|
|
1171
|
+
function isConnectError(err) {
|
|
1172
|
+
return err instanceof exports.ConnectError;
|
|
1173
|
+
}
|
|
1174
|
+
exports.ConnectError = void 0;
|
|
1175
|
+
var init_connect_error = __esm({
|
|
1176
|
+
"src/connect-error.ts"() {
|
|
1177
|
+
exports.ConnectError = class extends Error {
|
|
1178
|
+
code;
|
|
1179
|
+
nextAction;
|
|
1180
|
+
constructor(code, message, nextAction2) {
|
|
1181
|
+
super(message);
|
|
1182
|
+
this.name = "ConnectError";
|
|
1183
|
+
this.code = code;
|
|
1184
|
+
this.nextAction = nextAction2;
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1041
1190
|
// src/runtime-registry.ts
|
|
1042
1191
|
function runtimeProfile(runtime, env = process.env) {
|
|
1043
1192
|
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
@@ -1047,6 +1196,42 @@ function normalizeRuntime(runtime, env = process.env) {
|
|
|
1047
1196
|
if (explicit) return explicit;
|
|
1048
1197
|
return detectRuntime(env) ?? "other";
|
|
1049
1198
|
}
|
|
1199
|
+
async function resolveRuntimeSelection(explicit, force, options = {}) {
|
|
1200
|
+
const env = options.env ?? process.env;
|
|
1201
|
+
if (force !== void 0) {
|
|
1202
|
+
const forced = normalizeRuntimeName(force);
|
|
1203
|
+
if (!forced) {
|
|
1204
|
+
throw new exports.ConnectError(
|
|
1205
|
+
"runtime_force_unrecognized",
|
|
1206
|
+
`Unknown --runtime-force value "${force}". Valid values: ${exports.RUNTIME_FLAG_VALUES}.`,
|
|
1207
|
+
"rerun_connect_with_a_valid_runtime_name"
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
return { runtime: forced, source: "force" };
|
|
1211
|
+
}
|
|
1212
|
+
const detected = detectRuntime(env);
|
|
1213
|
+
const supplied = explicit?.trim() || options.selfReported?.trim() || void 0;
|
|
1214
|
+
const hint = normalizeRuntimeName(supplied);
|
|
1215
|
+
if (supplied && !hint) {
|
|
1216
|
+
if (!detected) {
|
|
1217
|
+
throw new exports.ConnectError(
|
|
1218
|
+
"runtime_unrecognized",
|
|
1219
|
+
`"${supplied}" is not an agent runtime Haven knows. Valid values: ${exports.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.`,
|
|
1220
|
+
"rerun_connect_with_a_valid_runtime_name"
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
return { runtime: detected, source: "detected", discardedHint: supplied };
|
|
1224
|
+
}
|
|
1225
|
+
if (detected && hint && detected !== hint) {
|
|
1226
|
+
return { runtime: detected, source: "detected", overrodeHint: hint };
|
|
1227
|
+
}
|
|
1228
|
+
if (hint) return { runtime: hint, source: "explicit" };
|
|
1229
|
+
if (detected) return { runtime: detected, source: "detected" };
|
|
1230
|
+
if (options.promptForRuntime) {
|
|
1231
|
+
return { runtime: await options.promptForRuntime(), source: "prompted" };
|
|
1232
|
+
}
|
|
1233
|
+
return { runtime: null, source: "none" };
|
|
1234
|
+
}
|
|
1050
1235
|
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
1051
1236
|
const mode = runtimeProfile(runtime, env).restartMode;
|
|
1052
1237
|
return mode === "restart-session" || mode === "restart-app";
|
|
@@ -1067,9 +1252,10 @@ function detectRuntime(env) {
|
|
|
1067
1252
|
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
1068
1253
|
return null;
|
|
1069
1254
|
}
|
|
1070
|
-
var RUNTIME_PROFILES, RUNTIME_ALIASES;
|
|
1255
|
+
var RUNTIME_PROFILES, RUNTIME_ALIASES; exports.RUNTIME_FLAG_VALUES = void 0;
|
|
1071
1256
|
var init_runtime_registry = __esm({
|
|
1072
1257
|
"src/runtime-registry.ts"() {
|
|
1258
|
+
init_connect_error();
|
|
1073
1259
|
RUNTIME_PROFILES = {
|
|
1074
1260
|
"claude-code": {
|
|
1075
1261
|
id: "claude-code",
|
|
@@ -1140,6 +1326,11 @@ var init_runtime_registry = __esm({
|
|
|
1140
1326
|
"claude-code": "claude-code",
|
|
1141
1327
|
claudecode: "claude-code",
|
|
1142
1328
|
"claude_code": "claude-code",
|
|
1329
|
+
// #1682: Cowork runs Claude Code's config, so it resolves to the same
|
|
1330
|
+
// profile. The dashboard's command carries no --runtime for it (it is a
|
|
1331
|
+
// detected, command-path runtime), but an explicit `--runtime cowork` typed
|
|
1332
|
+
// by hand must not fall through to the no-runtime refusal.
|
|
1333
|
+
cowork: "claude-code",
|
|
1143
1334
|
codex: "codex-cli",
|
|
1144
1335
|
"codex-cli": "codex-cli",
|
|
1145
1336
|
codexcli: "codex-cli",
|
|
@@ -1169,9 +1360,18 @@ var init_runtime_registry = __esm({
|
|
|
1169
1360
|
"hermes-agent": "hermes",
|
|
1170
1361
|
hermes_agent: "hermes",
|
|
1171
1362
|
hermesagent: "hermes",
|
|
1363
|
+
// #1682: OpenClaw is a SNIPPET target — the user pastes an mcpServers entry
|
|
1364
|
+
// into ~/.openclaw/openclaw.json and restarts the gateway. That is exactly
|
|
1365
|
+
// the 'other' profile's behaviour (credentials written to disk, no config
|
|
1366
|
+
// auto-written, manual finish), so it resolves there rather than earning a
|
|
1367
|
+
// profile whose only distinguishing feature would be its label.
|
|
1368
|
+
openclaw: "other",
|
|
1369
|
+
"open-claw": "other",
|
|
1370
|
+
"open_claw": "other",
|
|
1172
1371
|
other: "other",
|
|
1173
1372
|
manual: "other"
|
|
1174
1373
|
};
|
|
1374
|
+
exports.RUNTIME_FLAG_VALUES = "claude-code, codex-cli, codex-desktop, cursor, vscode, vscode-insiders, claude-desktop, hermes, other";
|
|
1175
1375
|
}
|
|
1176
1376
|
});
|
|
1177
1377
|
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
@@ -1248,6 +1448,82 @@ var init_signer_consent = __esm({
|
|
|
1248
1448
|
}
|
|
1249
1449
|
});
|
|
1250
1450
|
|
|
1451
|
+
// src/tombstone.ts
|
|
1452
|
+
var tombstone_exports = {};
|
|
1453
|
+
__export(tombstone_exports, {
|
|
1454
|
+
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
1455
|
+
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
1456
|
+
readAgentTombstone: () => readAgentTombstone,
|
|
1457
|
+
writeAgentTombstone: () => writeAgentTombstone
|
|
1458
|
+
});
|
|
1459
|
+
function tombstoneScript(info) {
|
|
1460
|
+
const lines = [
|
|
1461
|
+
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
1462
|
+
"",
|
|
1463
|
+
` agent: ${info.agent_id}`,
|
|
1464
|
+
` retired at: ${info.retired_at}`,
|
|
1465
|
+
` reason: ${info.reason}`,
|
|
1466
|
+
...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
|
|
1467
|
+
"",
|
|
1468
|
+
"This process is running with a wiring snapshot that predates the",
|
|
1469
|
+
"retirement \u2014 it loaded its MCP config at startup and has kept it since.",
|
|
1470
|
+
"Restart THIS host to pick up the current wiring. If several long-lived",
|
|
1471
|
+
"hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
|
|
1472
|
+
"one of them: each holds the snapshot from its own start time, so after",
|
|
1473
|
+
"a chain of recreations each can be parked on a DIFFERENT old agent.",
|
|
1474
|
+
"",
|
|
1475
|
+
"Then verify with: npx @haven_ai/connect@alpha --doctor --runtime <runtime>"
|
|
1476
|
+
];
|
|
1477
|
+
return [
|
|
1478
|
+
"#!/usr/bin/env node",
|
|
1479
|
+
`// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
|
|
1480
|
+
"// once every long-lived MCP host on this machine has been restarted.",
|
|
1481
|
+
`process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
|
|
1482
|
+
"process.exit(1)",
|
|
1483
|
+
""
|
|
1484
|
+
].join("\n");
|
|
1485
|
+
}
|
|
1486
|
+
async function writeAgentTombstone(input) {
|
|
1487
|
+
const dirStat = await promises.stat(input.directory).catch(() => null);
|
|
1488
|
+
if (!dirStat?.isDirectory()) {
|
|
1489
|
+
throw new Error(`Not a directory: ${input.directory} \u2014 nothing to tombstone.`);
|
|
1490
|
+
}
|
|
1491
|
+
const info = {
|
|
1492
|
+
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
1493
|
+
// MCP stderr log on EVERY stale probe, potentially for months — redact
|
|
1494
|
+
// like every other output path, at the write layer so any future caller
|
|
1495
|
+
// inherits it. (#1681 review, finding 1)
|
|
1496
|
+
agent_id: input.agentId,
|
|
1497
|
+
retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1498
|
+
reason: redactSecrets(input.reason),
|
|
1499
|
+
...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
|
|
1500
|
+
};
|
|
1501
|
+
const binDir = path.join(input.directory, "bin");
|
|
1502
|
+
await promises.mkdir(binDir, { recursive: true });
|
|
1503
|
+
const wrapperPath = path.join(binDir, "haven-signer.mjs");
|
|
1504
|
+
await promises.writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
1505
|
+
await promises.chmod(wrapperPath, 493);
|
|
1506
|
+
await promises.writeFile(path.join(input.directory, TOMBSTONE_FILENAME), JSON.stringify(info, null, 2) + "\n", "utf8");
|
|
1507
|
+
return info;
|
|
1508
|
+
}
|
|
1509
|
+
async function readAgentTombstone(directory) {
|
|
1510
|
+
try {
|
|
1511
|
+
const parsed = JSON.parse(await promises.readFile(path.join(directory, TOMBSTONE_FILENAME), "utf8"));
|
|
1512
|
+
if (typeof parsed?.agent_id !== "string") return null;
|
|
1513
|
+
return parsed;
|
|
1514
|
+
} catch {
|
|
1515
|
+
return null;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
1519
|
+
var init_tombstone = __esm({
|
|
1520
|
+
"src/tombstone.ts"() {
|
|
1521
|
+
init_redact();
|
|
1522
|
+
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
1523
|
+
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1251
1527
|
// src/doctor.ts
|
|
1252
1528
|
var doctor_exports = {};
|
|
1253
1529
|
__export(doctor_exports, {
|
|
@@ -1255,129 +1531,101 @@ __export(doctor_exports, {
|
|
|
1255
1531
|
runRepair: () => runRepair
|
|
1256
1532
|
});
|
|
1257
1533
|
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
1258
|
-
|
|
1259
|
-
const root = path.join(homeDir, ".haven", "agents");
|
|
1534
|
+
const root = explicit ? path.dirname(explicit) : path.join(homeDir, ".haven", "agents");
|
|
1260
1535
|
let entries = [];
|
|
1261
1536
|
try {
|
|
1262
1537
|
entries = await promises.readdir(root);
|
|
1263
1538
|
} catch {
|
|
1264
|
-
return {};
|
|
1539
|
+
return explicit ? { directory: explicit, others: [] } : { others: [] };
|
|
1265
1540
|
}
|
|
1266
1541
|
const candidates = [];
|
|
1542
|
+
const tombstonedOnly = [];
|
|
1267
1543
|
for (const entry of entries) {
|
|
1268
1544
|
const directory = path.join(root, entry);
|
|
1269
1545
|
try {
|
|
1270
1546
|
const s = await promises.stat(path.join(directory, "identity.json"));
|
|
1271
1547
|
candidates.push({ directory, mtimeMs: s.mtimeMs });
|
|
1272
1548
|
} catch {
|
|
1549
|
+
try {
|
|
1550
|
+
await promises.stat(path.join(directory, TOMBSTONE_FILENAME));
|
|
1551
|
+
tombstonedOnly.push(directory);
|
|
1552
|
+
} catch {
|
|
1553
|
+
}
|
|
1273
1554
|
}
|
|
1274
1555
|
}
|
|
1275
|
-
if (candidates.length === 0) return {};
|
|
1276
1556
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1557
|
+
if (explicit) {
|
|
1558
|
+
return {
|
|
1559
|
+
directory: explicit,
|
|
1560
|
+
others: [...candidates.map((c) => c.directory), ...tombstonedOnly].filter((d) => d !== explicit)
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
if (candidates.length === 0 && tombstonedOnly.length === 0) return { others: [] };
|
|
1277
1564
|
return {
|
|
1278
|
-
directory: candidates[0]
|
|
1279
|
-
|
|
1565
|
+
directory: candidates[0]?.directory,
|
|
1566
|
+
others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly]
|
|
1280
1567
|
};
|
|
1281
1568
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1569
|
+
function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
|
|
1570
|
+
if (configText === null) return isPrimary;
|
|
1571
|
+
if (slug) {
|
|
1572
|
+
for (const name of [names.hosted, names.codexHosted, names.signer, names.codexSigner]) {
|
|
1573
|
+
if (new RegExp(`(^|[."'\\s\\[])${name}(["'\\]:\\s]|$)`, "m").test(configText)) return true;
|
|
1574
|
+
}
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1577
|
+
if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
|
|
1578
|
+
if (bareOwnerExists) return false;
|
|
1579
|
+
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
1580
|
+
}
|
|
1581
|
+
async function readIdentity(directory) {
|
|
1582
|
+
try {
|
|
1583
|
+
return JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
1584
|
+
} catch {
|
|
1585
|
+
return void 0;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
async function checksForAgent(entry, input, deps) {
|
|
1589
|
+
const { directory, identity, sidecar } = entry;
|
|
1284
1590
|
const checks = [];
|
|
1285
1591
|
let signerCapabilities;
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1592
|
+
let signerFile;
|
|
1593
|
+
try {
|
|
1594
|
+
const parsed = JSON.parse(await promises.readFile(path.join(directory, "signer.json"), "utf8"));
|
|
1595
|
+
signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
1596
|
+
} catch {
|
|
1597
|
+
signerFile = void 0;
|
|
1598
|
+
}
|
|
1599
|
+
const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
|
|
1600
|
+
checks.push({
|
|
1601
|
+
id: "credentials",
|
|
1602
|
+
label: "Agent credentials",
|
|
1603
|
+
ok: credentialsOk,
|
|
1604
|
+
detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})` : "identity.json or signer.json is missing or unparseable.",
|
|
1605
|
+
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
1606
|
+
});
|
|
1607
|
+
if (!sidecar) {
|
|
1290
1608
|
checks.push({
|
|
1291
|
-
id: "
|
|
1292
|
-
label: "
|
|
1609
|
+
id: "signer_runtime",
|
|
1610
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
1293
1611
|
ok: false,
|
|
1294
|
-
detail: "No
|
|
1295
|
-
repair: `Run
|
|
1612
|
+
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
1613
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1296
1614
|
});
|
|
1297
1615
|
} else {
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
identity = void 0;
|
|
1302
|
-
}
|
|
1303
|
-
try {
|
|
1304
|
-
const signer = JSON.parse(await promises.readFile(path.join(directory, "signer.json"), "utf8"));
|
|
1305
|
-
signerParses = typeof signer === "object" && signer !== null;
|
|
1306
|
-
} catch {
|
|
1307
|
-
signerParses = false;
|
|
1308
|
-
}
|
|
1309
|
-
const ok = Boolean(identity?.api_key) && signerParses;
|
|
1616
|
+
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
1617
|
+
const versionOk = sidecar.signer_version === exports.MCP_RUNTIME_MANIFEST.signerVersion;
|
|
1618
|
+
const ok = matches && versionOk;
|
|
1310
1619
|
checks.push({
|
|
1311
|
-
id: "
|
|
1312
|
-
label: "
|
|
1620
|
+
id: "signer_runtime",
|
|
1621
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
1313
1622
|
ok,
|
|
1314
|
-
detail: ok ? `
|
|
1315
|
-
...ok ? {} : { repair: `
|
|
1623
|
+
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 ${exports.MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
1624
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1316
1625
|
});
|
|
1317
1626
|
}
|
|
1318
|
-
|
|
1319
|
-
if (
|
|
1320
|
-
sidecar = await readRuntimeSidecar(directory);
|
|
1321
|
-
if (!sidecar) {
|
|
1322
|
-
checks.push({
|
|
1323
|
-
id: "signer_runtime",
|
|
1324
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
1325
|
-
ok: false,
|
|
1326
|
-
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
1327
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1328
|
-
});
|
|
1329
|
-
} else {
|
|
1330
|
-
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
1331
|
-
const versionOk = sidecar.signer_version === exports.MCP_RUNTIME_MANIFEST.signerVersion;
|
|
1332
|
-
const ok = matches && versionOk;
|
|
1333
|
-
checks.push({
|
|
1334
|
-
id: "signer_runtime",
|
|
1335
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
1336
|
-
ok,
|
|
1337
|
-
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 ${exports.MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
1338
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1339
|
-
});
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1343
|
-
if (configPath === null) {
|
|
1344
|
-
checks.push({
|
|
1345
|
-
id: "runtime_config",
|
|
1346
|
-
label: "Runtime MCP config",
|
|
1347
|
-
ok: true,
|
|
1348
|
-
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1349
|
-
});
|
|
1350
|
-
} else {
|
|
1351
|
-
let configText = null;
|
|
1352
|
-
try {
|
|
1353
|
-
configText = await promises.readFile(configPath, "utf8");
|
|
1354
|
-
} catch {
|
|
1355
|
-
configText = null;
|
|
1356
|
-
}
|
|
1357
|
-
if (configText === null) {
|
|
1358
|
-
checks.push({
|
|
1359
|
-
id: "runtime_config",
|
|
1360
|
-
label: "Runtime MCP config",
|
|
1361
|
-
ok: false,
|
|
1362
|
-
detail: `No runtime config at ${configPath}.`,
|
|
1363
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1364
|
-
});
|
|
1365
|
-
} else {
|
|
1366
|
-
const hasHaven = identity?.hosted_mcp_url ? configText.includes(identity.hosted_mcp_url) : configText.includes("haven");
|
|
1367
|
-
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1368
|
-
const wrapperReferenced = sidecar ? configText.includes(sidecar.wrapper_path) : false;
|
|
1369
|
-
const ok = hasHaven && !signerViaNpx && (sidecar ? wrapperReferenced : true);
|
|
1370
|
-
checks.push({
|
|
1371
|
-
id: "runtime_config",
|
|
1372
|
-
label: "Runtime MCP config",
|
|
1373
|
-
ok,
|
|
1374
|
-
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)" : ""}.`,
|
|
1375
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
if (identity?.api_key && (identity.hosted_mcp_url || identity.api_url)) {
|
|
1380
|
-
const hostedUrl = identity.hosted_mcp_url ?? `${identity.api_url}/mcp`;
|
|
1627
|
+
const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
1628
|
+
if (identity?.api_key && hostedUrl) {
|
|
1381
1629
|
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
1382
1630
|
checks.push({
|
|
1383
1631
|
id: "hosted_mcp",
|
|
@@ -1397,7 +1645,43 @@ async function runDoctor(input, deps = {}) {
|
|
|
1397
1645
|
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
1398
1646
|
});
|
|
1399
1647
|
}
|
|
1400
|
-
|
|
1648
|
+
const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
|
|
1649
|
+
if (identity?.api_key && identity.api_url) {
|
|
1650
|
+
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
1651
|
+
identity.api_key,
|
|
1652
|
+
identity.api_url,
|
|
1653
|
+
deps.fetch
|
|
1654
|
+
);
|
|
1655
|
+
if (probe.status !== "ok") {
|
|
1656
|
+
checks.push({
|
|
1657
|
+
id: "identity_match",
|
|
1658
|
+
label: "Hosted identity matches the local signing key",
|
|
1659
|
+
ok: false,
|
|
1660
|
+
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.`,
|
|
1661
|
+
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}`
|
|
1662
|
+
});
|
|
1663
|
+
} else if (!localDelegate) {
|
|
1664
|
+
checks.push({
|
|
1665
|
+
id: "identity_match",
|
|
1666
|
+
label: "Hosted identity matches the local signing key",
|
|
1667
|
+
ok: false,
|
|
1668
|
+
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
1669
|
+
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
1670
|
+
});
|
|
1671
|
+
} else {
|
|
1672
|
+
const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
|
|
1673
|
+
checks.push({
|
|
1674
|
+
id: "identity_match",
|
|
1675
|
+
label: "Hosted identity matches the local signing key",
|
|
1676
|
+
ok: same,
|
|
1677
|
+
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.`,
|
|
1678
|
+
...same ? {} : {
|
|
1679
|
+
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.`
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (sidecar) {
|
|
1401
1685
|
const consent = await getLocalSignerConsentStatus(path.join(directory, "signer.json"));
|
|
1402
1686
|
if (!consent.acknowledged) {
|
|
1403
1687
|
checks.push({
|
|
@@ -1425,7 +1709,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
1425
1709
|
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1426
1710
|
});
|
|
1427
1711
|
}
|
|
1428
|
-
} else
|
|
1712
|
+
} else {
|
|
1429
1713
|
checks.push({
|
|
1430
1714
|
id: "signer_process",
|
|
1431
1715
|
label: "Signer stdio handshake",
|
|
@@ -1434,6 +1718,171 @@ async function runDoctor(input, deps = {}) {
|
|
|
1434
1718
|
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1435
1719
|
});
|
|
1436
1720
|
}
|
|
1721
|
+
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
1722
|
+
}
|
|
1723
|
+
async function runDoctor(input, deps = {}) {
|
|
1724
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
1725
|
+
const checks = [];
|
|
1726
|
+
let signerCapabilities;
|
|
1727
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1728
|
+
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1729
|
+
let configText = null;
|
|
1730
|
+
if (configPath !== null) {
|
|
1731
|
+
try {
|
|
1732
|
+
configText = await promises.readFile(configPath, "utf8");
|
|
1733
|
+
} catch {
|
|
1734
|
+
configText = null;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const allDirectories = directory ? [directory, ...others] : others;
|
|
1738
|
+
let bareOwnerExists = false;
|
|
1739
|
+
for (const dir of allDirectories) {
|
|
1740
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
1741
|
+
if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
|
|
1742
|
+
bareOwnerExists = true;
|
|
1743
|
+
break;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
const inventory = [];
|
|
1747
|
+
const capabilitiesByDirectory = /* @__PURE__ */ new Map();
|
|
1748
|
+
const primaryChecksById = /* @__PURE__ */ new Map();
|
|
1749
|
+
for (const dir of allDirectories) {
|
|
1750
|
+
const identity = await readIdentity(dir);
|
|
1751
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
1752
|
+
const tombstone = await readAgentTombstone(dir);
|
|
1753
|
+
const slug = sidecar?.server_name;
|
|
1754
|
+
const names = serverNamesFor(slug);
|
|
1755
|
+
if (!identity?.api_key) {
|
|
1756
|
+
inventory.push({
|
|
1757
|
+
...slug ? { slug } : {},
|
|
1758
|
+
...tombstone?.agent_id ? { agentId: tombstone.agent_id } : {},
|
|
1759
|
+
directory: dir,
|
|
1760
|
+
classification: tombstone ? "retired" : "orphaned",
|
|
1761
|
+
checks: []
|
|
1762
|
+
});
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
|
|
1766
|
+
const entry = {
|
|
1767
|
+
...slug ? { slug } : {},
|
|
1768
|
+
...identity.agent_id ? { agentId: identity.agent_id } : {},
|
|
1769
|
+
directory: dir,
|
|
1770
|
+
classification: wired ? "wired" : "superseded",
|
|
1771
|
+
checks: []
|
|
1772
|
+
};
|
|
1773
|
+
if (wired) {
|
|
1774
|
+
const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
|
|
1775
|
+
entry.checks = result.checks;
|
|
1776
|
+
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
1777
|
+
}
|
|
1778
|
+
inventory.push(entry);
|
|
1779
|
+
}
|
|
1780
|
+
const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
|
|
1781
|
+
const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
|
|
1782
|
+
if (primaryDirectory) {
|
|
1783
|
+
const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
|
|
1784
|
+
signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
|
|
1785
|
+
for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
|
|
1786
|
+
}
|
|
1787
|
+
if (!primaryDirectory) {
|
|
1788
|
+
checks.push({
|
|
1789
|
+
id: "credentials",
|
|
1790
|
+
label: "Agent credentials",
|
|
1791
|
+
ok: false,
|
|
1792
|
+
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
1793
|
+
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
1794
|
+
});
|
|
1795
|
+
} else {
|
|
1796
|
+
const primaryIdentity = await readIdentity(primaryDirectory);
|
|
1797
|
+
const primarySidecar = await readRuntimeSidecar(primaryDirectory);
|
|
1798
|
+
if (primaryChecksById.size === 0) {
|
|
1799
|
+
const result = await checksForAgent(
|
|
1800
|
+
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
1801
|
+
input,
|
|
1802
|
+
deps
|
|
1803
|
+
);
|
|
1804
|
+
signerCapabilities = result.signerCapabilities;
|
|
1805
|
+
for (const check of result.checks) primaryChecksById.set(check.id, check);
|
|
1806
|
+
}
|
|
1807
|
+
for (const id of ["credentials", "signer_runtime"]) {
|
|
1808
|
+
const check = primaryChecksById.get(id);
|
|
1809
|
+
if (check) checks.push(check);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (configPath === null) {
|
|
1813
|
+
checks.push({
|
|
1814
|
+
id: "runtime_config",
|
|
1815
|
+
label: "Runtime MCP config",
|
|
1816
|
+
ok: true,
|
|
1817
|
+
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1818
|
+
});
|
|
1819
|
+
} else if (configText === null) {
|
|
1820
|
+
checks.push({
|
|
1821
|
+
id: "runtime_config",
|
|
1822
|
+
label: "Runtime MCP config",
|
|
1823
|
+
ok: false,
|
|
1824
|
+
detail: `No runtime config at ${configPath}.`,
|
|
1825
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1826
|
+
});
|
|
1827
|
+
} else {
|
|
1828
|
+
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
1829
|
+
const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
|
|
1830
|
+
const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
|
|
1831
|
+
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1832
|
+
const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
|
|
1833
|
+
const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
|
|
1834
|
+
checks.push({
|
|
1835
|
+
id: "runtime_config",
|
|
1836
|
+
label: "Runtime MCP config",
|
|
1837
|
+
ok,
|
|
1838
|
+
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)" : ""}.`,
|
|
1839
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
for (const id of ["hosted_mcp", "identity_match"]) {
|
|
1843
|
+
const check = primaryChecksById.get(id);
|
|
1844
|
+
if (check) checks.push(check);
|
|
1845
|
+
}
|
|
1846
|
+
const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
|
|
1847
|
+
if (otherEntries.length > 0) {
|
|
1848
|
+
const live = [];
|
|
1849
|
+
const revoked = [];
|
|
1850
|
+
const unverifiable = [];
|
|
1851
|
+
const retired = [];
|
|
1852
|
+
for (const entry of otherEntries) {
|
|
1853
|
+
const identity = await readIdentity(entry.directory);
|
|
1854
|
+
const tombstone = await readAgentTombstone(entry.directory);
|
|
1855
|
+
const otherAgent = identity?.agent_id ?? tombstone?.agent_id ?? path.basename(entry.directory);
|
|
1856
|
+
const otherUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
1857
|
+
if (!identity?.api_key || !otherUrl) {
|
|
1858
|
+
if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
|
|
1859
|
+
else unverifiable.push(`${otherAgent} (no stored key/URL to probe)`);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
|
|
1863
|
+
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, otherUrl, deps.fetch);
|
|
1864
|
+
if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
|
|
1865
|
+
else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
|
|
1866
|
+
else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
|
|
1867
|
+
}
|
|
1868
|
+
const parts = [];
|
|
1869
|
+
if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
|
|
1870
|
+
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
1871
|
+
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
1872
|
+
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
1873
|
+
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
1874
|
+
checks.push({
|
|
1875
|
+
id: "superseded_agents",
|
|
1876
|
+
label: "Superseded agent credentials",
|
|
1877
|
+
ok: supersededLive.length === 0,
|
|
1878
|
+
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("; ")}.`,
|
|
1879
|
+
...supersededLive.length > 0 ? {
|
|
1880
|
+
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.`
|
|
1881
|
+
} : {}
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
const signerProcess = primaryChecksById.get("signer_process");
|
|
1885
|
+
if (signerProcess) checks.push(signerProcess);
|
|
1437
1886
|
const restart = restartRequiredForRuntime(input.runtime, deps.env);
|
|
1438
1887
|
checks.push({
|
|
1439
1888
|
id: "restart",
|
|
@@ -1441,20 +1890,24 @@ async function runDoctor(input, deps = {}) {
|
|
|
1441
1890
|
ok: true,
|
|
1442
1891
|
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."
|
|
1443
1892
|
});
|
|
1893
|
+
const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
|
|
1444
1894
|
return {
|
|
1445
1895
|
version: 1,
|
|
1446
|
-
ok: checks.every((check) => check.ok),
|
|
1896
|
+
ok: checks.every((check) => check.ok) && wiredOk,
|
|
1447
1897
|
runtime: input.runtime,
|
|
1448
|
-
credentialDirectory:
|
|
1898
|
+
credentialDirectory: primaryDirectory,
|
|
1449
1899
|
checks,
|
|
1900
|
+
agents: inventory,
|
|
1450
1901
|
...signerCapabilities ? { signerCapabilities } : {}
|
|
1451
1902
|
};
|
|
1452
1903
|
}
|
|
1453
1904
|
async function runRepair(input, deps = {}) {
|
|
1454
1905
|
const homeDir = deps.homeDir ?? os.homedir();
|
|
1455
1906
|
const messages = [];
|
|
1456
|
-
const { directory,
|
|
1457
|
-
if (
|
|
1907
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1908
|
+
if (others.length > 0) {
|
|
1909
|
+
messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
|
|
1910
|
+
}
|
|
1458
1911
|
if (!directory) {
|
|
1459
1912
|
return {
|
|
1460
1913
|
ok: false,
|
|
@@ -1516,6 +1969,9 @@ var init_doctor = __esm({
|
|
|
1516
1969
|
init_config_writers();
|
|
1517
1970
|
init_runtime_registry();
|
|
1518
1971
|
init_signer_consent();
|
|
1972
|
+
init_tombstone();
|
|
1973
|
+
init_server_names();
|
|
1974
|
+
init_redact();
|
|
1519
1975
|
RERUN = "npx @haven_ai/connect@alpha";
|
|
1520
1976
|
}
|
|
1521
1977
|
});
|
|
@@ -1625,16 +2081,9 @@ function agentApiKeyPrefix(apiKey) {
|
|
|
1625
2081
|
return apiKey.slice(0, 12);
|
|
1626
2082
|
}
|
|
1627
2083
|
|
|
1628
|
-
// src/
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
function redactSecrets(value) {
|
|
1632
|
-
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
1633
|
-
}
|
|
1634
|
-
function shortAddress(address) {
|
|
1635
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
1636
|
-
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1637
|
-
}
|
|
2084
|
+
// src/runtime.ts
|
|
2085
|
+
init_redact();
|
|
2086
|
+
init_server_names();
|
|
1638
2087
|
async function preflightCredentialStorage(input = {}) {
|
|
1639
2088
|
const directory = defaultCredentialRoot(input.baseDir);
|
|
1640
2089
|
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
@@ -1648,7 +2097,7 @@ async function preflightCredentialStorage(input = {}) {
|
|
|
1648
2097
|
return directory;
|
|
1649
2098
|
}
|
|
1650
2099
|
async function writeCredentialFiles(input) {
|
|
1651
|
-
const directory = defaultAgentDirectory(input.agentId, input.baseDir);
|
|
2100
|
+
const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
|
|
1652
2101
|
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
1653
2102
|
await restrictPermissions(directory, 448, input.warn);
|
|
1654
2103
|
const identityPath = path.join(directory, "identity.json");
|
|
@@ -1713,6 +2162,17 @@ async function writeCredentialFiles(input) {
|
|
|
1713
2162
|
}
|
|
1714
2163
|
return { directory, identityPath, signerPath, agentPath };
|
|
1715
2164
|
}
|
|
2165
|
+
async function assertServerSlugAvailable(serverName, baseDir) {
|
|
2166
|
+
const directory = defaultAgentDirectory(serverName, baseDir);
|
|
2167
|
+
try {
|
|
2168
|
+
await promises.stat(path.join(directory, "identity.json"));
|
|
2169
|
+
} catch {
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
throw new Error(
|
|
2173
|
+
`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.`
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
1716
2176
|
function defaultAgentDirectory(agentId, baseDir = path.join(os.homedir(), ".haven", "agents")) {
|
|
1717
2177
|
return path.resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
1718
2178
|
}
|
|
@@ -1752,6 +2212,7 @@ async function restrictPermissions(path, mode, warn) {
|
|
|
1752
2212
|
|
|
1753
2213
|
// src/runtime-install.ts
|
|
1754
2214
|
init_config_writers();
|
|
2215
|
+
init_server_names();
|
|
1755
2216
|
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
1756
2217
|
try {
|
|
1757
2218
|
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
@@ -1877,7 +2338,8 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
|
1877
2338
|
wrapperPath,
|
|
1878
2339
|
runtimeDirectory,
|
|
1879
2340
|
npmCacheDirectory,
|
|
1880
|
-
cliPath
|
|
2341
|
+
cliPath,
|
|
2342
|
+
serverName: input.serverName
|
|
1881
2343
|
});
|
|
1882
2344
|
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1883
2345
|
return {
|
|
@@ -1973,6 +2435,7 @@ async function writeWrapper2(input) {
|
|
|
1973
2435
|
}
|
|
1974
2436
|
async function writeRuntimeSidecar2(input) {
|
|
1975
2437
|
const value = {
|
|
2438
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1976
2439
|
mcp_package: exports.MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
1977
2440
|
mcp_version: exports.MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
1978
2441
|
sdk_package: exports.MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
@@ -2206,12 +2669,13 @@ async function installRuntime(input, deps = {}) {
|
|
|
2206
2669
|
}
|
|
2207
2670
|
const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
|
|
2208
2671
|
progress("Setting up your Haven tools\u2026");
|
|
2209
|
-
const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await configureClaudeCodeHosted(deps, input, signerCommand) : await writeRuntimeConfig({
|
|
2672
|
+
const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "", input.serverName) : await configureClaudeCodeHosted(deps, input, signerCommand) : await writeRuntimeConfig({
|
|
2210
2673
|
runtime,
|
|
2211
2674
|
hostedMcpUrl: input.hostedMcpUrl,
|
|
2212
2675
|
apiKey: input.apiKey,
|
|
2213
2676
|
identityPath: input.identityPath,
|
|
2214
2677
|
signerPath: input.signerPath,
|
|
2678
|
+
serverName: input.serverName,
|
|
2215
2679
|
credentialDirectory: input.credentialDirectory,
|
|
2216
2680
|
localMcpCommand: localRuntimeInstall?.command,
|
|
2217
2681
|
signerCommand,
|
|
@@ -2291,7 +2755,7 @@ function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
|
2291
2755
|
restartRequired: restartRequiredForRuntime(runtime, env)
|
|
2292
2756
|
};
|
|
2293
2757
|
}
|
|
2294
|
-
async function configureClaudeCode(deps, localMcpCommand) {
|
|
2758
|
+
async function configureClaudeCode(deps, localMcpCommand, serverName) {
|
|
2295
2759
|
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
2296
2760
|
const serverJson = JSON.stringify({
|
|
2297
2761
|
type: "stdio",
|
|
@@ -2301,12 +2765,13 @@ async function configureClaudeCode(deps, localMcpCommand) {
|
|
|
2301
2765
|
});
|
|
2302
2766
|
try {
|
|
2303
2767
|
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
2304
|
-
|
|
2305
|
-
await runCommand("claude", ["mcp", "remove",
|
|
2306
|
-
await runCommand("claude", ["mcp", "
|
|
2307
|
-
|
|
2768
|
+
const names = serverNamesFor(serverName);
|
|
2769
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2770
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2771
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, serverJson, "--scope", "user"]).catch(async () => {
|
|
2772
|
+
await runCommand("claude", ["mcp", "add", names.hosted, "--scope", "user", "--", localMcpCommand]);
|
|
2308
2773
|
});
|
|
2309
|
-
const verified = await runCommand("claude", ["mcp", "get",
|
|
2774
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2310
2775
|
return {
|
|
2311
2776
|
hostedConfigured: false,
|
|
2312
2777
|
signerConfigured: true,
|
|
@@ -2351,11 +2816,12 @@ async function configureClaudeCodeHosted(deps, input, signerCommand) {
|
|
|
2351
2816
|
env: {}
|
|
2352
2817
|
});
|
|
2353
2818
|
try {
|
|
2354
|
-
|
|
2355
|
-
await runCommand("claude", ["mcp", "remove",
|
|
2356
|
-
await runCommand("claude", ["mcp", "
|
|
2357
|
-
await runCommand("claude", ["mcp", "add-json",
|
|
2358
|
-
|
|
2819
|
+
const names = serverNamesFor(input.serverName);
|
|
2820
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2821
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2822
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, hostedJson, "--scope", "user"]);
|
|
2823
|
+
await runCommand("claude", ["mcp", "add-json", names.signer, signerJson, "--scope", "user"]);
|
|
2824
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2359
2825
|
return {
|
|
2360
2826
|
hostedConfigured: true,
|
|
2361
2827
|
signerConfigured: true,
|
|
@@ -2459,7 +2925,8 @@ async function prepareRuntimeForLocalMcp(input, deps) {
|
|
|
2459
2925
|
credentialDirectory: input.credentialDirectory,
|
|
2460
2926
|
identityPath: input.identityPath,
|
|
2461
2927
|
signerPath: input.signerPath,
|
|
2462
|
-
homeDir: deps.homeDir
|
|
2928
|
+
homeDir: deps.homeDir,
|
|
2929
|
+
serverName: input.serverName
|
|
2463
2930
|
});
|
|
2464
2931
|
}
|
|
2465
2932
|
async function prepareSignerForRuntime(input, deps) {
|
|
@@ -2473,7 +2940,8 @@ async function prepareSignerForRuntime(input, deps) {
|
|
|
2473
2940
|
return prepare({
|
|
2474
2941
|
credentialDirectory: input.credentialDirectory,
|
|
2475
2942
|
signerPath: input.signerPath,
|
|
2476
|
-
homeDir: deps.homeDir
|
|
2943
|
+
homeDir: deps.homeDir,
|
|
2944
|
+
serverName: input.serverName
|
|
2477
2945
|
});
|
|
2478
2946
|
}
|
|
2479
2947
|
async function runLocalMcpProbe(runtimeInstall, deps) {
|
|
@@ -2493,8 +2961,179 @@ function localRuntimePrepareErrorCode(err) {
|
|
|
2493
2961
|
|
|
2494
2962
|
// src/runtime.ts
|
|
2495
2963
|
init_runtime_registry();
|
|
2964
|
+
init_connect_error();
|
|
2965
|
+
|
|
2966
|
+
// src/installed-clients.ts
|
|
2967
|
+
init_connect_error();
|
|
2968
|
+
init_config_writers();
|
|
2969
|
+
init_runtime_registry();
|
|
2970
|
+
var SCAN_ORDER = [
|
|
2971
|
+
"claude-code",
|
|
2972
|
+
"codex-cli",
|
|
2973
|
+
"cursor",
|
|
2974
|
+
"vscode",
|
|
2975
|
+
"vscode-insiders",
|
|
2976
|
+
"claude-desktop",
|
|
2977
|
+
"hermes"
|
|
2978
|
+
];
|
|
2979
|
+
function installedClientTargets(homeDir = os.homedir(), cwd = process.cwd(), env = process.env) {
|
|
2980
|
+
const targets = [
|
|
2981
|
+
{
|
|
2982
|
+
runtime: "claude-code",
|
|
2983
|
+
label: "Claude Code",
|
|
2984
|
+
// Claude Code is configured through its own CLI (`claude mcp add-json`),
|
|
2985
|
+
// not by writing a file this module owns — so its evidence is the
|
|
2986
|
+
// client directory, never a config path.
|
|
2987
|
+
configPath: null,
|
|
2988
|
+
markers: [path.join(homeDir, ".claude"), path.join(homeDir, ".claude.json")]
|
|
2989
|
+
},
|
|
2990
|
+
{
|
|
2991
|
+
runtime: "codex-cli",
|
|
2992
|
+
// Both Codex surfaces write the same ~/.codex/config.toml, so they are
|
|
2993
|
+
// ONE candidate. Splitting them would ask the user to answer a question
|
|
2994
|
+
// whose answers are the same write.
|
|
2995
|
+
label: "Codex (CLI or Desktop)",
|
|
2996
|
+
configPath: runtimeConfigPathFor("codex-cli", homeDir),
|
|
2997
|
+
markers: [path.join(homeDir, ".codex")]
|
|
2998
|
+
},
|
|
2999
|
+
{
|
|
3000
|
+
runtime: "cursor",
|
|
3001
|
+
label: "Cursor",
|
|
3002
|
+
configPath: runtimeConfigPathFor("cursor", homeDir),
|
|
3003
|
+
markers: [path.join(homeDir, ".cursor")]
|
|
3004
|
+
},
|
|
3005
|
+
{
|
|
3006
|
+
runtime: "vscode",
|
|
3007
|
+
label: "VS Code",
|
|
3008
|
+
configPath: runtimeConfigPathFor("vscode", homeDir),
|
|
3009
|
+
markers: [path.resolve(cwd, ".vscode")]
|
|
3010
|
+
},
|
|
3011
|
+
{
|
|
3012
|
+
runtime: "vscode-insiders",
|
|
3013
|
+
label: "VS Code Insiders",
|
|
3014
|
+
configPath: runtimeConfigPathFor("vscode-insiders", homeDir),
|
|
3015
|
+
markers: []
|
|
3016
|
+
},
|
|
3017
|
+
{
|
|
3018
|
+
runtime: "claude-desktop",
|
|
3019
|
+
label: "Claude Desktop",
|
|
3020
|
+
configPath: runtimeConfigPathFor("claude-desktop", homeDir),
|
|
3021
|
+
markers: []
|
|
3022
|
+
},
|
|
3023
|
+
{
|
|
3024
|
+
runtime: "hermes",
|
|
3025
|
+
label: "Hermes Agent",
|
|
3026
|
+
configPath: runtimeConfigPathFor("hermes", homeDir),
|
|
3027
|
+
markers: [env.HERMES_HOME ?? path.join(homeDir, ".hermes")]
|
|
3028
|
+
}
|
|
3029
|
+
];
|
|
3030
|
+
return targets.filter((target) => runtimeProfile(target.runtime, {}).canWriteRuntimeConfig);
|
|
3031
|
+
}
|
|
3032
|
+
async function scanInstalledClients(options = {}) {
|
|
3033
|
+
const exists = options.exists ?? pathExists;
|
|
3034
|
+
const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
|
|
3035
|
+
const found = [];
|
|
3036
|
+
for (const target of targets) {
|
|
3037
|
+
if (target.configPath && await exists(target.configPath)) {
|
|
3038
|
+
found.push({
|
|
3039
|
+
runtime: target.runtime,
|
|
3040
|
+
label: target.label,
|
|
3041
|
+
detail: `MCP config found at ${target.configPath}`,
|
|
3042
|
+
configPath: target.configPath,
|
|
3043
|
+
evidence: "config-file"
|
|
3044
|
+
});
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
for (const marker of target.markers) {
|
|
3048
|
+
if (!await exists(marker)) continue;
|
|
3049
|
+
found.push({
|
|
3050
|
+
runtime: target.runtime,
|
|
3051
|
+
label: target.label,
|
|
3052
|
+
detail: `installed (${marker})`,
|
|
3053
|
+
configPath: target.configPath,
|
|
3054
|
+
evidence: "client-directory"
|
|
3055
|
+
});
|
|
3056
|
+
break;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
return found.sort((a, b) => {
|
|
3060
|
+
if (a.evidence !== b.evidence) return a.evidence === "config-file" ? -1 : 1;
|
|
3061
|
+
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
var MAX_PROMPT_ATTEMPTS = 3;
|
|
3065
|
+
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3066
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3067
|
+
io.write("Haven could not detect which agent runtime this is.\n");
|
|
3068
|
+
io.write("These agent clients are installed on this machine:\n");
|
|
3069
|
+
candidates.forEach((candidate, index) => {
|
|
3070
|
+
io.write(` ${index + 1}) ${candidate.label} \u2014 ${candidate.detail}
|
|
3071
|
+
`);
|
|
3072
|
+
});
|
|
3073
|
+
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");
|
|
3074
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
3075
|
+
const answer = await io.question(`Which one? [1-${candidates.length}] (default 1 \u2014 ${candidates[0].label}): `);
|
|
3076
|
+
if (answer === null) throw promptAbortedError("the prompt was cancelled");
|
|
3077
|
+
const trimmed = answer.trim();
|
|
3078
|
+
if (trimmed === "") return candidates[0].runtime;
|
|
3079
|
+
const picked = Number.parseInt(trimmed, 10);
|
|
3080
|
+
if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
|
|
3081
|
+
return candidates[picked - 1].runtime;
|
|
3082
|
+
}
|
|
3083
|
+
io.write(`"${trimmed}" is not one of 1-${candidates.length}.
|
|
3084
|
+
`);
|
|
3085
|
+
}
|
|
3086
|
+
throw promptAbortedError(`no valid choice after ${MAX_PROMPT_ATTEMPTS} attempts`);
|
|
3087
|
+
}
|
|
3088
|
+
async function resolveRuntimeByInstalledClientPrompt(options = {}) {
|
|
3089
|
+
const candidates = await scanInstalledClients(options);
|
|
3090
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3091
|
+
return promptForInstalledClient(candidates, options.io ?? defaultPromptIo());
|
|
3092
|
+
}
|
|
3093
|
+
function noInstalledClientsError() {
|
|
3094
|
+
return new exports.ConnectError(
|
|
3095
|
+
"runtime_no_installed_clients",
|
|
3096
|
+
"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.",
|
|
3097
|
+
"rerun_connect_with_explicit_runtime"
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
function promptAbortedError(reason) {
|
|
3101
|
+
return new exports.ConnectError(
|
|
3102
|
+
"runtime_prompt_aborted",
|
|
3103
|
+
`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.`,
|
|
3104
|
+
"rerun_connect_and_choose_a_runtime"
|
|
3105
|
+
);
|
|
3106
|
+
}
|
|
3107
|
+
async function pathExists(path) {
|
|
3108
|
+
try {
|
|
3109
|
+
await promises.access(path);
|
|
3110
|
+
return true;
|
|
3111
|
+
} catch {
|
|
3112
|
+
return false;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
function defaultPromptIo() {
|
|
3116
|
+
return {
|
|
3117
|
+
write: (text) => process.stdout.write(text),
|
|
3118
|
+
question: (query) => new Promise((resolvePromise) => {
|
|
3119
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3120
|
+
let settled = false;
|
|
3121
|
+
const settle = (value) => {
|
|
3122
|
+
if (settled) return;
|
|
3123
|
+
settled = true;
|
|
3124
|
+
rl.close();
|
|
3125
|
+
resolvePromise(value);
|
|
3126
|
+
};
|
|
3127
|
+
rl.once("SIGINT", () => settle(null));
|
|
3128
|
+
rl.once("close", () => settle(null));
|
|
3129
|
+
rl.question(query, (answer) => settle(answer));
|
|
3130
|
+
})
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
// src/runtime.ts
|
|
2496
3135
|
init_runtime_manifest();
|
|
2497
|
-
var CONNECTOR_VERSION = "0.1.
|
|
3136
|
+
var CONNECTOR_VERSION = "0.1.29-alpha.0";
|
|
2498
3137
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
2499
3138
|
async function runConnect(options, deps = {}) {
|
|
2500
3139
|
assertSupportedNodeVersion(deps.nodeVersion, exports.MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
@@ -2510,24 +3149,49 @@ async function runConnect(options, deps = {}) {
|
|
|
2510
3149
|
const runRuntimeInstall = deps.installRuntime ?? installRuntime;
|
|
2511
3150
|
const generateKey = deps.generateKey ?? generateDelegateKey;
|
|
2512
3151
|
const generateLocalApiKey = deps.generateApiKey ?? generateAgentApiKey;
|
|
2513
|
-
const
|
|
3152
|
+
const selection = await resolveRuntimeSelection(options.runtime, options.runtimeForce, {
|
|
3153
|
+
env: deps.env ?? process.env,
|
|
3154
|
+
selfReported: options.runtimeSelfReport,
|
|
3155
|
+
promptForRuntime: runtimeSelectionPrompt(options, deps)
|
|
3156
|
+
});
|
|
3157
|
+
if (!selection.runtime) {
|
|
3158
|
+
throw new exports.ConnectError(
|
|
3159
|
+
"runtime_undetermined",
|
|
3160
|
+
`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: ${exports.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.`,
|
|
3161
|
+
"rerun_connect_with_explicit_runtime"
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
const runtime = selection.runtime;
|
|
3165
|
+
const installCapabilities = runtimeInstallCapabilities(runtime);
|
|
2514
3166
|
if (options.localMcp) {
|
|
2515
|
-
|
|
2516
|
-
if (!supportsLocalMcp(resolvedRuntime)) {
|
|
3167
|
+
if (!supportsLocalMcp(runtime)) {
|
|
2517
3168
|
throw new Error(
|
|
2518
|
-
`--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(
|
|
3169
|
+
`--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.`
|
|
2519
3170
|
);
|
|
2520
3171
|
}
|
|
2521
3172
|
}
|
|
3173
|
+
if (selection.overrodeHint) {
|
|
3174
|
+
log(`runtime: ${runtime} (detected; ignoring the ${selection.overrodeHint} hint \u2014 pass --runtime-force ${selection.overrodeHint} to override)`);
|
|
3175
|
+
}
|
|
3176
|
+
if (selection.discardedHint) {
|
|
3177
|
+
log(`runtime: ${runtime} (detected; "${selection.discardedHint}" is not a runtime Haven knows \u2014 valid values: ${exports.RUNTIME_FLAG_VALUES})`);
|
|
3178
|
+
}
|
|
3179
|
+
if (selection.source === "prompted") {
|
|
3180
|
+
log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
|
|
3181
|
+
}
|
|
2522
3182
|
log("Warming up your connection to Haven\u2026");
|
|
2523
3183
|
const setup = await api.resolveSetup({
|
|
2524
3184
|
setupToken: options.setupToken,
|
|
2525
3185
|
connectorVersion,
|
|
2526
|
-
runtime
|
|
3186
|
+
runtime
|
|
2527
3187
|
});
|
|
2528
3188
|
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
2529
3189
|
printSetupSummary(setup, log);
|
|
2530
3190
|
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
3191
|
+
if (options.serverName) {
|
|
3192
|
+
assertValidServerSlug(options.serverName);
|
|
3193
|
+
await assertServerSlugAvailable(options.serverName, options.credentialsDir);
|
|
3194
|
+
}
|
|
2531
3195
|
log("Checked local credential storage \u2014 all clear.");
|
|
2532
3196
|
const localKey = generateKey();
|
|
2533
3197
|
const localApiKey = generateLocalApiKey();
|
|
@@ -2539,7 +3203,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2539
3203
|
registration = await api.registerSetup({
|
|
2540
3204
|
setupToken: options.setupToken,
|
|
2541
3205
|
connectorVersion,
|
|
2542
|
-
runtime
|
|
3206
|
+
runtime,
|
|
2543
3207
|
challengeId: setup.challenge.id,
|
|
2544
3208
|
delegateAddress: localKey.address,
|
|
2545
3209
|
proofSignature,
|
|
@@ -2564,6 +3228,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2564
3228
|
const credentialPaths = await writeCredentials({
|
|
2565
3229
|
baseDir: options.credentialsDir,
|
|
2566
3230
|
agentId: registration.agent_id,
|
|
3231
|
+
serverName: options.serverName,
|
|
2567
3232
|
apiKey: localApiKey,
|
|
2568
3233
|
delegateKey: localKey.privateKey,
|
|
2569
3234
|
delegateAddress: localKey.address,
|
|
@@ -2591,7 +3256,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2591
3256
|
);
|
|
2592
3257
|
}
|
|
2593
3258
|
const runtimeInstall = await runRuntimeInstall({
|
|
2594
|
-
runtime
|
|
3259
|
+
runtime,
|
|
2595
3260
|
hostedMcpUrl: registration.hosted_mcp_url,
|
|
2596
3261
|
apiKey: localApiKey,
|
|
2597
3262
|
signerPath: credentialPaths.signerPath,
|
|
@@ -2600,7 +3265,8 @@ async function runConnect(options, deps = {}) {
|
|
|
2600
3265
|
environmentLabel: options.environmentLabel ?? "Local workspace",
|
|
2601
3266
|
ackSigner: options.ackSigner,
|
|
2602
3267
|
ackLocalTools: options.ackLocalTools,
|
|
2603
|
-
localMcp: options.localMcp
|
|
3268
|
+
localMcp: options.localMcp,
|
|
3269
|
+
serverName: options.serverName
|
|
2604
3270
|
}, {
|
|
2605
3271
|
onProgress: log,
|
|
2606
3272
|
// #1543: report "runtime configured" the moment the config write settles,
|
|
@@ -2636,6 +3302,19 @@ async function runConnect(options, deps = {}) {
|
|
|
2636
3302
|
} else {
|
|
2637
3303
|
log("Haven setup on this machine is complete.");
|
|
2638
3304
|
}
|
|
3305
|
+
try {
|
|
3306
|
+
const supersededIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
3307
|
+
if (supersededIds.length > 0) {
|
|
3308
|
+
log("");
|
|
3309
|
+
log(
|
|
3310
|
+
`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.`
|
|
3311
|
+
);
|
|
3312
|
+
log(
|
|
3313
|
+
`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.`
|
|
3314
|
+
);
|
|
3315
|
+
}
|
|
3316
|
+
} catch {
|
|
3317
|
+
}
|
|
2639
3318
|
try {
|
|
2640
3319
|
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
2641
3320
|
runtime: runtimeInstall.runtime,
|
|
@@ -2710,11 +3389,16 @@ function completionOutcome(input) {
|
|
|
2710
3389
|
};
|
|
2711
3390
|
return outcome;
|
|
2712
3391
|
}
|
|
3392
|
+
function runtimeSelectionPrompt(options, deps) {
|
|
3393
|
+
if (options.interactive !== true) return void 0;
|
|
3394
|
+
if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
|
|
3395
|
+
return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
|
|
3396
|
+
}
|
|
2713
3397
|
function failedConnectOutcome(runtimeHint, error) {
|
|
2714
3398
|
const message = error instanceof Error ? error.message : "";
|
|
2715
|
-
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";
|
|
3399
|
+
const code = error instanceof exports.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";
|
|
2716
3400
|
const runtime = normalizeRuntime(runtimeHint);
|
|
2717
|
-
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";
|
|
3401
|
+
const nextAction2 = error instanceof exports.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";
|
|
2718
3402
|
return {
|
|
2719
3403
|
schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
|
|
2720
3404
|
outcome: "failed",
|
|
@@ -2800,7 +3484,7 @@ function describeApprovedBudget(budget) {
|
|
|
2800
3484
|
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
2801
3485
|
const intervalMs = options.intervalMs ?? 5e3;
|
|
2802
3486
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
2803
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
3487
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve9) => setTimeout(resolve9, ms)));
|
|
2804
3488
|
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
2805
3489
|
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
2806
3490
|
let waitingAnnounced = false;
|
|
@@ -2886,11 +3570,39 @@ function activationInstructionWithWhy(profile) {
|
|
|
2886
3570
|
}
|
|
2887
3571
|
return profile.activationInstruction;
|
|
2888
3572
|
}
|
|
3573
|
+
var RERUN_HINT = "npx @haven_ai/connect@alpha";
|
|
3574
|
+
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
3575
|
+
const root = defaultCredentialRoot(baseDir);
|
|
3576
|
+
let entries = [];
|
|
3577
|
+
try {
|
|
3578
|
+
entries = await promises.readdir(root);
|
|
3579
|
+
} catch {
|
|
3580
|
+
return [];
|
|
3581
|
+
}
|
|
3582
|
+
const ids = [];
|
|
3583
|
+
for (const entry of entries) {
|
|
3584
|
+
if (path.join(root, entry) === currentDirectory) continue;
|
|
3585
|
+
const identityPath = path.join(root, entry, "identity.json");
|
|
3586
|
+
try {
|
|
3587
|
+
await promises.stat(identityPath);
|
|
3588
|
+
} catch {
|
|
3589
|
+
continue;
|
|
3590
|
+
}
|
|
3591
|
+
try {
|
|
3592
|
+
const identity = JSON.parse(await promises.readFile(identityPath, "utf8"));
|
|
3593
|
+
ids.push(identity.agent_id ?? entry);
|
|
3594
|
+
} catch {
|
|
3595
|
+
ids.push(entry);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
return ids;
|
|
3599
|
+
}
|
|
2889
3600
|
function printNextSteps(result, log, approval) {
|
|
2890
3601
|
for (const line of completionHandoffLines(result, approval)) log(line);
|
|
2891
3602
|
}
|
|
2892
3603
|
|
|
2893
3604
|
// src/args.ts
|
|
3605
|
+
init_server_names();
|
|
2894
3606
|
function parseArgs(argv, env = process.env) {
|
|
2895
3607
|
const options = {
|
|
2896
3608
|
apiBaseUrl: env.HAVEN_API_URL ?? "http://localhost:3001",
|
|
@@ -2900,6 +3612,9 @@ function parseArgs(argv, env = process.env) {
|
|
|
2900
3612
|
let json = false;
|
|
2901
3613
|
let doctor = false;
|
|
2902
3614
|
let repair = false;
|
|
3615
|
+
let tombstoneDir;
|
|
3616
|
+
let tombstoneReason;
|
|
3617
|
+
let tombstoneReplacedBy;
|
|
2903
3618
|
for (let i = 0; i < argv.length; i += 1) {
|
|
2904
3619
|
const arg = argv[i];
|
|
2905
3620
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -2910,14 +3625,25 @@ function parseArgs(argv, env = process.env) {
|
|
|
2910
3625
|
doctor = true;
|
|
2911
3626
|
} else if (arg === "--repair") {
|
|
2912
3627
|
repair = true;
|
|
3628
|
+
} else if (arg === "--tombstone") {
|
|
3629
|
+
tombstoneDir = requireValue(argv, ++i, arg);
|
|
3630
|
+
} else if (arg === "--reason") {
|
|
3631
|
+
tombstoneReason = requireValue(argv, ++i, arg);
|
|
3632
|
+
} else if (arg === "--replaced-by") {
|
|
3633
|
+
tombstoneReplacedBy = requireValue(argv, ++i, arg);
|
|
2913
3634
|
} else if (arg === "--setup" || arg === "--setup-token") {
|
|
2914
3635
|
options.setupToken = requireValue(argv, ++i, arg);
|
|
2915
3636
|
} else if (arg === "--api" || arg === "--api-url") {
|
|
2916
3637
|
options.apiBaseUrl = requireValue(argv, ++i, arg);
|
|
2917
3638
|
} else if (arg === "--runtime") {
|
|
2918
3639
|
options.runtime = requireValue(argv, ++i, arg);
|
|
3640
|
+
} else if (arg === "--runtime-force") {
|
|
3641
|
+
options.runtimeForce = requireValue(argv, ++i, arg);
|
|
2919
3642
|
} else if (arg === "--credentials-dir") {
|
|
2920
3643
|
options.credentialsDir = requireValue(argv, ++i, arg);
|
|
3644
|
+
} else if (arg === "--name") {
|
|
3645
|
+
options.serverName = requireValue(argv, ++i, arg);
|
|
3646
|
+
assertValidServerSlug(options.serverName);
|
|
2921
3647
|
} else if (arg === "--environment-label") {
|
|
2922
3648
|
options.environmentLabel = requireValue(argv, ++i, arg);
|
|
2923
3649
|
} else if (arg === "--ack-local-tools") {
|
|
@@ -2935,14 +3661,21 @@ function parseArgs(argv, env = process.env) {
|
|
|
2935
3661
|
throw new Error(`Unknown option: ${arg}`);
|
|
2936
3662
|
}
|
|
2937
3663
|
}
|
|
3664
|
+
const tombstone = tombstoneDir ? { directory: tombstoneDir, reason: tombstoneReason, replacedBy: tombstoneReplacedBy } : void 0;
|
|
2938
3665
|
if (help) {
|
|
2939
|
-
return { options, help, json, doctor, repair };
|
|
3666
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
3667
|
+
}
|
|
3668
|
+
if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
3669
|
+
throw new Error("--reason and --replaced-by require --tombstone <dir>.");
|
|
3670
|
+
}
|
|
3671
|
+
if (tombstone) {
|
|
3672
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2940
3673
|
}
|
|
2941
3674
|
if (doctor || repair) {
|
|
2942
3675
|
if (!options.runtime) {
|
|
2943
3676
|
throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
|
|
2944
3677
|
}
|
|
2945
|
-
return { options, help, json, doctor, repair };
|
|
3678
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2946
3679
|
}
|
|
2947
3680
|
if (!options.setupToken) {
|
|
2948
3681
|
throw new Error("Missing --setup <hv_setup_...> setup token.");
|
|
@@ -2951,7 +3684,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
2951
3684
|
throw new Error("Missing --api <Haven API URL>.");
|
|
2952
3685
|
}
|
|
2953
3686
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
2954
|
-
return { options, help, json, doctor, repair };
|
|
3687
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2955
3688
|
}
|
|
2956
3689
|
function helpText() {
|
|
2957
3690
|
return [
|
|
@@ -2961,14 +3694,23 @@ function helpText() {
|
|
|
2961
3694
|
"sends Haven only the public signing address plus a proof signature.",
|
|
2962
3695
|
"",
|
|
2963
3696
|
"Usage:",
|
|
2964
|
-
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools
|
|
3697
|
+
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools",
|
|
2965
3698
|
"",
|
|
2966
3699
|
"Options:",
|
|
2967
3700
|
" --setup <token> Short-lived setup token from Haven.",
|
|
2968
3701
|
" --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
|
|
2969
3702
|
" --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, claude-desktop, or hermes.",
|
|
3703
|
+
" Usually unnecessary: the connector detects the runtime it runs inside, and a detection",
|
|
3704
|
+
" that contradicts this hint wins (with a printed notice). When nothing is detected, an",
|
|
3705
|
+
" interactive terminal is offered the agent clients installed on this machine; this flag is",
|
|
3706
|
+
" how an agent, or a non-interactive run, answers instead. An unknown name is refused, never guessed.",
|
|
3707
|
+
" --runtime-force <name> Escape hatch: use exactly this runtime, ignoring environment detection.",
|
|
2970
3708
|
" --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
|
|
2971
3709
|
" --environment-label <text> Non-sensitive label shown in Haven setup review.",
|
|
3710
|
+
" --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
|
|
3711
|
+
" MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
|
|
3712
|
+
" agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
|
|
3713
|
+
" single hyphens; immutable once wired. Omit for the bare haven / haven-signer pair.",
|
|
2972
3714
|
" --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
|
|
2973
3715
|
" --ack-signer Backward-compatible alias for --ack-local-tools.",
|
|
2974
3716
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
@@ -2979,6 +3721,11 @@ function helpText() {
|
|
|
2979
3721
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
2980
3722
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
2981
3723
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
3724
|
+
" --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
|
|
3725
|
+
" wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
|
|
3726
|
+
" writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
|
|
3727
|
+
" --reason <text> Reason recorded in the tombstone (with --tombstone).",
|
|
3728
|
+
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone).",
|
|
2982
3729
|
" --help Show this help.",
|
|
2983
3730
|
"",
|
|
2984
3731
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -2991,6 +3738,10 @@ function requireValue(argv, index, option) {
|
|
|
2991
3738
|
}
|
|
2992
3739
|
return value;
|
|
2993
3740
|
}
|
|
3741
|
+
|
|
3742
|
+
// src/index.ts
|
|
3743
|
+
init_redact();
|
|
3744
|
+
init_redact();
|
|
2994
3745
|
async function runCli(argv, io = {
|
|
2995
3746
|
stdout: (message) => process.stdout.write(message),
|
|
2996
3747
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -3014,6 +3765,45 @@ async function runCli(argv, io = {
|
|
|
3014
3765
|
`);
|
|
3015
3766
|
return 0;
|
|
3016
3767
|
}
|
|
3768
|
+
if (parsed.tombstone) {
|
|
3769
|
+
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
3770
|
+
const { readFile: readFile11 } = await import('fs/promises');
|
|
3771
|
+
const { join: join10 } = await import('path');
|
|
3772
|
+
try {
|
|
3773
|
+
let agentId = "unknown";
|
|
3774
|
+
try {
|
|
3775
|
+
const identity = JSON.parse(
|
|
3776
|
+
await readFile11(join10(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
3777
|
+
);
|
|
3778
|
+
agentId = identity.agent_id ?? "unknown";
|
|
3779
|
+
} catch {
|
|
3780
|
+
}
|
|
3781
|
+
const info = await writeAgentTombstone2({
|
|
3782
|
+
directory: parsed.tombstone.directory,
|
|
3783
|
+
agentId,
|
|
3784
|
+
reason: parsed.tombstone.reason ?? "retired by operator via --tombstone",
|
|
3785
|
+
replacedBy: parsed.tombstone.replacedBy
|
|
3786
|
+
});
|
|
3787
|
+
if (parsed.json) {
|
|
3788
|
+
io.stdout(`${redactSecrets(JSON.stringify({ tombstoned: true, ...info }))}
|
|
3789
|
+
`);
|
|
3790
|
+
} else {
|
|
3791
|
+
io.stdout(redactSecrets(`Tombstoned agent ${info.agent_id} at ${parsed.tombstone.directory}.
|
|
3792
|
+
`));
|
|
3793
|
+
io.stdout(
|
|
3794
|
+
"Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
|
|
3795
|
+
);
|
|
3796
|
+
io.stdout(
|
|
3797
|
+
"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"
|
|
3798
|
+
);
|
|
3799
|
+
}
|
|
3800
|
+
return 0;
|
|
3801
|
+
} catch (err) {
|
|
3802
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
3803
|
+
`);
|
|
3804
|
+
return 1;
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3017
3807
|
if (parsed.doctor || parsed.repair) {
|
|
3018
3808
|
const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
3019
3809
|
const runtime = parsed.options.runtime ?? "";
|
|
@@ -3036,6 +3826,23 @@ async function runCli(argv, io = {
|
|
|
3036
3826
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3037
3827
|
`));
|
|
3038
3828
|
}
|
|
3829
|
+
const otherAgents = report.agents.filter((agent) => agent.directory !== report.credentialDirectory);
|
|
3830
|
+
if (otherAgents.length > 0) {
|
|
3831
|
+
io.stdout("\nOther agents on this machine:\n");
|
|
3832
|
+
for (const agent of otherAgents) {
|
|
3833
|
+
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
3834
|
+
const failed = agent.checks.filter((check) => !check.ok);
|
|
3835
|
+
const verdict = agent.classification === "wired" ? failed.length === 0 ? "wired, all checks passed" : `wired, ${failed.length} check(s) FAILED` : agent.classification;
|
|
3836
|
+
io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : "\u2022"} ${name}: ${verdict}
|
|
3837
|
+
`));
|
|
3838
|
+
for (const check of failed) {
|
|
3839
|
+
io.stdout(redactSecrets(` \u2717 ${check.label}: ${check.detail}
|
|
3840
|
+
`));
|
|
3841
|
+
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3842
|
+
`));
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3039
3846
|
io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
|
|
3040
3847
|
}
|
|
3041
3848
|
return report.ok ? 0 : 1;
|
|
@@ -3047,7 +3854,15 @@ async function runCli(argv, io = {
|
|
|
3047
3854
|
}
|
|
3048
3855
|
try {
|
|
3049
3856
|
const result = await runConnect(
|
|
3050
|
-
{
|
|
3857
|
+
{
|
|
3858
|
+
...parsed.options,
|
|
3859
|
+
waitForApproval: !parsed.json,
|
|
3860
|
+
// #1719: only a human-facing run may be asked which installed client to
|
|
3861
|
+
// configure. --json is the automation contract — it must fail with a
|
|
3862
|
+
// machine-readable code, never block on stdin. runConnect additionally
|
|
3863
|
+
// requires a real TTY before it prompts.
|
|
3864
|
+
interactive: !parsed.json
|
|
3865
|
+
},
|
|
3051
3866
|
{
|
|
3052
3867
|
log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
|
|
3053
3868
|
`),
|
|
@@ -3085,6 +3900,7 @@ if (isCliEntrypoint()) void main();
|
|
|
3085
3900
|
// src/index.ts
|
|
3086
3901
|
init_signer_runtime();
|
|
3087
3902
|
init_runtime_registry();
|
|
3903
|
+
init_connect_error();
|
|
3088
3904
|
init_runtime_manifest();
|
|
3089
3905
|
|
|
3090
3906
|
exports.CONNECTOR_VERSION = CONNECTOR_VERSION;
|
|
@@ -3097,15 +3913,20 @@ exports.failedConnectOutcome = failedConnectOutcome;
|
|
|
3097
3913
|
exports.generateDelegateKey = generateDelegateKey;
|
|
3098
3914
|
exports.helpText = helpText;
|
|
3099
3915
|
exports.installRuntime = installRuntime;
|
|
3916
|
+
exports.isConnectError = isConnectError;
|
|
3100
3917
|
exports.mcpPackageSpec = mcpPackageSpec;
|
|
3101
3918
|
exports.normalizeRuntime = normalizeRuntime;
|
|
3102
3919
|
exports.parseArgs = parseArgs;
|
|
3103
3920
|
exports.prepareSignerRuntime = prepareSignerRuntime;
|
|
3921
|
+
exports.promptForInstalledClient = promptForInstalledClient;
|
|
3104
3922
|
exports.redactSecrets = redactSecrets;
|
|
3923
|
+
exports.resolveRuntimeByInstalledClientPrompt = resolveRuntimeByInstalledClientPrompt;
|
|
3924
|
+
exports.resolveRuntimeSelection = resolveRuntimeSelection;
|
|
3105
3925
|
exports.runCli = runCli;
|
|
3106
3926
|
exports.runConnect = runConnect;
|
|
3107
3927
|
exports.runtimeInstallCapabilities = runtimeInstallCapabilities;
|
|
3108
3928
|
exports.runtimeProfile = runtimeProfile;
|
|
3929
|
+
exports.scanInstalledClients = scanInstalledClients;
|
|
3109
3930
|
exports.sdkPackageSpec = sdkPackageSpec;
|
|
3110
3931
|
exports.shortAddress = shortAddress;
|
|
3111
3932
|
exports.signerPackageSpec = signerPackageSpec;
|