@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/cli.cjs CHANGED
@@ -325,6 +325,19 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
325
325
  `No Haven credentials at ${directory}. Nothing to re-key \u2014 connect this agent first, or pass the --name you wired it under.`
326
326
  );
327
327
  }
328
+ let discoveredSlug;
329
+ if (!key) {
330
+ const binding = await readJsonFile(path.join(directory, "mcp-server-binding.json"));
331
+ const hostedName = binding && typeof binding === "object" ? asString(binding.server_name) : void 0;
332
+ if (hostedName && hostedName.startsWith("haven-")) {
333
+ const slug = hostedName.slice("haven-".length);
334
+ try {
335
+ assertValidServerSlug(slug);
336
+ discoveredSlug = slug;
337
+ } catch {
338
+ }
339
+ }
340
+ }
328
341
  const agent = await readJsonFile(path.join(directory, "agent.json")) ?? {};
329
342
  const signer = await readJsonFile(path.join(directory, "signer.json")) ?? {};
330
343
  const agentId = asString(identity.agent_id);
@@ -340,6 +353,7 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
340
353
  directory,
341
354
  agentId,
342
355
  apiKey,
356
+ ...discoveredSlug ? { discoveredSlug } : {},
343
357
  apiUrl,
344
358
  hostedMcpUrl,
345
359
  delegateAddress: asString(agent.delegate_address) ?? asString(signer.delegate_address),
@@ -368,7 +382,9 @@ async function discoverSoleAgentDirectory(baseDir) {
368
382
  const candidates = [];
369
383
  for (const entry of entries) {
370
384
  const directory = path.join(root, entry);
371
- if (!await readJsonFile(path.join(directory, "identity.json"))) continue;
385
+ const identity = await readJsonFile(path.join(directory, "identity.json"));
386
+ if (!identity) continue;
387
+ if (!asString(identity.api_key)) continue;
372
388
  if (await readJsonFile(path.join(directory, "TOMBSTONE.json"))) continue;
373
389
  candidates.push(directory);
374
390
  }
@@ -598,6 +614,7 @@ async function listMcpServerBindings(baseDir, excludeDirectory) {
598
614
  var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME, MCP_SERVER_BINDING_FILENAME;
599
615
  var init_storage = __esm({
600
616
  "src/storage.ts"() {
617
+ init_server_names();
601
618
  REKEY_PENDING_FILENAME = "rekey-pending.json";
602
619
  REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
603
620
  CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
@@ -620,9 +637,9 @@ var init_runtime_manifest = __esm({
620
637
  mcpPackage: "@haven_ai/mcp",
621
638
  mcpVersion: mcp.MCP_VERSION,
622
639
  sdkPackage: "@haven_ai/sdk",
623
- sdkVersion: "0.8.1-alpha.0",
640
+ sdkVersion: "0.9.0-alpha.0",
624
641
  signerPackage: "@haven_ai/signer",
625
- signerVersion: "0.8.1-alpha.0",
642
+ signerVersion: "0.9.0-alpha.0",
626
643
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
627
644
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
628
645
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -1035,7 +1052,9 @@ async function writeHermesConfig(input, deps) {
1035
1052
  restartRequired: true,
1036
1053
  messages: [
1037
1054
  `Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
1038
- "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`.",
1055
+ // #3737: name the pair actually written — a bare run prints exactly the
1056
+ // historical sentence; a named run names its own pair.
1057
+ `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}\`.`,
1039
1058
  "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.",
1040
1059
  "If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
1041
1060
  ]
@@ -3003,9 +3022,23 @@ function installedClientTargets(homeDir = os.homedir(), cwd = process.cwd(), env
3003
3022
  runtime: "claude-code",
3004
3023
  label: "Claude Code",
3005
3024
  // Claude Code is configured through its own CLI (`claude mcp add-json`),
3006
- // not by writing a file this module owns — so its evidence is the
3007
- // client directory, never a config path.
3025
+ // not by writing a file this module owns — so `configPath` stays null
3026
+ // and the write still goes through the CLI. But a user-scope
3027
+ // `~/.claude.json` carrying an `mcpServers` key IS config-file
3028
+ // evidence: the user has already pointed Claude Code at some MCP
3029
+ // server. It is not the file the connector would write, hence the
3030
+ // separate evidence path (#3732): without it, a Codex config won by
3031
+ // construction on any machine where the user runs Claude Code.
3008
3032
  configPath: null,
3033
+ evidencePath: path.join(homeDir, ".claude.json"),
3034
+ evidenceMatches: (contents) => {
3035
+ try {
3036
+ const parsed = JSON.parse(contents);
3037
+ return typeof parsed === "object" && parsed !== null && "mcpServers" in parsed;
3038
+ } catch {
3039
+ return false;
3040
+ }
3041
+ },
3009
3042
  markers: [path.join(homeDir, ".claude"), path.join(homeDir, ".claude.json")]
3010
3043
  },
3011
3044
  {
@@ -3037,7 +3070,10 @@ function installedClientTargets(homeDir = os.homedir(), cwd = process.cwd(), env
3037
3070
  },
3038
3071
  {
3039
3072
  runtime: "claude-desktop",
3040
- label: "Claude Desktop",
3073
+ // The chat app, a separate runtime from Claude Code. Named so a user in
3074
+ // the desktop app's Code tab cannot read this row as "where I am" —
3075
+ // it sorted above Claude Code whenever both had config (#3732).
3076
+ label: "Claude Desktop (chat app)",
3041
3077
  configPath: runtimeConfigPathFor("claude-desktop", homeDir),
3042
3078
  markers: []
3043
3079
  },
@@ -3052,6 +3088,7 @@ function installedClientTargets(homeDir = os.homedir(), cwd = process.cwd(), env
3052
3088
  }
3053
3089
  async function scanInstalledClients(options = {}) {
3054
3090
  const exists = options.exists ?? pathExists;
3091
+ const readFile15 = options.readFile ?? ((path) => promises.readFile(path, "utf8"));
3055
3092
  const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
3056
3093
  const found = [];
3057
3094
  for (const target of targets) {
@@ -3061,6 +3098,18 @@ async function scanInstalledClients(options = {}) {
3061
3098
  label: target.label,
3062
3099
  detail: `MCP config found at ${target.configPath}`,
3063
3100
  configPath: target.configPath,
3101
+ evidencePath: target.configPath,
3102
+ evidence: "config-file"
3103
+ });
3104
+ continue;
3105
+ }
3106
+ if (target.evidencePath && await hasConfigEvidence(target, exists, readFile15)) {
3107
+ found.push({
3108
+ runtime: target.runtime,
3109
+ label: target.label,
3110
+ detail: `MCP config found at ${target.evidencePath}`,
3111
+ configPath: target.configPath,
3112
+ evidencePath: target.evidencePath,
3064
3113
  evidence: "config-file"
3065
3114
  });
3066
3115
  continue;
@@ -3072,6 +3121,7 @@ async function scanInstalledClients(options = {}) {
3072
3121
  label: target.label,
3073
3122
  detail: `installed (${marker})`,
3074
3123
  configPath: target.configPath,
3124
+ evidencePath: null,
3075
3125
  evidence: "client-directory"
3076
3126
  });
3077
3127
  break;
@@ -3082,6 +3132,16 @@ async function scanInstalledClients(options = {}) {
3082
3132
  return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
3083
3133
  });
3084
3134
  }
3135
+ async function hasConfigEvidence(target, exists, readFile15) {
3136
+ const path = target.evidencePath;
3137
+ if (!await exists(path)) return false;
3138
+ if (!target.evidenceMatches) return true;
3139
+ try {
3140
+ return target.evidenceMatches(await readFile15(path));
3141
+ } catch {
3142
+ return false;
3143
+ }
3144
+ }
3085
3145
  function installedClientHint(candidates) {
3086
3146
  const installedClients = candidates.map((candidate) => candidate.runtime);
3087
3147
  if (candidates.length === 1) {
@@ -3092,18 +3152,24 @@ function installedClientHint(candidates) {
3092
3152
  }
3093
3153
  async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
3094
3154
  if (candidates.length === 0) throw noInstalledClientsError();
3155
+ const hint = installedClientHint(candidates);
3095
3156
  io.write("Haven could not detect which agent runtime this is.\n");
3096
3157
  io.write("These agent clients are installed on this machine:\n");
3097
3158
  candidates.forEach((candidate, index) => {
3098
- io.write(` ${index + 1}) ${candidate.label} \u2014 ${candidate.detail}
3159
+ const suggested = hint.suggestedRuntime === candidate.runtime ? " (suggested)" : "";
3160
+ io.write(` ${index + 1}) ${candidate.label}${suggested} \u2014 ${candidate.detail}
3099
3161
  `);
3100
3162
  });
3101
3163
  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");
3102
3164
  for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
3103
- const answer = await io.question(`Which one? [1-${candidates.length}] (default 1 \u2014 ${candidates[0].label}): `);
3165
+ const answer = await io.question(`Which one? [1-${candidates.length}]: `);
3104
3166
  if (answer === null) throw promptAbortedError("the prompt was cancelled");
3105
3167
  const trimmed = answer.trim();
3106
- if (trimmed === "") return candidates[0].runtime;
3168
+ if (trimmed === "") {
3169
+ io.write(`Type the number of the client to configure (1-${candidates.length}).
3170
+ `);
3171
+ continue;
3172
+ }
3107
3173
  const picked = Number.parseInt(trimmed, 10);
3108
3174
  if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
3109
3175
  return candidates[picked - 1].runtime;
@@ -3345,17 +3411,45 @@ async function detectWiringCollision(input) {
3345
3411
  suggestedServerName: proposeServerSlug(input.agentName, taken)
3346
3412
  };
3347
3413
  }
3348
- function proposeServerSlug(agentName, taken) {
3349
- let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 32).replace(/-+$/g, "");
3414
+ function proposeServerSlug(agentName, taken, maxBaseLength = 32) {
3415
+ let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, maxBaseLength).replace(/-+$/g, "");
3350
3416
  if (!slugIsValid(base)) base = "agent";
3351
3417
  if (!taken.has(base)) return base;
3352
3418
  for (let n = 2; n < 1e3; n += 1) {
3353
3419
  const suffix = `-${n}`;
3354
- const candidate = `${base.slice(0, 32 - suffix.length).replace(/-+$/g, "")}${suffix}`;
3420
+ const candidate = `${base.slice(0, maxBaseLength - suffix.length).replace(/-+$/g, "")}${suffix}`;
3355
3421
  if (slugIsValid(candidate) && !taken.has(candidate)) return candidate;
3356
3422
  }
3357
3423
  throw new Error(`Could not propose an unused server name for ${JSON.stringify(agentName)}.`);
3358
3424
  }
3425
+ function deriveServerSlug(agentName, taken, markNonProduction) {
3426
+ const mark = "-dev";
3427
+ if (!markNonProduction) return proposeServerSlug(agentName, taken);
3428
+ const base = proposeServerSlug(agentName, taken, 32 - mark.length);
3429
+ if (base.endsWith(mark)) return base;
3430
+ const marked = `${base}${mark}`;
3431
+ if (!taken.has(marked)) return marked;
3432
+ return proposeServerSlug(marked, taken);
3433
+ }
3434
+ async function listTakenDirectoryNames(credentialsDir) {
3435
+ const root = defaultCredentialRoot(credentialsDir);
3436
+ let entries = [];
3437
+ try {
3438
+ entries = await promises.readdir(root);
3439
+ } catch {
3440
+ return /* @__PURE__ */ new Set();
3441
+ }
3442
+ const taken = /* @__PURE__ */ new Set();
3443
+ for (const entry of entries) {
3444
+ try {
3445
+ if (!(await promises.stat(path.join(root, entry))).isDirectory()) continue;
3446
+ } catch {
3447
+ continue;
3448
+ }
3449
+ taken.add(entry);
3450
+ }
3451
+ return taken;
3452
+ }
3359
3453
  function slugIsValid(slug) {
3360
3454
  try {
3361
3455
  assertValidServerSlug(slug);
@@ -3371,7 +3465,7 @@ async function promptWiringCollisionResolution(collision, agentName, io) {
3371
3465
  io.write(`Setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring.
3372
3466
  `);
3373
3467
  io.write(" r) replace \u2014 re-point haven / haven-signer at the new agent and retire the previous directory locally\n");
3374
- io.write(" (tombstoned, local key files removed; you still revoke it on the Haven agent page)\n");
3468
+ io.write(" (tombstoned, local key files removed; you still use Remove agent\u2026 on the Haven agent page)\n");
3375
3469
  io.write(` a) alongside \u2014 install as a named agent (suggested: ${collision.suggestedServerName}) with its own
3376
3470
  `);
3377
3471
  io.write(` haven-<name> / haven-signer-<name> pair, leaving the current wiring untouched
@@ -3594,7 +3688,7 @@ async function decideTeardown(identity, input) {
3594
3688
  status: "retained",
3595
3689
  probe: "ok",
3596
3690
  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.",
3597
- 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}.`
3691
+ 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}.`
3598
3692
  };
3599
3693
  case "unauthorized":
3600
3694
  return {
@@ -3755,7 +3849,16 @@ async function executeConnect(options, deps, trace) {
3755
3849
  log("Checked local credential storage \u2014 all clear.");
3756
3850
  let serverName = options.serverName;
3757
3851
  let replacing;
3758
- if (!serverName) {
3852
+ if (!serverName && options.deriveServerName) {
3853
+ const taken = await listTakenDirectoryNames(options.credentialsDir);
3854
+ const nonProduction = setup.haven_wallet.chain_id !== PRODUCTION_CHAIN_ID;
3855
+ serverName = deriveServerSlug(setup.agent.name, taken, nonProduction);
3856
+ await assertServerSlugAvailable(serverName, options.credentialsDir);
3857
+ const derivedNames = serverNamesFor(serverName);
3858
+ log(
3859
+ `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>.`
3860
+ );
3861
+ } else if (!serverName) {
3759
3862
  const collision = await detectWiringCollision({
3760
3863
  credentialsDir: options.credentialsDir,
3761
3864
  agentName: setup.agent.name
@@ -3981,7 +4084,7 @@ async function executeConnect(options, deps, trace) {
3981
4084
  if (supersededAgentsRetiredLocally === true) {
3982
4085
  log("");
3983
4086
  log(
3984
- `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.`
4087
+ `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.`
3985
4088
  );
3986
4089
  } else if (supersededAgentIds.length > 0) {
3987
4090
  log("");
@@ -3989,7 +4092,7 @@ async function executeConnect(options, deps, trace) {
3989
4092
  `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.`
3990
4093
  );
3991
4094
  log(
3992
- `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.`
4095
+ `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.`
3993
4096
  );
3994
4097
  }
3995
4098
  } catch {
@@ -4030,12 +4133,29 @@ async function executeConnect(options, deps, trace) {
4030
4133
  runtimeInstall,
4031
4134
  delegateAddress: registration.delegate_address,
4032
4135
  hostedMcpUrl: registration.hosted_mcp_url,
4136
+ // #3737: the resolved name — derived, explicit, or the bare pair — rides
4137
+ // the machine-readable outcome alongside the prose completion text.
4138
+ serverName,
4033
4139
  supersededAgentIds,
4034
4140
  supersededAgentsRetiredLocally,
4035
4141
  ...replacing ? { retiredAgentIds } : {},
4036
4142
  ...Object.keys(retirementMirrorErrors).length > 0 ? { retirementMirrorErrors } : {},
4037
4143
  existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
4038
4144
  ...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
4145
+ // #3772: only when the install completed. An install that ended with an
4146
+ // error code may not have written the wiring, and on --replace it leaves the
4147
+ // old agent's wiring as possibly the only working one (retirement is skipped
4148
+ // on the same condition) — so it gets no such sentence. That includes
4149
+ // manual_runtime_setup_required, deliberately: the connector wrote nothing
4150
+ // and the manual instruction already says to start a fresh session. A
4151
+ // --replace names every displaced agent even when no binding record exists
4152
+ // (pre-0.4.0 wiring, or a failed best-effort write).
4153
+ ...!runtimeInstall.errorCode && (replacing || reboundFrom) ? {
4154
+ staleSession: {
4155
+ agentIds: [.../* @__PURE__ */ new Set([...replacing?.superseded.map((entry) => entry.agentId) ?? [], ...reboundFrom ? [reboundFrom.agent_id] : []])],
4156
+ ...reboundFrom?.backend_changed ? { otherBackendAgentId: reboundFrom.agent_id } : {}
4157
+ }
4158
+ } : {},
4039
4159
  setupChallengeExpiresAt: setup.challenge.expires_at,
4040
4160
  approvalRequired: registration.agent_status === "pending_approval",
4041
4161
  approvalUrl: registration.approval_url
@@ -4051,6 +4171,10 @@ async function executeConnect(options, deps, trace) {
4051
4171
  outcome
4052
4172
  };
4053
4173
  }
4174
+ function staleSessionNotice(previousAgentIds, otherBackendAgentId) {
4175
+ const one = previousAgentIds.length === 1;
4176
+ 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).`;
4177
+ }
4054
4178
  function completionOutcome(input) {
4055
4179
  const { runtimeInstall } = input;
4056
4180
  const manualSetup = runtimeInstall.errorCode === "manual_runtime_setup_required";
@@ -4068,7 +4192,10 @@ function completionOutcome(input) {
4068
4192
  probe: { result: runtimeInstall.probeResult },
4069
4193
  activation: {
4070
4194
  restart_required: runtimeInstall.restartRequired,
4071
- 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
4195
+ 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
4196
+ // session that is already running keeps the config it loaded — it
4197
+ // goes on acting as the previous agent, which Connect never revokes.
4198
+ (input.staleSession && input.staleSession.agentIds.length > 0 ? ` ${staleSessionNotice(input.staleSession.agentIds, input.staleSession.otherBackendAgentId)}` : "")
4072
4199
  },
4073
4200
  next_action: nextAction2,
4074
4201
  approval: {
@@ -4104,6 +4231,9 @@ function completionOutcome(input) {
4104
4231
  // #3122: always emitted on a completed run (empty list included), for the
4105
4232
  // same reason as superseded_agent_ids above.
4106
4233
  existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
4234
+ // #3737: the resolved hosted server name — always present on a completed
4235
+ // run, bare pair included, so a caller never has to parse the prose.
4236
+ server_name: serverNamesFor(input.serverName).hosted,
4107
4237
  ...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
4108
4238
  ...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
4109
4239
  ...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
@@ -4127,7 +4257,8 @@ function installedClientProse(hint) {
4127
4257
  }
4128
4258
  function runtimeSelectionPrompt(options, deps) {
4129
4259
  if (!interactivePromptAllowed(options, deps)) return void 0;
4130
- return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
4260
+ if (deps.promptRuntime) return deps.promptRuntime;
4261
+ return () => (deps.promptRuntimeByInstalledClient ?? resolveRuntimeByInstalledClientPrompt)({ env: deps.env });
4131
4262
  }
4132
4263
  function interactivePromptAllowed(options, deps) {
4133
4264
  if (options.interactive !== true) return false;
@@ -4153,7 +4284,7 @@ async function resolveWiringCollision(collision, agentName, options, deps) {
4153
4284
  }
4154
4285
  throw new ConnectError(
4155
4286
  "wiring_collision",
4156
- `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.`,
4287
+ `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.`,
4157
4288
  "relay_wiring_collision_to_user",
4158
4289
  { supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
4159
4290
  );
@@ -4434,7 +4565,7 @@ async function listOtherAgentIds(baseDir, currentDirectory) {
4434
4565
  function printNextSteps(result, log, approval, approvalUrl) {
4435
4566
  for (const line of completionHandoffLines(result, approval, approvalUrl)) log(line);
4436
4567
  }
4437
- var CONNECTOR_VERSION, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
4568
+ var CONNECTOR_VERSION, PRODUCTION_CHAIN_ID, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
4438
4569
  var init_runtime = __esm({
4439
4570
  "src/runtime.ts"() {
4440
4571
  init_api();
@@ -4451,7 +4582,8 @@ var init_runtime = __esm({
4451
4582
  init_tombstone();
4452
4583
  init_local_mcp_runtime();
4453
4584
  init_runtime_manifest();
4454
- CONNECTOR_VERSION = "0.8.1-alpha.0";
4585
+ CONNECTOR_VERSION = "0.9.0-alpha.0";
4586
+ PRODUCTION_CHAIN_ID = 8453;
4455
4587
  CONNECTOR_CLIENT_IDENTITY = `@haven_ai/connect/${CONNECTOR_VERSION}`;
4456
4588
  CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4457
4589
  failureOutcomesByError = /* @__PURE__ */ new WeakMap();
@@ -4661,9 +4793,10 @@ async function startRekey(options, deps = {}) {
4661
4793
  started_at: startedAt,
4662
4794
  expires_at: expiresAt
4663
4795
  });
4796
+ const finishName = options.serverName ?? stored.discoveredSlug;
4664
4797
  const finishCommand = [
4665
4798
  sdk.connectorRerunCommand("--rekey-finish"),
4666
- options.serverName ? `--name ${options.serverName}` : void 0,
4799
+ finishName ? `--name ${finishName}` : void 0,
4667
4800
  "--api-key <the key the dashboard showed you>",
4668
4801
  options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
4669
4802
  ].filter(Boolean).join(" ");
@@ -5495,9 +5628,9 @@ async function runDoctor(input, deps = {}) {
5495
5628
  level: supersededLevel,
5496
5629
  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("; ")}.`,
5497
5630
  ...supersededLevel === "failed" ? {
5498
- 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.`
5631
+ 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.`
5499
5632
  } : supersededLevel === "advisory" ? {
5500
- 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.`
5633
+ 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.`
5501
5634
  } : {}
5502
5635
  });
5503
5636
  }
@@ -5722,6 +5855,7 @@ function parseArgs(argv, env = process.env) {
5722
5855
  let dryRun = false;
5723
5856
  let unwireDir;
5724
5857
  let replace = false;
5858
+ let bare = false;
5725
5859
  for (let i = 0; i < argv.length; i += 1) {
5726
5860
  const arg = argv[i];
5727
5861
  if (arg === "--help" || arg === "-h") {
@@ -5769,6 +5903,8 @@ function parseArgs(argv, env = process.env) {
5769
5903
  options.credentialsDir = requireValue(argv, ++i, arg);
5770
5904
  } else if (arg === "--replace") {
5771
5905
  replace = true;
5906
+ } else if (arg === "--bare") {
5907
+ bare = true;
5772
5908
  } else if (arg === "--name") {
5773
5909
  options.serverName = requireValue(argv, ++i, arg);
5774
5910
  assertValidServerSlug(options.serverName);
@@ -5794,16 +5930,23 @@ function parseArgs(argv, env = process.env) {
5794
5930
  if (help) {
5795
5931
  return { options, help, json, doctor, repair, tombstone, rekey };
5796
5932
  }
5797
- if (replace) {
5933
+ if (replace || bare) {
5798
5934
  if (rekeyPhase || tombstoneDir || unwire || doctor || repair || pruneSignerRuntimes2) {
5799
- 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.");
5935
+ 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.`);
5800
5936
  }
5801
- if (options.serverName) {
5937
+ if (bare && options.serverName) {
5802
5938
  throw new Error(
5803
- "--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
+ "--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."
5804
5940
  );
5805
5941
  }
5806
- options.replaceExistingWiring = true;
5942
+ if (replace) {
5943
+ if (options.serverName) {
5944
+ throw new Error(
5945
+ "--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."
5946
+ );
5947
+ }
5948
+ options.replaceExistingWiring = true;
5949
+ }
5807
5950
  }
5808
5951
  if (destroyKeyMaterial && !unwire) {
5809
5952
  throw new Error("--destroy-key-material only applies to --unwire.");
@@ -5877,6 +6020,9 @@ function parseArgs(argv, env = process.env) {
5877
6020
  if (!options.apiBaseUrl) {
5878
6021
  throw new Error("Missing --api <Haven API URL>.");
5879
6022
  }
6023
+ if (!options.serverName && !bare && !replace) {
6024
+ options.deriveServerName = true;
6025
+ }
5880
6026
  options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
5881
6027
  return { options, help, json, doctor, repair, tombstone, rekey };
5882
6028
  }
@@ -5904,13 +6050,19 @@ function helpText() {
5904
6050
  " --replace When this machine is already wired to a different Haven agent on the bare",
5905
6051
  " haven / haven-signer pair, re-point that pair at the new agent and retire the",
5906
6052
  " previous agent directory locally (tombstoned, local key files removed).",
5907
- " Nothing is revoked \u2014 revoke the old agent on the Haven agent page. Without",
5908
- " this flag a non-interactive run REFUSES such a collision (wiring_collision)",
5909
- " and an interactive terminal is asked; --name installs alongside instead.",
6053
+ " Nothing is revoked \u2014 use Remove agent\u2026 on the Haven agent page. Implies",
6054
+ " the bare pair. Without it (or --bare) a non-interactive run REFUSES such a",
6055
+ " collision (wiring_collision) and an interactive terminal is asked.",
6056
+ " --bare Wire the bare haven / haven-signer pair. Without --name a setup now names",
6057
+ " its pair from the agent's display name (haven-<slug> /",
6058
+ " haven-signer-<slug>, de-collided against this machine, ending -dev on a",
6059
+ " non-production backend), so adding an agent never has to displace anyone;",
6060
+ " --bare is the explicit opt-in to the unnamed pair. --name overrides it.",
5910
6061
  " --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
5911
6062
  " MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
5912
6063
  " agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
5913
- " single hyphens; immutable once wired. Omit for the bare haven / haven-signer pair.",
6064
+ " single hyphens; immutable once wired. Without it the pair is named from the",
6065
+ " agent's display name; --bare wires the bare haven / haven-signer pair.",
5914
6066
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
5915
6067
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
5916
6068
  " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
@@ -6056,7 +6208,7 @@ async function runCli(argv, io = {
6056
6208
  io.stdout(redactSecrets(`Tombstoned agent ${info.agent_id} at ${parsed.tombstone.directory}.
6057
6209
  `));
6058
6210
  io.stdout(
6059
- "Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
6211
+ "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"
6060
6212
  );
6061
6213
  io.stdout(
6062
6214
  info.recordPath !== null ? `A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
@@ -6165,7 +6317,7 @@ async function runCli(argv, io = {
6165
6317
  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"
6166
6318
  );
6167
6319
  io.stdout(
6168
- "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"
6320
+ "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"
6169
6321
  );
6170
6322
  }
6171
6323
  return failures.length > 0 || retained ? 1 : 0;