@haven_ai/connect 0.0.0-dev.202609190647.18526db → 0.0.0-dev.202609190756.c419f60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -109,7 +109,7 @@ report stays `version: 1`; `level` is additive.
109
109
  | Level | Meaning | Example |
110
110
  | --- | --- | --- |
111
111
  | `ok` | Nothing to say. | The installed signer matches the connector's pin. |
112
- | `advisory` | Worth reading; nothing is broken. Exit 0. | `signer_runtime`: the install is intact but behind the pinned version ("intact, but outdated" — both versions named, `--repair` offered); `signer_runtime_unused` (#3123): runtime directories nothing references. `superseded_agents` on a recognised runtime with no config file the connector can read (Claude Code, `other`): a second live key is reported, and the check says why "wired" cannot be verified from this machine. |
112
+ | `advisory` | Worth reading; nothing is broken. Exit 0. | `signer_runtime`: the install is intact but behind the pinned version ("intact, but outdated" — both versions named, `--repair` offered); `signer_runtime_unused` (#3123): runtime directories nothing references; `mcp_server_name_rebound` (#3122): two local binding records claim one server name. `superseded_agents` on a recognised runtime with no config file the connector can read (Claude Code, `other`): a second live key is reported, and the check says why "wired" cannot be verified from this machine. |
113
113
  | `failed` | A real failure with one repair action. Exit 1. | A stale or empty runtime directory; a live key in a directory the runtime's config demonstrably does not use. |
114
114
 
115
115
  What stays blocking is live spend authority: a directory whose stored key
@@ -191,7 +191,9 @@ npx @haven_ai/connect@<channel> --unwire --name research [--reason "..."]
191
191
  touches local files only, so any published connector does the same job.
192
192
 
193
193
  It tombstone-first (so a stale long-lived host still hears `HAVEN-TOMBSTONE`,
194
- never a masked `ENOENT`), then removes THAT agent's hosted + signer pair from
194
+ never a masked `ENOENT`), releases the directory's local server-name binding
195
+ record (#3122, whatever the key-material decision below), then removes THAT
196
+ agent's hosted + signer pair from
195
197
  every runtime config it appears in (Hermes YAML, Codex TOML, the Cursor / VS
196
198
  Code / Insiders / Claude Desktop JSON configs), plus the Hermes dotenv API-key
197
199
  line — bare `MCP_HAVEN_API_KEY` or named `MCP_HAVEN_<SLUG>_API_KEY`. Then it
@@ -224,7 +226,8 @@ revoked or destroyed — that is the honest state. **`--destroy-key-material`**
224
226
  proceeds on every answer, states what it destroyed and that local recovery
225
227
  of a stranded balance ends with it. In `--json` the record carries an
226
228
  additive `teardown: { status: destroyed | retained | forced, probe, detail,
227
- remedy? }`.
229
+ remedy? }` and, since #3122, `binding_released` (whether a local
230
+ `mcp-server-binding.json` was removed).
228
231
 
229
232
  The `claude-code` copy of the key (written by `claude mcp add`, into a config
230
233
  the connector does not own) is out of scope for `--unwire`, as it always was:
@@ -271,6 +274,67 @@ revoked on the backend; `connect reports, the user decides` (#1688) survives,
271
274
  and revocation stays an owner action on the Haven agent page. Restart every
272
275
  long-lived host afterwards, as with any retirement.
273
276
 
277
+ ### The wallet warning arrives before the write; names are bound locally (#3122)
278
+
279
+ Setup guards the **name slot** (a taken `--name`, the bare pair already wired
280
+ — #2551 refuses or asks) but never the **wallet**: a machine can carry several
281
+ agents with live keys, and until #3122 the "your previous agent(s) still exist
282
+ with their own keys" heads-up was printed only after the credentials were
283
+ written (that completion heads-up, #1688, is unchanged — this slice ADDS an
284
+ earlier notice). Two things changed, both from local files only (no network call is
285
+ added; the backend is not asked whether any key still authenticates):
286
+
287
+ - **Before the key is minted or anything is written**, setup names every
288
+ other credential directory that still holds a stored key, with the account
289
+ it can spend from — `Heads-up (before anything is written): this machine
290
+ already carries N agent director(y/ies) with stored keys — <id> (spends
291
+ from 0x…)`. It **warns, it does not refuse** (owner decision on #3119): a
292
+ refusal would change behaviour for every non-interactive caller, including
293
+ the dashboard's connect flow. `--json` carries the same list as
294
+ `existing_agents_before_write: [{ agent_id, account_address }]`, always
295
+ present on a completed run (`[]` on a clean machine) — a subset of
296
+ `superseded_agent_ids`, which names every other directory that has an
297
+ `identity.json` at all, key-less and tombstoned ones included.
298
+ "Holds a stored key" is the whole test: a tombstoned directory whose key
299
+ files were never deleted is counted (it can still spend), which is the
300
+ opposite side of the doctor's rule that ignores a retired directory's
301
+ binding record (it launches nothing). The scan covers exactly the credential
302
+ root this run writes into — `~/.haven/agents`, or the root given by
303
+ `--credentials-dir` — and never unions the two (#2551's collision check uses
304
+ the same root; `--doctor` shares the no-union rule but reads its
305
+ `--credentials-dir` as the agent directory itself, so its root is that
306
+ directory's parent; #3123's union was for the one shared `signer-runtime`
307
+ root).
308
+ - **Each setup records what it bound** in a non-secret
309
+ `mcp-server-binding.json` beside `last-connect-outcome.json`: `{ version:
310
+ 1, server_name, signer_name, agent_id, api_url, hosted_mcp_url?, bound_at }`
311
+ — no key material, by construction (pinned by test). Per credential
312
+ directory, not machine-wide: two concurrent setups never write the same
313
+ file, and "who else claims this name" is answered by scanning the root,
314
+ which setup and the doctor already do. When a setup is about to bind a
315
+ server name another directory's record holds, it says so before the write —
316
+ previous agent (the NEWEST holder when several records claim the name),
317
+ previous backend URL, when — and flags a **DIFFERENT backend** explicitly, because that is the case the backend cannot see and
318
+ the one that silently repoints a saved session; `--json` carries it as
319
+ `server_name_rebound_from: { server_name, agent_id, api_url, bound_at,
320
+ backend_changed }`. `--unwire` releases the record (its `--json` gains
321
+ `binding_released`), and so does a `--replace` retirement, so a legitimately
322
+ free name does not warn forever; `--tombstone` leaves it, and `--doctor`
323
+ ignores a retired directory's record. `--doctor` reports two records
324
+ claiming one name — excluding tombstoned (retired) directories — as the
325
+ `mcp_server_name_rebound` advisory (oldest → newest, backend change
326
+ flagged). The record is written
327
+ after the credentials, best-effort: a directory with credentials and no
328
+ record (a crash in between, or one retired by hand) is not proof the name
329
+ is free — the absence of a warning is not a guarantee, as with
330
+ `superseded_agent_ids`.
331
+
332
+ **What wins when the local record disagrees with the backend.** The backend's
333
+ `agents.mcp_server_name` is the authority for the same backend; the local
334
+ record is a reporting aid that covers the two cases the backend cannot —
335
+ another backend, or offline — and it never asserts itself as the backend's
336
+ current truth. Every message that reads it says "locally recorded".
337
+
274
338
  ### Structured output for automation
275
339
 
276
340
  Pass `--json` when a launcher needs a machine-readable completion record. Connect
@@ -282,9 +346,12 @@ whenever ready and verify later with the read-only `haven_get_agent`
282
346
  tool. The object includes runtime/topology status,
283
347
  probe result, activation and next-action guidance, approval state/expiry (null
284
348
  when the backend does not provide an approval expiry) and `approval.url`, the two
285
- read-only verification tools, `hosted_mcp_url`, `superseded_agent_ids`, and —
286
- on a run that replaced existing wiring — `superseded_agents_retired_locally`
287
- with `retired_agent_ids`. It
349
+ read-only verification tools, `hosted_mcp_url`, `superseded_agent_ids`,
350
+ `existing_agents_before_write` (#3122 — the other live-keyed directories named
351
+ before the first write, each with the account it spends from), and — on a run
352
+ that replaced existing wiring — `superseded_agents_retired_locally` with
353
+ `retired_agent_ids`; `server_name_rebound_from` (#3122) appears only when the
354
+ run took a server name over from another directory's local binding record. It
288
355
  contains no API key, private key, credential
289
356
  contents, full credential paths, or full delegate address. The same redacted
290
357
  object is available to library callers as `runConnect(...).outcome`; the older
package/dist/cli.cjs CHANGED
@@ -172,6 +172,16 @@ function redactSecrets(value) {
172
172
  function redactForAutomation(value) {
173
173
  return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
174
174
  }
175
+ function withoutUserinfo(url) {
176
+ try {
177
+ const parsed = new URL(url);
178
+ parsed.username = "";
179
+ parsed.password = "";
180
+ return parsed.toString().replace(/\/+$/, "");
181
+ } catch {
182
+ return url;
183
+ }
184
+ }
175
185
  function shortAddress(address) {
176
186
  if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
177
187
  return `${address.slice(0, 6)}...${address.slice(-4)}`;
@@ -543,12 +553,54 @@ async function readConnectOutcomeRuntime(directory) {
543
553
  return null;
544
554
  }
545
555
  }
546
- var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME;
556
+ async function writeMcpServerBinding(directory, binding) {
557
+ const path$1 = path.join(directory, MCP_SERVER_BINDING_FILENAME);
558
+ await promises.writeFile(path$1, `${JSON.stringify(binding, null, 2)}
559
+ `, { mode: 384 });
560
+ return path$1;
561
+ }
562
+ async function readMcpServerBinding(directory) {
563
+ try {
564
+ const parsed = JSON.parse(await promises.readFile(path.join(directory, MCP_SERVER_BINDING_FILENAME), "utf8"));
565
+ if (typeof parsed !== "object" || parsed === null) return null;
566
+ const record = parsed;
567
+ if (record.version !== 1 || typeof record.server_name !== "string" || typeof record.agent_id !== "string" || typeof record.api_url !== "string" || typeof record.bound_at !== "string") return null;
568
+ return record;
569
+ } catch {
570
+ return null;
571
+ }
572
+ }
573
+ async function clearMcpServerBinding(directory) {
574
+ try {
575
+ await promises.rm(path.join(directory, MCP_SERVER_BINDING_FILENAME));
576
+ return true;
577
+ } catch {
578
+ return false;
579
+ }
580
+ }
581
+ async function listMcpServerBindings(baseDir, excludeDirectory) {
582
+ const root = defaultCredentialRoot(baseDir);
583
+ let entries = [];
584
+ try {
585
+ entries = await promises.readdir(root);
586
+ } catch {
587
+ return [];
588
+ }
589
+ const out = [];
590
+ for (const entry of entries) {
591
+ const directory = path.join(root, entry);
592
+ const binding = await readMcpServerBinding(directory);
593
+ if (binding) out.push({ directory, binding });
594
+ }
595
+ return out;
596
+ }
597
+ var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME, MCP_SERVER_BINDING_FILENAME;
547
598
  var init_storage = __esm({
548
599
  "src/storage.ts"() {
549
600
  REKEY_PENDING_FILENAME = "rekey-pending.json";
550
601
  REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
551
602
  CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
603
+ MCP_SERVER_BINDING_FILENAME = "mcp-server-binding.json";
552
604
  }
553
605
  });
554
606
  function mcpPackageSpec() {
@@ -567,9 +619,9 @@ var init_runtime_manifest = __esm({
567
619
  mcpPackage: "@haven_ai/mcp",
568
620
  mcpVersion: mcp.MCP_VERSION,
569
621
  sdkPackage: "@haven_ai/sdk",
570
- sdkVersion: "0.0.0-dev.202609190647.18526db",
622
+ sdkVersion: "0.0.0-dev.202609190756.c419f60",
571
623
  signerPackage: "@haven_ai/signer",
572
- signerVersion: "0.0.0-dev.202609190647.18526db",
624
+ signerVersion: "0.0.0-dev.202609190756.c419f60",
573
625
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
574
626
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
575
627
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -3195,9 +3247,10 @@ async function unwireAgent(input) {
3195
3247
  }
3196
3248
  }
3197
3249
  }
3250
+ const bindingReleased = await clearMcpServerBinding(input.directory);
3198
3251
  const teardown = await decideTeardown(identity, input);
3199
3252
  if (teardown.status !== "retained") await teardownLocalKeyMaterial(input.directory, identity);
3200
- return { directory: input.directory, agentId, slug, tombstoned, runtimes, teardown };
3253
+ return { directory: input.directory, agentId, slug, tombstoned, runtimes, teardown, bindingReleased };
3201
3254
  }
3202
3255
  async function decideTeardown(identity, input) {
3203
3256
  const ended = "Local recovery of a stranded delegate balance ends with it: the sweep-recovery routes accept only this agent's API key and delegate signature, and neither exists on this machine any more.";
@@ -4296,6 +4349,30 @@ async function runDoctor(input, deps = {}) {
4296
4349
  } : {}
4297
4350
  });
4298
4351
  }
4352
+ const bindings = (await Promise.all(
4353
+ inventory.filter((entry) => entry.classification !== "retired").map(async (entry) => ({ entry, binding: await readMcpServerBinding(entry.directory) }))
4354
+ )).filter((item) => item.binding !== null);
4355
+ const byName = /* @__PURE__ */ new Map();
4356
+ for (const item of bindings) {
4357
+ const list = byName.get(item.binding.server_name) ?? [];
4358
+ list.push(item);
4359
+ byName.set(item.binding.server_name, list);
4360
+ }
4361
+ const rebound = [...byName.entries()].filter(([, items]) => items.length > 1);
4362
+ if (rebound.length > 0) {
4363
+ const parts = rebound.map(([name, items]) => {
4364
+ const ordered = [...items].sort((a, b) => a.binding.bound_at < b.binding.bound_at ? -1 : a.binding.bound_at > b.binding.bound_at ? 1 : 0);
4365
+ const backends = new Set(ordered.map((i) => withoutUserinfo(i.binding.api_url)));
4366
+ return `'${name}': ` + ordered.map((i) => `${i.binding.agent_id} on ${withoutUserinfo(i.binding.api_url)} at ${i.binding.bound_at} [${i.entry.classification}]`).join(" \u2192 ") + (backends.size > 1 ? " (BACKEND CHANGED)" : "");
4367
+ });
4368
+ checks.push({
4369
+ id: "mcp_server_name_rebound",
4370
+ label: "MCP server names bound more than once",
4371
+ level: "advisory",
4372
+ detail: `${rebound.length} MCP server name${rebound.length === 1 ? "" : "s"} changed hands on this machine (locally recorded bindings; the backend's own record is the authority for the same backend): ${parts.join("; ")}. A saved session, script or document naming the server may still mean the earlier agent.`,
4373
+ repair: `Retire the earlier director(y/ies) with ${RERUN} --unwire <dir> to release the name, or keep both and address them by --name.`
4374
+ });
4375
+ }
4299
4376
  const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
4300
4377
  if (parkedElsewhere.length > 0) {
4301
4378
  const abandoned = parkedElsewhere.filter((item) => item.pending.state !== "pending");
@@ -4743,7 +4820,7 @@ async function pathExists2(path) {
4743
4820
  init_unwire();
4744
4821
  init_local_mcp_runtime();
4745
4822
  init_runtime_manifest();
4746
- var CONNECTOR_VERSION = "0.0.0-dev.202609190647.18526db";
4823
+ var CONNECTOR_VERSION = "0.0.0-dev.202609190756.c419f60";
4747
4824
  var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4748
4825
  var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
4749
4826
  function failureOutcomeFor(runtimeHint, error) {
@@ -4870,6 +4947,27 @@ async function executeConnect(options, deps, trace) {
4870
4947
  }
4871
4948
  }
4872
4949
  }
4950
+ const existingAgents = await listExistingKeyedAgents(options.credentialsDir);
4951
+ if (existingAgents.length > 0) {
4952
+ log("");
4953
+ log(
4954
+ `Heads-up (before anything is written to this machine): it already carries ${existingAgents.length} agent director${existingAgents.length === 1 ? "y" : "ies"} with stored keys \u2014 ` + existingAgents.map((a) => `${a.agentId}${a.accountAddress ? ` (spends from ${shortAddress(a.accountAddress)})` : ""}`).join(", ") + ". This setup creates a NEW agent alongside them and revokes nothing."
4955
+ );
4956
+ }
4957
+ const hostedNameForRun = serverNamesFor(serverName).hosted;
4958
+ let reboundFrom;
4959
+ const holders = (await listMcpServerBindings(options.credentialsDir)).filter(({ binding }) => binding.server_name === hostedNameForRun).sort((a, b) => a.binding.bound_at < b.binding.bound_at ? 1 : a.binding.bound_at > b.binding.bound_at ? -1 : 0);
4960
+ const newest = holders[0];
4961
+ if (newest) {
4962
+ const { binding, directory } = newest;
4963
+ const backendChanged = withoutUserinfo(binding.api_url) !== withoutUserinfo(options.apiBaseUrl);
4964
+ const previousApiUrl = withoutUserinfo(binding.api_url);
4965
+ reboundFrom = { server_name: binding.server_name, agent_id: binding.agent_id, api_url: previousApiUrl, bound_at: binding.bound_at, backend_changed: backendChanged };
4966
+ const beingReplaced = replacing?.superseded.some((entry) => entry.directory === directory) ?? false;
4967
+ log(
4968
+ `Heads-up: the MCP server name '${binding.server_name}' was bound to agent ${binding.agent_id} on ${previousApiUrl} at ${binding.bound_at} (locally recorded${holders.length > 1 ? `; ${holders.length - 1} older record${holders.length > 2 ? "s" : ""} also claim${holders.length > 2 ? "" : "s"} it` : ""}). ` + (beingReplaced ? `This run replaces that wiring, as you chose, with a new agent on ${withoutUserinfo(options.apiBaseUrl)}` : `This run rebinds it to a new agent on ${withoutUserinfo(options.apiBaseUrl)}`) + (backendChanged ? " \u2014 a DIFFERENT backend: any saved session, script or document naming this MCP server now resolves to a different backend and agent." : ".")
4969
+ );
4970
+ }
4873
4971
  const localKey = generateKey();
4874
4972
  const localApiKey = generateLocalApiKey();
4875
4973
  log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
@@ -4933,6 +5031,21 @@ async function executeConnect(options, deps, trace) {
4933
5031
  warn: log
4934
5032
  });
4935
5033
  trace.directory = credentialPaths.directory;
5034
+ try {
5035
+ const bindingRecord = {
5036
+ version: 1,
5037
+ server_name: hostedNameForRun,
5038
+ signer_name: serverNamesFor(serverName).signer,
5039
+ agent_id: registration.agent_id,
5040
+ api_url: withoutUserinfo(options.apiBaseUrl),
5041
+ // never persist `user:pass@` (#3154 doc review r2)
5042
+ ...registration.hosted_mcp_url ? { hosted_mcp_url: registration.hosted_mcp_url } : {},
5043
+ bound_at: (/* @__PURE__ */ new Date()).toISOString()
5044
+ };
5045
+ await writeMcpServerBinding(credentialPaths.directory, bindingRecord);
5046
+ } catch {
5047
+ log("Could not record the MCP server-name binding locally (non-fatal; the next setup cannot see this binding).");
5048
+ }
4936
5049
  log(`Stored Haven identity credential locally: ${credentialPaths.identityPath}`);
4937
5050
  log(`Stored local signer credential locally: ${credentialPaths.signerPath}`);
4938
5051
  log(`Stored non-secret agent orientation locally: ${credentialPaths.agentPath}`);
@@ -5012,6 +5125,7 @@ async function executeConnect(options, deps, trace) {
5012
5125
  replacedBy: registration.agent_id
5013
5126
  });
5014
5127
  await teardownLocalKeyMaterial(entry.directory, await readIdentityFile(entry.directory));
5128
+ await clearMcpServerBinding(entry.directory);
5015
5129
  retiredAgentIds.push(entry.agentId);
5016
5130
  log(`Retired previous agent ${entry.agentId} locally: tombstoned, local key files removed.`);
5017
5131
  } catch (err) {
@@ -5081,6 +5195,8 @@ async function executeConnect(options, deps, trace) {
5081
5195
  supersededAgentIds,
5082
5196
  supersededAgentsRetiredLocally,
5083
5197
  ...replacing ? { retiredAgentIds } : {},
5198
+ existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
5199
+ ...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
5084
5200
  setupChallengeExpiresAt: setup.challenge.expires_at,
5085
5201
  approvalRequired: registration.agent_status === "pending_approval",
5086
5202
  approvalUrl: registration.approval_url
@@ -5145,6 +5261,10 @@ function completionOutcome(input) {
5145
5261
  superseded_agent_ids: input.supersededAgentIds ?? [],
5146
5262
  ...input.supersededAgentsRetiredLocally !== void 0 ? { superseded_agents_retired_locally: input.supersededAgentsRetiredLocally } : {},
5147
5263
  ...input.retiredAgentIds ? { retired_agent_ids: input.retiredAgentIds } : {},
5264
+ // #3122: always emitted on a completed run (empty list included), for the
5265
+ // same reason as superseded_agent_ids above.
5266
+ existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
5267
+ ...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
5148
5268
  ...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
5149
5269
  ...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
5150
5270
  };
@@ -5420,6 +5540,32 @@ function activationInstructionWithWhy(profile) {
5420
5540
  return profile.activationInstruction;
5421
5541
  }
5422
5542
  var RERUN_HINT = sdk.connectorRerunCommand();
5543
+ async function listExistingKeyedAgents(baseDir) {
5544
+ const root = defaultCredentialRoot(baseDir);
5545
+ let entries = [];
5546
+ try {
5547
+ entries = await promises.readdir(root);
5548
+ } catch {
5549
+ return [];
5550
+ }
5551
+ const out = [];
5552
+ for (const entry of entries) {
5553
+ const directory = path.join(root, entry);
5554
+ try {
5555
+ const identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
5556
+ if (typeof identity.api_key !== "string" || identity.api_key.length === 0) continue;
5557
+ let agent = {};
5558
+ try {
5559
+ agent = JSON.parse(await promises.readFile(path.join(directory, "agent.json"), "utf8"));
5560
+ } catch {
5561
+ }
5562
+ const { accountAddress } = readStoredAccountAddress(identity, agent);
5563
+ out.push({ agentId: typeof identity.agent_id === "string" ? identity.agent_id : entry, directory, accountAddress: accountAddress ?? null });
5564
+ } catch {
5565
+ }
5566
+ }
5567
+ return out;
5568
+ }
5423
5569
  async function listOtherAgentIds(baseDir, currentDirectory) {
5424
5570
  const root = defaultCredentialRoot(baseDir);
5425
5571
  let entries = [];
@@ -5847,6 +5993,8 @@ async function runCli(argv, io = {
5847
5993
  status: r.status,
5848
5994
  ...r.detail ? { detail: r.detail } : {}
5849
5995
  })),
5996
+ // #3122: additive — whether the local server-name binding record was released.
5997
+ binding_released: result.bindingReleased,
5850
5998
  // #3123: additive — what happened to the key material and why.
5851
5999
  teardown: {
5852
6000
  status: result.teardown.status,
@@ -5869,6 +6017,7 @@ async function runCli(argv, io = {
5869
6017
  io.stdout(redactSecrets(` ${mark} ${r.label}: ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}
5870
6018
  `));
5871
6019
  }
6020
+ io.stdout(result.bindingReleased ? " \u2713 MCP server-name binding: released (the name is free for the next setup).\n" : " \u2013 MCP server-name binding: none recorded for this directory.\n");
5872
6021
  const t = result.teardown;
5873
6022
  io.stdout(redactSecrets(` ${t.status === "retained" ? "\u2717" : t.status === "forced" ? "!" : "\u2713"} Key material: ${t.status} (probe: ${t.probe}) \u2014 ${t.detail}
5874
6023
  `));