@haven_ai/connect 0.8.0-alpha.0 → 0.9.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -318,6 +318,19 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
318
318
  `No Haven credentials at ${directory}. Nothing to re-key \u2014 connect this agent first, or pass the --name you wired it under.`
319
319
  );
320
320
  }
321
+ let discoveredSlug;
322
+ if (!key) {
323
+ const binding = await readJsonFile(join(directory, "mcp-server-binding.json"));
324
+ const hostedName = binding && typeof binding === "object" ? asString(binding.server_name) : void 0;
325
+ if (hostedName && hostedName.startsWith("haven-")) {
326
+ const slug = hostedName.slice("haven-".length);
327
+ try {
328
+ assertValidServerSlug(slug);
329
+ discoveredSlug = slug;
330
+ } catch {
331
+ }
332
+ }
333
+ }
321
334
  const agent = await readJsonFile(join(directory, "agent.json")) ?? {};
322
335
  const signer = await readJsonFile(join(directory, "signer.json")) ?? {};
323
336
  const agentId = asString(identity.agent_id);
@@ -333,6 +346,7 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
333
346
  directory,
334
347
  agentId,
335
348
  apiKey,
349
+ ...discoveredSlug ? { discoveredSlug } : {},
336
350
  apiUrl,
337
351
  hostedMcpUrl,
338
352
  delegateAddress: asString(agent.delegate_address) ?? asString(signer.delegate_address),
@@ -361,7 +375,9 @@ async function discoverSoleAgentDirectory(baseDir) {
361
375
  const candidates = [];
362
376
  for (const entry of entries) {
363
377
  const directory = join(root, entry);
364
- if (!await readJsonFile(join(directory, "identity.json"))) continue;
378
+ const identity = await readJsonFile(join(directory, "identity.json"));
379
+ if (!identity) continue;
380
+ if (!asString(identity.api_key)) continue;
365
381
  if (await readJsonFile(join(directory, "TOMBSTONE.json"))) continue;
366
382
  candidates.push(directory);
367
383
  }
@@ -591,6 +607,7 @@ async function listMcpServerBindings(baseDir, excludeDirectory) {
591
607
  var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME, MCP_SERVER_BINDING_FILENAME;
592
608
  var init_storage = __esm({
593
609
  "src/storage.ts"() {
610
+ init_server_names();
594
611
  REKEY_PENDING_FILENAME = "rekey-pending.json";
595
612
  REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
596
613
  CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
@@ -613,9 +630,9 @@ var init_runtime_manifest = __esm({
613
630
  mcpPackage: "@haven_ai/mcp",
614
631
  mcpVersion: MCP_VERSION,
615
632
  sdkPackage: "@haven_ai/sdk",
616
- sdkVersion: "0.8.0-alpha.0",
633
+ sdkVersion: "0.9.0-alpha.0",
617
634
  signerPackage: "@haven_ai/signer",
618
- signerVersion: "0.8.0-alpha.0",
635
+ signerVersion: "0.9.0-alpha.0",
619
636
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
620
637
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
621
638
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -1028,7 +1045,9 @@ async function writeHermesConfig(input, deps) {
1028
1045
  restartRequired: true,
1029
1046
  messages: [
1030
1047
  `Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
1031
- "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`.",
1048
+ // #3737: name the pair actually written — a bare run prints exactly the
1049
+ // historical sentence; a named run names its own pair.
1050
+ `Restart Hermes (start a new session; gateway users: /restart), then verify with \`hermes mcp list\`, \`hermes mcp test ${names.hosted}\`, and \`hermes mcp test ${names.signer}\`.`,
1032
1051
  "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.",
1033
1052
  "If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
1034
1053
  ]
@@ -2996,9 +3015,23 @@ function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env =
2996
3015
  runtime: "claude-code",
2997
3016
  label: "Claude Code",
2998
3017
  // Claude Code is configured through its own CLI (`claude mcp add-json`),
2999
- // not by writing a file this module owns — so its evidence is the
3000
- // client directory, never a config path.
3018
+ // not by writing a file this module owns — so `configPath` stays null
3019
+ // and the write still goes through the CLI. But a user-scope
3020
+ // `~/.claude.json` carrying an `mcpServers` key IS config-file
3021
+ // evidence: the user has already pointed Claude Code at some MCP
3022
+ // server. It is not the file the connector would write, hence the
3023
+ // separate evidence path (#3732): without it, a Codex config won by
3024
+ // construction on any machine where the user runs Claude Code.
3001
3025
  configPath: null,
3026
+ evidencePath: join(homeDir, ".claude.json"),
3027
+ evidenceMatches: (contents) => {
3028
+ try {
3029
+ const parsed = JSON.parse(contents);
3030
+ return typeof parsed === "object" && parsed !== null && "mcpServers" in parsed;
3031
+ } catch {
3032
+ return false;
3033
+ }
3034
+ },
3002
3035
  markers: [join(homeDir, ".claude"), join(homeDir, ".claude.json")]
3003
3036
  },
3004
3037
  {
@@ -3030,7 +3063,10 @@ function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env =
3030
3063
  },
3031
3064
  {
3032
3065
  runtime: "claude-desktop",
3033
- label: "Claude Desktop",
3066
+ // The chat app, a separate runtime from Claude Code. Named so a user in
3067
+ // the desktop app's Code tab cannot read this row as "where I am" —
3068
+ // it sorted above Claude Code whenever both had config (#3732).
3069
+ label: "Claude Desktop (chat app)",
3034
3070
  configPath: runtimeConfigPathFor("claude-desktop", homeDir),
3035
3071
  markers: []
3036
3072
  },
@@ -3045,6 +3081,7 @@ function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env =
3045
3081
  }
3046
3082
  async function scanInstalledClients(options = {}) {
3047
3083
  const exists = options.exists ?? pathExists;
3084
+ const readFile15 = options.readFile ?? ((path) => readFile(path, "utf8"));
3048
3085
  const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
3049
3086
  const found = [];
3050
3087
  for (const target of targets) {
@@ -3054,6 +3091,18 @@ async function scanInstalledClients(options = {}) {
3054
3091
  label: target.label,
3055
3092
  detail: `MCP config found at ${target.configPath}`,
3056
3093
  configPath: target.configPath,
3094
+ evidencePath: target.configPath,
3095
+ evidence: "config-file"
3096
+ });
3097
+ continue;
3098
+ }
3099
+ if (target.evidencePath && await hasConfigEvidence(target, exists, readFile15)) {
3100
+ found.push({
3101
+ runtime: target.runtime,
3102
+ label: target.label,
3103
+ detail: `MCP config found at ${target.evidencePath}`,
3104
+ configPath: target.configPath,
3105
+ evidencePath: target.evidencePath,
3057
3106
  evidence: "config-file"
3058
3107
  });
3059
3108
  continue;
@@ -3065,6 +3114,7 @@ async function scanInstalledClients(options = {}) {
3065
3114
  label: target.label,
3066
3115
  detail: `installed (${marker})`,
3067
3116
  configPath: target.configPath,
3117
+ evidencePath: null,
3068
3118
  evidence: "client-directory"
3069
3119
  });
3070
3120
  break;
@@ -3075,6 +3125,16 @@ async function scanInstalledClients(options = {}) {
3075
3125
  return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
3076
3126
  });
3077
3127
  }
3128
+ async function hasConfigEvidence(target, exists, readFile15) {
3129
+ const path = target.evidencePath;
3130
+ if (!await exists(path)) return false;
3131
+ if (!target.evidenceMatches) return true;
3132
+ try {
3133
+ return target.evidenceMatches(await readFile15(path));
3134
+ } catch {
3135
+ return false;
3136
+ }
3137
+ }
3078
3138
  function installedClientHint(candidates) {
3079
3139
  const installedClients = candidates.map((candidate) => candidate.runtime);
3080
3140
  if (candidates.length === 1) {
@@ -3085,18 +3145,24 @@ function installedClientHint(candidates) {
3085
3145
  }
3086
3146
  async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
3087
3147
  if (candidates.length === 0) throw noInstalledClientsError();
3148
+ const hint = installedClientHint(candidates);
3088
3149
  io.write("Haven could not detect which agent runtime this is.\n");
3089
3150
  io.write("These agent clients are installed on this machine:\n");
3090
3151
  candidates.forEach((candidate, index) => {
3091
- io.write(` ${index + 1}) ${candidate.label} \u2014 ${candidate.detail}
3152
+ const suggested = hint.suggestedRuntime === candidate.runtime ? " (suggested)" : "";
3153
+ io.write(` ${index + 1}) ${candidate.label}${suggested} \u2014 ${candidate.detail}
3092
3154
  `);
3093
3155
  });
3094
3156
  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");
3095
3157
  for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
3096
- const answer = await io.question(`Which one? [1-${candidates.length}] (default 1 \u2014 ${candidates[0].label}): `);
3158
+ const answer = await io.question(`Which one? [1-${candidates.length}]: `);
3097
3159
  if (answer === null) throw promptAbortedError("the prompt was cancelled");
3098
3160
  const trimmed = answer.trim();
3099
- if (trimmed === "") return candidates[0].runtime;
3161
+ if (trimmed === "") {
3162
+ io.write(`Type the number of the client to configure (1-${candidates.length}).
3163
+ `);
3164
+ continue;
3165
+ }
3100
3166
  const picked = Number.parseInt(trimmed, 10);
3101
3167
  if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
3102
3168
  return candidates[picked - 1].runtime;
@@ -3338,17 +3404,45 @@ async function detectWiringCollision(input) {
3338
3404
  suggestedServerName: proposeServerSlug(input.agentName, taken)
3339
3405
  };
3340
3406
  }
3341
- function proposeServerSlug(agentName, taken) {
3342
- let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 32).replace(/-+$/g, "");
3407
+ function proposeServerSlug(agentName, taken, maxBaseLength = 32) {
3408
+ let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, maxBaseLength).replace(/-+$/g, "");
3343
3409
  if (!slugIsValid(base)) base = "agent";
3344
3410
  if (!taken.has(base)) return base;
3345
3411
  for (let n = 2; n < 1e3; n += 1) {
3346
3412
  const suffix = `-${n}`;
3347
- const candidate = `${base.slice(0, 32 - suffix.length).replace(/-+$/g, "")}${suffix}`;
3413
+ const candidate = `${base.slice(0, maxBaseLength - suffix.length).replace(/-+$/g, "")}${suffix}`;
3348
3414
  if (slugIsValid(candidate) && !taken.has(candidate)) return candidate;
3349
3415
  }
3350
3416
  throw new Error(`Could not propose an unused server name for ${JSON.stringify(agentName)}.`);
3351
3417
  }
3418
+ function deriveServerSlug(agentName, taken, markNonProduction) {
3419
+ const mark = "-dev";
3420
+ if (!markNonProduction) return proposeServerSlug(agentName, taken);
3421
+ const base = proposeServerSlug(agentName, taken, 32 - mark.length);
3422
+ if (base.endsWith(mark)) return base;
3423
+ const marked = `${base}${mark}`;
3424
+ if (!taken.has(marked)) return marked;
3425
+ return proposeServerSlug(marked, taken);
3426
+ }
3427
+ async function listTakenDirectoryNames(credentialsDir) {
3428
+ const root = defaultCredentialRoot(credentialsDir);
3429
+ let entries = [];
3430
+ try {
3431
+ entries = await readdir(root);
3432
+ } catch {
3433
+ return /* @__PURE__ */ new Set();
3434
+ }
3435
+ const taken = /* @__PURE__ */ new Set();
3436
+ for (const entry of entries) {
3437
+ try {
3438
+ if (!(await stat(join(root, entry))).isDirectory()) continue;
3439
+ } catch {
3440
+ continue;
3441
+ }
3442
+ taken.add(entry);
3443
+ }
3444
+ return taken;
3445
+ }
3352
3446
  function slugIsValid(slug) {
3353
3447
  try {
3354
3448
  assertValidServerSlug(slug);
@@ -3364,7 +3458,7 @@ async function promptWiringCollisionResolution(collision, agentName, io) {
3364
3458
  io.write(`Setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring.
3365
3459
  `);
3366
3460
  io.write(" r) replace \u2014 re-point haven / haven-signer at the new agent and retire the previous directory locally\n");
3367
- io.write(" (tombstoned, local key files removed; you still revoke it on the Haven agent page)\n");
3461
+ io.write(" (tombstoned, local key files removed; you still use Remove agent\u2026 on the Haven agent page)\n");
3368
3462
  io.write(` a) alongside \u2014 install as a named agent (suggested: ${collision.suggestedServerName}) with its own
3369
3463
  `);
3370
3464
  io.write(` haven-<name> / haven-signer-<name> pair, leaving the current wiring untouched
@@ -3587,7 +3681,7 @@ async function decideTeardown(identity, input) {
3587
3681
  status: "retained",
3588
3682
  probe: "ok",
3589
3683
  detail: "This agent is still ACTIVE on the backend: its API key and delegate key still carry spend authority, so destroying them would be a live spend-authority change. Key material kept in this directory (0o600); its MCP wiring above is gone.",
3590
- remedy: `Revoke the agent on the Haven agent page (connect never revokes), then re-run --unwire; or, to delete the key anyway, re-run with ${DESTROY_FLAG}.`
3684
+ remedy: `Use Remove agent\u2026 on the Haven agent page (connect never revokes), then re-run --unwire; or, to delete the key anyway, re-run with ${DESTROY_FLAG}.`
3591
3685
  };
3592
3686
  case "unauthorized":
3593
3687
  return {
@@ -3748,7 +3842,16 @@ async function executeConnect(options, deps, trace) {
3748
3842
  log("Checked local credential storage \u2014 all clear.");
3749
3843
  let serverName = options.serverName;
3750
3844
  let replacing;
3751
- if (!serverName) {
3845
+ if (!serverName && options.deriveServerName) {
3846
+ const taken = await listTakenDirectoryNames(options.credentialsDir);
3847
+ const nonProduction = setup.haven_wallet.chain_id !== PRODUCTION_CHAIN_ID;
3848
+ serverName = deriveServerSlug(setup.agent.name, taken, nonProduction);
3849
+ await assertServerSlugAvailable(serverName, options.credentialsDir);
3850
+ const derivedNames = serverNamesFor(serverName);
3851
+ log(
3852
+ `Naming this agent's MCP pair from its display name: ${derivedNames.hosted} / ${derivedNames.signer}. Nothing was displaced; to wire the bare haven / haven-signer pair instead, re-run with --bare, or choose the name yourself with --name <slug>.`
3853
+ );
3854
+ } else if (!serverName) {
3752
3855
  const collision = await detectWiringCollision({
3753
3856
  credentialsDir: options.credentialsDir,
3754
3857
  agentName: setup.agent.name
@@ -3974,7 +4077,7 @@ async function executeConnect(options, deps, trace) {
3974
4077
  if (supersededAgentsRetiredLocally === true) {
3975
4078
  log("");
3976
4079
  log(
3977
- `Replaced: previous agent(s) ${supersededIds(replacing)} are retired on this machine but NOT revoked \u2014 revoke them on the Haven agent page, then restart EVERY long-lived host (gateways, TUI workers, editors): each holds the MCP wiring snapshot from its own start time, and the tombstone speaks only when a stale host next probes the old path.`
4080
+ `Replaced: previous agent(s) ${supersededIds(replacing)} are retired on this machine but NOT revoked \u2014 use Remove agent\u2026 on the Haven agent page for each (it ends their live budgets), then restart EVERY long-lived host (gateways, TUI workers, editors): each holds the MCP wiring snapshot from its own start time, and the tombstone speaks only when a stale host next probes the old path.`
3978
4081
  );
3979
4082
  } else if (supersededAgentIds.length > 0) {
3980
4083
  log("");
@@ -3982,7 +4085,7 @@ async function executeConnect(options, deps, trace) {
3982
4085
  `Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${supersededAgentIds.join(", ")} \u2014 still exist with their own keys, and any host that was already running keeps acting as them.`
3983
4086
  );
3984
4087
  log(
3985
- `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.`
4088
+ `If you meant to replace them: use Remove agent\u2026 on the Haven agent page for each (it ends their live budgets), 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.`
3986
4089
  );
3987
4090
  }
3988
4091
  } catch {
@@ -4023,12 +4126,29 @@ async function executeConnect(options, deps, trace) {
4023
4126
  runtimeInstall,
4024
4127
  delegateAddress: registration.delegate_address,
4025
4128
  hostedMcpUrl: registration.hosted_mcp_url,
4129
+ // #3737: the resolved name — derived, explicit, or the bare pair — rides
4130
+ // the machine-readable outcome alongside the prose completion text.
4131
+ serverName,
4026
4132
  supersededAgentIds,
4027
4133
  supersededAgentsRetiredLocally,
4028
4134
  ...replacing ? { retiredAgentIds } : {},
4029
4135
  ...Object.keys(retirementMirrorErrors).length > 0 ? { retirementMirrorErrors } : {},
4030
4136
  existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
4031
4137
  ...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
4138
+ // #3772: only when the install completed. An install that ended with an
4139
+ // error code may not have written the wiring, and on --replace it leaves the
4140
+ // old agent's wiring as possibly the only working one (retirement is skipped
4141
+ // on the same condition) — so it gets no such sentence. That includes
4142
+ // manual_runtime_setup_required, deliberately: the connector wrote nothing
4143
+ // and the manual instruction already says to start a fresh session. A
4144
+ // --replace names every displaced agent even when no binding record exists
4145
+ // (pre-0.4.0 wiring, or a failed best-effort write).
4146
+ ...!runtimeInstall.errorCode && (replacing || reboundFrom) ? {
4147
+ staleSession: {
4148
+ agentIds: [.../* @__PURE__ */ new Set([...replacing?.superseded.map((entry) => entry.agentId) ?? [], ...reboundFrom ? [reboundFrom.agent_id] : []])],
4149
+ ...reboundFrom?.backend_changed ? { otherBackendAgentId: reboundFrom.agent_id } : {}
4150
+ }
4151
+ } : {},
4032
4152
  setupChallengeExpiresAt: setup.challenge.expires_at,
4033
4153
  approvalRequired: registration.agent_status === "pending_approval",
4034
4154
  approvalUrl: registration.approval_url
@@ -4044,6 +4164,10 @@ async function executeConnect(options, deps, trace) {
4044
4164
  outcome
4045
4165
  };
4046
4166
  }
4167
+ function staleSessionNotice(previousAgentIds, otherBackendAgentId) {
4168
+ const one = previousAgentIds.length === 1;
4169
+ return `Any session that was already running keeps acting as the previous agent${one ? "" : "s"} (${previousAgentIds.join(", ")}) until it is restarted, and ${one ? "that agent" : "they"} may still be active in Haven` + (otherBackendAgentId ? ` (${otherBackendAgentId} is on the backend it was created on, not this one)` : "") + `: if ${one ? "it" : "they"} should no longer spend, use Remove agent\u2026 on the Haven agent page (it ends ${one ? "its" : "their"} live budgets).`;
4170
+ }
4047
4171
  function completionOutcome(input) {
4048
4172
  const { runtimeInstall } = input;
4049
4173
  const manualSetup = runtimeInstall.errorCode === "manual_runtime_setup_required";
@@ -4061,7 +4185,10 @@ function completionOutcome(input) {
4061
4185
  probe: { result: runtimeInstall.probeResult },
4062
4186
  activation: {
4063
4187
  restart_required: runtimeInstall.restartRequired,
4064
- instruction: manualSetup ? "Finish the manual MCP setup using the secret-free references shown in normal Connect output, then start a fresh session." : runtimeProfile(runtimeInstall.runtime).activationInstruction
4188
+ instruction: (manualSetup ? "Finish the manual MCP setup using the secret-free references shown in normal Connect output, then start a fresh session." : runtimeProfile(runtimeInstall.runtime).activationInstruction) + // #3772: a rebind changes which agent the server name means, but a
4189
+ // session that is already running keeps the config it loaded — it
4190
+ // goes on acting as the previous agent, which Connect never revokes.
4191
+ (input.staleSession && input.staleSession.agentIds.length > 0 ? ` ${staleSessionNotice(input.staleSession.agentIds, input.staleSession.otherBackendAgentId)}` : "")
4065
4192
  },
4066
4193
  next_action: nextAction2,
4067
4194
  approval: {
@@ -4097,6 +4224,9 @@ function completionOutcome(input) {
4097
4224
  // #3122: always emitted on a completed run (empty list included), for the
4098
4225
  // same reason as superseded_agent_ids above.
4099
4226
  existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
4227
+ // #3737: the resolved hosted server name — always present on a completed
4228
+ // run, bare pair included, so a caller never has to parse the prose.
4229
+ server_name: serverNamesFor(input.serverName).hosted,
4100
4230
  ...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
4101
4231
  ...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
4102
4232
  ...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
@@ -4120,7 +4250,8 @@ function installedClientProse(hint) {
4120
4250
  }
4121
4251
  function runtimeSelectionPrompt(options, deps) {
4122
4252
  if (!interactivePromptAllowed(options, deps)) return void 0;
4123
- return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
4253
+ if (deps.promptRuntime) return deps.promptRuntime;
4254
+ return () => (deps.promptRuntimeByInstalledClient ?? resolveRuntimeByInstalledClientPrompt)({ env: deps.env });
4124
4255
  }
4125
4256
  function interactivePromptAllowed(options, deps) {
4126
4257
  if (options.interactive !== true) return false;
@@ -4146,7 +4277,7 @@ async function resolveWiringCollision(collision, agentName, options, deps) {
4146
4277
  }
4147
4278
  throw new ConnectError(
4148
4279
  "wiring_collision",
4149
- `This machine is already wired to a Haven agent with a live key (${supersededIds(collision)}), and setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring. Nothing was written and the Haven setup token is still unused. If you are an AI agent running this command: do NOT add a flag yourself \u2014 relay this to your user and stop. Your user decides: REPLACE the existing wiring (re-run the same command with --replace added, which re-points the pair at the new agent and retires the previous directory locally \u2014 they still revoke the old agent on the Haven agent page), or install ALONGSIDE it (re-run with --name <slug> added, e.g. --name ${collision.suggestedServerName}, which gives the new agent its own haven-<slug> / haven-signer-<slug> pair). Re-run only with the flag your user chooses.`,
4280
+ `This machine is already wired to a Haven agent with a live key (${supersededIds(collision)}), and setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring. Nothing was written and the Haven setup token is still unused. If you are an AI agent running this command: do NOT add a flag yourself \u2014 relay this to your user and stop. Your user decides: REPLACE the existing wiring (re-run the same command with --replace added, which re-points the pair at the new agent and retires the previous directory locally \u2014 they still use Remove agent\u2026 on the Haven agent page to end the old agent\u2019s budgets), or install ALONGSIDE it (re-run with --name <slug> added, e.g. --name ${collision.suggestedServerName}, which gives the new agent its own haven-<slug> / haven-signer-<slug> pair). Re-run only with the flag your user chooses.`,
4150
4281
  "relay_wiring_collision_to_user",
4151
4282
  { supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
4152
4283
  );
@@ -4427,7 +4558,7 @@ async function listOtherAgentIds(baseDir, currentDirectory) {
4427
4558
  function printNextSteps(result, log, approval, approvalUrl) {
4428
4559
  for (const line of completionHandoffLines(result, approval, approvalUrl)) log(line);
4429
4560
  }
4430
- var CONNECTOR_VERSION, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
4561
+ var CONNECTOR_VERSION, PRODUCTION_CHAIN_ID, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
4431
4562
  var init_runtime = __esm({
4432
4563
  "src/runtime.ts"() {
4433
4564
  init_api();
@@ -4444,7 +4575,8 @@ var init_runtime = __esm({
4444
4575
  init_tombstone();
4445
4576
  init_local_mcp_runtime();
4446
4577
  init_runtime_manifest();
4447
- CONNECTOR_VERSION = "0.8.0-alpha.0";
4578
+ CONNECTOR_VERSION = "0.9.0-alpha.0";
4579
+ PRODUCTION_CHAIN_ID = 8453;
4448
4580
  CONNECTOR_CLIENT_IDENTITY = `@haven_ai/connect/${CONNECTOR_VERSION}`;
4449
4581
  CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4450
4582
  failureOutcomesByError = /* @__PURE__ */ new WeakMap();
@@ -4654,9 +4786,10 @@ async function startRekey(options, deps = {}) {
4654
4786
  started_at: startedAt,
4655
4787
  expires_at: expiresAt
4656
4788
  });
4789
+ const finishName = options.serverName ?? stored.discoveredSlug;
4657
4790
  const finishCommand = [
4658
4791
  connectorRerunCommand("--rekey-finish"),
4659
- options.serverName ? `--name ${options.serverName}` : void 0,
4792
+ finishName ? `--name ${finishName}` : void 0,
4660
4793
  "--api-key <the key the dashboard showed you>",
4661
4794
  options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
4662
4795
  ].filter(Boolean).join(" ");
@@ -5488,9 +5621,9 @@ async function runDoctor(input, deps = {}) {
5488
5621
  level: supersededLevel,
5489
5622
  detail: supersededLevel === "failed" ? `${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.` : supersededLevel === "advisory" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. Runtime '${input2.runtime}' has no config file the connector can read, so which of these agents are wired cannot be verified from this machine: a live key here may be an agent you use deliberately, or one a host started before your latest setup is still spending as.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
5490
5623
  ...supersededLevel === "failed" ? {
5491
- 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.`
5624
+ repair: `Use Remove agent\u2026 on the Haven agent page for ${supersededLive.join(", ")} (it ends each one\u2019s live budgets), then remove the old director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
5492
5625
  } : supersededLevel === "advisory" ? {
5493
- repair: `Check ${supersededLive.join(", ")} on the Haven agent page: revoke the ones you no longer use, then remove their director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
5626
+ repair: `Check ${supersededLive.join(", ")} on the Haven agent page: use Remove agent\u2026 on the ones you no longer use, then remove their director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
5494
5627
  } : {}
5495
5628
  });
5496
5629
  }
@@ -5715,6 +5848,7 @@ function parseArgs(argv, env = process.env) {
5715
5848
  let dryRun = false;
5716
5849
  let unwireDir;
5717
5850
  let replace = false;
5851
+ let bare = false;
5718
5852
  for (let i = 0; i < argv.length; i += 1) {
5719
5853
  const arg = argv[i];
5720
5854
  if (arg === "--help" || arg === "-h") {
@@ -5762,6 +5896,8 @@ function parseArgs(argv, env = process.env) {
5762
5896
  options.credentialsDir = requireValue(argv, ++i, arg);
5763
5897
  } else if (arg === "--replace") {
5764
5898
  replace = true;
5899
+ } else if (arg === "--bare") {
5900
+ bare = true;
5765
5901
  } else if (arg === "--name") {
5766
5902
  options.serverName = requireValue(argv, ++i, arg);
5767
5903
  assertValidServerSlug(options.serverName);
@@ -5787,16 +5923,23 @@ function parseArgs(argv, env = process.env) {
5787
5923
  if (help) {
5788
5924
  return { options, help, json, doctor, repair, tombstone, rekey };
5789
5925
  }
5790
- if (replace) {
5926
+ if (replace || bare) {
5791
5927
  if (rekeyPhase || tombstoneDir || unwire || doctor || repair || pruneSignerRuntimes2) {
5792
- throw new Error("--replace belongs to a --setup run: it says what to do when the bare haven / haven-signer pair is already wired to another agent.");
5928
+ throw new Error(`${replace ? "--replace" : "--bare"} belongs to a --setup run: it says what to do when the bare haven / haven-signer pair is already wired to another agent.`);
5793
5929
  }
5794
- if (options.serverName) {
5930
+ if (bare && options.serverName) {
5795
5931
  throw new Error(
5796
- "--replace and --name contradict each other: --replace overwrites the bare haven / haven-signer wiring, --name installs alongside it under its own pair. Pass one of them."
5932
+ "--bare and --name contradict each other: --bare wires the bare haven / haven-signer pair, --name installs a named haven-<slug> pair. Pass one of them."
5797
5933
  );
5798
5934
  }
5799
- options.replaceExistingWiring = true;
5935
+ if (replace) {
5936
+ if (options.serverName) {
5937
+ throw new Error(
5938
+ "--replace and --name contradict each other: --replace overwrites the bare haven / haven-signer wiring, --name installs alongside it under its own pair. Pass one of them."
5939
+ );
5940
+ }
5941
+ options.replaceExistingWiring = true;
5942
+ }
5800
5943
  }
5801
5944
  if (destroyKeyMaterial && !unwire) {
5802
5945
  throw new Error("--destroy-key-material only applies to --unwire.");
@@ -5870,6 +6013,9 @@ function parseArgs(argv, env = process.env) {
5870
6013
  if (!options.apiBaseUrl) {
5871
6014
  throw new Error("Missing --api <Haven API URL>.");
5872
6015
  }
6016
+ if (!options.serverName && !bare && !replace) {
6017
+ options.deriveServerName = true;
6018
+ }
5873
6019
  options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
5874
6020
  return { options, help, json, doctor, repair, tombstone, rekey };
5875
6021
  }
@@ -5897,13 +6043,19 @@ function helpText() {
5897
6043
  " --replace When this machine is already wired to a different Haven agent on the bare",
5898
6044
  " haven / haven-signer pair, re-point that pair at the new agent and retire the",
5899
6045
  " previous agent directory locally (tombstoned, local key files removed).",
5900
- " Nothing is revoked \u2014 revoke the old agent on the Haven agent page. Without",
5901
- " this flag a non-interactive run REFUSES such a collision (wiring_collision)",
5902
- " and an interactive terminal is asked; --name installs alongside instead.",
6046
+ " Nothing is revoked \u2014 use Remove agent\u2026 on the Haven agent page. Implies",
6047
+ " the bare pair. Without it (or --bare) a non-interactive run REFUSES such a",
6048
+ " collision (wiring_collision) and an interactive terminal is asked.",
6049
+ " --bare Wire the bare haven / haven-signer pair. Without --name a setup now names",
6050
+ " its pair from the agent's display name (haven-<slug> /",
6051
+ " haven-signer-<slug>, de-collided against this machine, ending -dev on a",
6052
+ " non-production backend), so adding an agent never has to displace anyone;",
6053
+ " --bare is the explicit opt-in to the unnamed pair. --name overrides it.",
5903
6054
  " --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
5904
6055
  " MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
5905
6056
  " agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
5906
- " single hyphens; immutable once wired. Omit for the bare haven / haven-signer pair.",
6057
+ " single hyphens; immutable once wired. Without it the pair is named from the",
6058
+ " agent's display name; --bare wires the bare haven / haven-signer pair.",
5907
6059
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
5908
6060
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
5909
6061
  " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
@@ -6049,7 +6201,7 @@ async function runCli(argv, io = {
6049
6201
  io.stdout(redactSecrets(`Tombstoned agent ${info.agent_id} at ${parsed.tombstone.directory}.
6050
6202
  `));
6051
6203
  io.stdout(
6052
- "Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
6204
+ "Key files were NOT touched and nothing was revoked \u2014 on the Haven agent page, use Remove agent\u2026 (or Stop budget) if you have not already.\n"
6053
6205
  );
6054
6206
  io.stdout(
6055
6207
  info.recordPath !== null ? `A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
@@ -6158,7 +6310,7 @@ async function runCli(argv, io = {
6158
6310
  failures.length > 0 ? " Some entries were NOT removed (\u2717 above). Re-run `--unwire` after resolving each refusal \u2014\n it is idempotent.\n" : retained ? " The wiring is gone; the key material is not (\u2717 above). `--doctor` will keep reporting this directory\n as `superseded` until the key is revoked or destroyed \u2014 that is the honest state.\n" : " Verify: `--doctor --runtime <runtime>` per host should report this agent as `retired` with a\n clean runtime-config check.\n"
6159
6311
  );
6160
6312
  io.stdout(
6161
- "Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot\nfrom its own start time. " + (retained ? "This directory\u2019s local key material was KEPT (see above); the tombstone\n" : "This directory\u2019s local key material was removed and the tombstone\n") + "record + #2155 mirror survive \u2014 but nothing was REVOKED on the backend. If you have not\nalready, revoke the agent on the Haven agent page to stop it spending entirely.\n"
6313
+ "Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot\nfrom its own start time. " + (retained ? "This directory\u2019s local key material was KEPT (see above); the tombstone\n" : "This directory\u2019s local key material was removed and the tombstone\n") + "record + #2155 mirror survive \u2014 but nothing was REVOKED on the backend. If you have not\nalready, use Remove agent\u2026 on the Haven agent page to end its budgets on-chain and stop it spending.\n"
6162
6314
  );
6163
6315
  }
6164
6316
  return failures.length > 0 || retained ? 1 : 0;