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