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