edge-book 0.13.0 → 0.15.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.
Files changed (3) hide show
  1. package/README.md +13 -2
  2. package/dist/edge-book.js +1152 -285
  3. package/package.json +1 -1
package/dist/edge-book.js CHANGED
@@ -5,18 +5,19 @@ import { realpathSync } from "fs";
5
5
  import { fileURLToPath } from "url";
6
6
 
7
7
  // src/cli-shared.ts
8
- import fs8 from "fs/promises";
9
- import path8 from "path";
8
+ import fs9 from "fs/promises";
9
+ import path10 from "path";
10
10
 
11
11
  // src/dialout.ts
12
- import crypto5 from "crypto";
12
+ import crypto6 from "crypto";
13
13
  import WebSocket from "ws";
14
14
 
15
15
  // src/dialout-key.ts
16
16
  import crypto from "crypto";
17
17
  import fs from "fs/promises";
18
18
  import path from "path";
19
- var KEY_FILE = "host-dialout-key.json";
19
+ var DIALOUT_KEY_FILE = "host-dialout-key.json";
20
+ var KEY_FILE = DIALOUT_KEY_FILE;
20
21
  var DEFAULT_PAIR_TTL_MS = 5 * 60 * 1e3;
21
22
  var PAIRING_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
22
23
  function now() {
@@ -98,7 +99,7 @@ async function createSessionsRevokeFrame(store) {
98
99
  }
99
100
 
100
101
  // src/edge-book.ts
101
- import path6 from "path";
102
+ import path7 from "path";
102
103
 
103
104
  // src/types.ts
104
105
  var EPHEMERAL_TTL_POLICY = {
@@ -195,12 +196,12 @@ async function readJsonl(file) {
195
196
 
196
197
  // src/handles.ts
197
198
  var HANDLE_SLUG = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
198
- var RESERVED_HANDLES = /* @__PURE__ */ new Set(["add", "healthz", "metrics", "agent", "api", "handle", "auth"]);
199
+ var RESERVED_HANDLES = /* @__PURE__ */ new Set(["add", "healthz", "metrics", "agent", "api", "handle", "auth", "directory"]);
199
200
  function isValidHandle(handle) {
200
201
  return HANDLE_SLUG.test(handle) && !RESERVED_HANDLES.has(handle);
201
202
  }
202
203
  function slugifyHandle(input) {
203
- return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30);
204
+ return input.trim().replace(/^@/, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30);
204
205
  }
205
206
 
206
207
  // src/crypto.ts
@@ -404,6 +405,9 @@ var CONTACT_MUTES_FILE = "contact-mutes.json";
404
405
  var REPORTS_FILE = "reports.json";
405
406
  var INVITE_CODES_FILE = "invite-codes.json";
406
407
  var INBOUND_RATE_FILE = "inbound-rate.json";
408
+ var OUTBOX_FILE = "outbox.json";
409
+ var EVENTS_FILE = "events.ndjson";
410
+ var SUPPORT_BUNDLES_FILE = "support-bundles.json";
407
411
  var ATTESTATIONS_FILE = "attestations.json";
408
412
  var ENDORSEMENTS_FILE = "endorsements.json";
409
413
  var SIGNALS_FILE = "signals.json";
@@ -1503,6 +1507,74 @@ async function expireEscalations(store) {
1503
1507
  if (changed) await store.saveEscalations(all);
1504
1508
  }
1505
1509
 
1510
+ // src/event-log.ts
1511
+ import crypto4 from "crypto";
1512
+ import fs6 from "fs/promises";
1513
+ import path6 from "path";
1514
+ var MAX_EVENT_LINES = 2e3;
1515
+ var COMPACT_KEEP_LINES = 1e3;
1516
+ function eventErrorCode(e) {
1517
+ if (e instanceof Error) {
1518
+ const code = e.code;
1519
+ if (typeof code === "string" && code.length > 0) return code;
1520
+ return e.constructor.name || "Error";
1521
+ }
1522
+ return "unknown_error";
1523
+ }
1524
+ async function logEvent(store, kind, fields = {}, caps = {}) {
1525
+ try {
1526
+ const event = { ts: (/* @__PURE__ */ new Date()).toISOString(), kind, ...fields };
1527
+ const file = store.file(EVENTS_FILE);
1528
+ await fs6.mkdir(path6.dirname(file), { recursive: true });
1529
+ await fs6.appendFile(file, `${JSON.stringify(event)}
1530
+ `, "utf8");
1531
+ await compactIfNeeded(file, caps.maxLines ?? MAX_EVENT_LINES, caps.keepLines ?? COMPACT_KEEP_LINES);
1532
+ } catch {
1533
+ }
1534
+ }
1535
+ async function compactIfNeeded(file, maxLines, keepLines) {
1536
+ const text = await fs6.readFile(file, "utf8");
1537
+ const lines = text.split("\n").filter((l) => l.length > 0);
1538
+ if (lines.length <= maxLines) return;
1539
+ const kept = lines.slice(-keepLines);
1540
+ const tmp = `${file}.tmp-${crypto4.randomBytes(6).toString("hex")}`;
1541
+ try {
1542
+ await fs6.writeFile(tmp, `${kept.join("\n")}
1543
+ `, "utf8");
1544
+ await fs6.rename(tmp, file);
1545
+ } catch (error) {
1546
+ await fs6.rm(tmp, { force: true }).catch(() => void 0);
1547
+ throw error;
1548
+ }
1549
+ }
1550
+ async function readEvents(store, limit) {
1551
+ let text;
1552
+ try {
1553
+ text = await fs6.readFile(store.file(EVENTS_FILE), "utf8");
1554
+ } catch (error) {
1555
+ if (error.code === "ENOENT") return [];
1556
+ return [];
1557
+ }
1558
+ const out = [];
1559
+ for (const line of text.split("\n")) {
1560
+ if (!line) continue;
1561
+ try {
1562
+ const parsed = JSON.parse(line);
1563
+ if (parsed && typeof parsed === "object" && typeof parsed.kind === "string") out.push(parsed);
1564
+ } catch {
1565
+ }
1566
+ }
1567
+ return limit !== void 0 && out.length > limit ? out.slice(-limit) : out;
1568
+ }
1569
+ async function lastEvent(store, kind) {
1570
+ const events = await readEvents(store);
1571
+ for (let i = events.length - 1; i >= 0; i--) {
1572
+ const e = events[i];
1573
+ if (kind.endsWith(".") ? e.kind.startsWith(kind) : e.kind === kind) return e;
1574
+ }
1575
+ return void 0;
1576
+ }
1577
+
1506
1578
  // src/store-friends.ts
1507
1579
  async function upsertContactFromCard(store, card, state) {
1508
1580
  validateCard(card);
@@ -1569,6 +1641,7 @@ async function setRelationship(store, peerAgentId, nextState, type, reason = "")
1569
1641
  const event = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
1570
1642
  await appendJsonl(store.file(RELATIONSHIP_EVENTS_FILE), event);
1571
1643
  await store.audit(`relationship.${type}`, peerAgentId, { previous, next: nextState, reason });
1644
+ await logEvent(store, "friend.state_changed", { peer: peerAgentId, previous, next: nextState, event_type: type });
1572
1645
  return event;
1573
1646
  }
1574
1647
  async function createFriendRequest(store, targetCard, note = "", inviteCode = "") {
@@ -1608,6 +1681,7 @@ async function receiveFriendRequest(store, envelope) {
1608
1681
  }
1609
1682
  const contact = await store.upsertContactFromCard(body.card, "request_received");
1610
1683
  await store.setRelationship(envelope.from_agent_id, "request_received", "FriendRequest", body.note);
1684
+ await logEvent(store, "friend.request_received", { from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
1611
1685
  await appendJsonl(store.file(INBOX_FILE), envelope);
1612
1686
  const existingApprovals = await store.approvals();
1613
1687
  const alreadyPending = Object.values(existingApprovals).some(
@@ -1651,6 +1725,7 @@ async function acceptFriend(store, peerAgentId, reason = "accepted") {
1651
1725
  if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
1652
1726
  if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked_peer", "Cannot accept a blocked peer");
1653
1727
  await store.setRelationship(peerAgentId, "friend", "Accept", reason);
1728
+ await logEvent(store, "friend.accepted", { peer: peerAgentId });
1654
1729
  const grant = await store.issueGrant(peerAgentId, ["message.friend", "feed.read.friends", "profile.read.friend", "escalation.raise"]);
1655
1730
  const card = await store.writeCard();
1656
1731
  const profile = await store.buildFriendProfile();
@@ -1828,8 +1903,8 @@ async function consumeInviteCode(store, code) {
1828
1903
  }
1829
1904
 
1830
1905
  // src/store-identity.ts
1831
- import crypto4 from "crypto";
1832
- import fs6 from "fs/promises";
1906
+ import crypto5 from "crypto";
1907
+ import fs7 from "fs/promises";
1833
1908
  async function init(store, input = {}) {
1834
1909
  await ensureHome(store.home);
1835
1910
  const existing = await readJson(store.file(IDENTITY_FILE), null);
@@ -1837,7 +1912,7 @@ async function init(store, input = {}) {
1837
1912
  await store.updateConfig({ direct_url: input.directUrl, relay_url: input.relayUrl });
1838
1913
  return existing;
1839
1914
  }
1840
- const { publicKey, privateKey } = crypto4.generateKeyPairSync("ed25519");
1915
+ const { publicKey, privateKey } = crypto5.generateKeyPairSync("ed25519");
1841
1916
  const public_key_pem = publicKey.export({ type: "spki", format: "pem" }).toString();
1842
1917
  const private_key_pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
1843
1918
  const identity = {
@@ -1897,16 +1972,21 @@ async function setProfile(store, input) {
1897
1972
  await store.audit("identity.update", identity.agent_id, { display_name: identity.display_name, profile_version: profile.profile_version });
1898
1973
  return identity;
1899
1974
  }
1900
- async function setHandle(store, handle) {
1975
+ async function setHandle(store, handle, opts = {}) {
1901
1976
  if (!isValidHandle(handle)) {
1902
1977
  throw new EdgeBookError("invalid_handle", `invalid_handle: must be 3-30 chars [a-z0-9-], not reserved: ${handle}`);
1903
1978
  }
1904
1979
  const identity = await store.identity();
1905
1980
  identity.handle = handle;
1981
+ if (opts.discoverable === false) {
1982
+ identity.handle_discoverable = false;
1983
+ } else {
1984
+ delete identity.handle_discoverable;
1985
+ }
1906
1986
  identity.updated_at = now2();
1907
1987
  await writeJson(store.file(IDENTITY_FILE), identity, 384);
1908
1988
  await store.writeCard();
1909
- await store.audit("identity.set_handle", identity.agent_id, { handle });
1989
+ await store.audit("identity.set_handle", identity.agent_id, { handle, discoverable: opts.discoverable !== false });
1910
1990
  return identity;
1911
1991
  }
1912
1992
  async function exportIdentity(store) {
@@ -1942,6 +2022,7 @@ async function updateConfig(store, input) {
1942
2022
  if (input.inbound_window_ms !== void 0) next.inbound_window_ms = input.inbound_window_ms;
1943
2023
  if (input.handle_nudge_at !== void 0) next.handle_nudge_at = input.handle_nudge_at;
1944
2024
  if (input.greeter_mode !== void 0) next.greeter_mode = input.greeter_mode;
2025
+ if (input.support_inbox !== void 0) next.support_inbox = input.support_inbox;
1945
2026
  if (input.greeter_welcome_object_id !== void 0) next.greeter_welcome_object_id = input.greeter_welcome_object_id;
1946
2027
  await writeJson(store.file(CONFIG_FILE), next);
1947
2028
  return next;
@@ -1973,7 +2054,7 @@ async function buildCard(store, cardUrl) {
1973
2054
  refresh_after: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString(),
1974
2055
  expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString()
1975
2056
  };
1976
- const card_hash = crypto4.createHash("sha256").update(canonicalize(unsigned)).digest("base64url");
2057
+ const card_hash = crypto5.createHash("sha256").update(canonicalize(unsigned)).digest("base64url");
1977
2058
  const withHash = { ...unsigned, card_hash };
1978
2059
  return { ...withHash, signature: signPayload(withHash, identity.private_key_pem) };
1979
2060
  }
@@ -1990,7 +2071,7 @@ async function buildHandleClaim(store) {
1990
2071
  const card = await loadCard(store.file(CARD_FILE));
1991
2072
  const claimed_at = Date.now();
1992
2073
  const claim_sig = signPayload({ handle: identity.handle, agent_did: identity.agent_id, claimed_at }, identity.private_key_pem);
1993
- return { handle: identity.handle, agent_did: identity.agent_id, card, claimed_at, claim_sig };
2074
+ return { handle: identity.handle, agent_did: identity.agent_id, card, claimed_at, claim_sig, discoverable: identity.handle_discoverable !== false };
1994
2075
  }
1995
2076
  async function buildFriendProfile(store) {
1996
2077
  const identity = await store.identity();
@@ -2018,7 +2099,7 @@ async function doctor(store) {
2018
2099
  const files = {};
2019
2100
  for (const name of requiredFiles) {
2020
2101
  try {
2021
- const stat = await fs6.stat(store.file(name));
2102
+ const stat = await fs7.stat(store.file(name));
2022
2103
  files[name] = {
2023
2104
  exists: true,
2024
2105
  mode: `0${(stat.mode & 511).toString(8)}`
@@ -2118,6 +2199,79 @@ async function exportLocalData(store) {
2118
2199
  };
2119
2200
  }
2120
2201
 
2202
+ // src/store-support.ts
2203
+ var SUPPORT_BUNDLE_MAX_BYTES = 256 * 1024;
2204
+ var SUPPORT_BUNDLE_KEEP = 200;
2205
+ function pruneSupportBundles(all, keepId) {
2206
+ const records = Object.values(all);
2207
+ if (records.length <= SUPPORT_BUNDLE_KEEP) return all;
2208
+ const byAge = (a, b) => a.received_at.localeCompare(b.received_at);
2209
+ const evictable = [
2210
+ ...records.filter((r) => r.status === "dismissed" && r.bundle_id !== keepId).sort(byAge),
2211
+ ...records.filter((r) => r.status !== "dismissed" && r.bundle_id !== keepId).sort(byAge)
2212
+ ];
2213
+ for (const r of evictable) {
2214
+ if (Object.keys(all).length <= SUPPORT_BUNDLE_KEEP) break;
2215
+ delete all[r.bundle_id];
2216
+ }
2217
+ return all;
2218
+ }
2219
+ async function supportBundles(store) {
2220
+ return readJson(store.file(SUPPORT_BUNDLES_FILE), {});
2221
+ }
2222
+ async function saveSupportBundles(store, all) {
2223
+ await writeJson(store.file(SUPPORT_BUNDLES_FILE), all);
2224
+ }
2225
+ async function receiveSupportBundle(store, envelope) {
2226
+ if ((await store.config()).support_inbox !== true) {
2227
+ throw new EdgeBookError("support_inbox_disabled", "This agent does not accept support bundles (operator opt-in: edge-book support inbox --on)");
2228
+ }
2229
+ if (envelope.type !== "support_bundle") throw new EdgeBookError("wrong_message_type", "Expected support_bundle envelope");
2230
+ const size = Buffer.byteLength(JSON.stringify(envelope), "utf8");
2231
+ if (size > SUPPORT_BUNDLE_MAX_BYTES) {
2232
+ throw new EdgeBookError("support_bundle_too_large", `Support bundle is ${size} bytes; the receiver cap is ${SUPPORT_BUNDLE_MAX_BYTES} (256 KiB)`);
2233
+ }
2234
+ await store.enforceInboundRate(envelope.from_agent_id);
2235
+ await store.verifyEnvelope(envelope);
2236
+ const body = envelope.body;
2237
+ if (!body || typeof body !== "object" || !body.report || typeof body.report !== "object") {
2238
+ throw new EdgeBookError("bad_support_bundle", "support_bundle envelope carries no report");
2239
+ }
2240
+ validateCard(body.card);
2241
+ if (body.card.agent_id !== envelope.from_agent_id) {
2242
+ throw new EdgeBookError("agent_id_mismatch", "Embedded card does not match the envelope sender");
2243
+ }
2244
+ const record = {
2245
+ bundle_id: envelope.message_id,
2246
+ from_agent_id: envelope.from_agent_id,
2247
+ ...body.card.display_name ? { from_display_name: body.card.display_name } : {},
2248
+ ...envelope.trace_id ? { trace_id: envelope.trace_id } : {},
2249
+ received_at: now2(),
2250
+ status: "pending",
2251
+ report: body.report,
2252
+ ...typeof body.note === "string" && body.note ? { note: body.note } : {}
2253
+ };
2254
+ const all = await supportBundles(store);
2255
+ all[record.bundle_id] = record;
2256
+ await saveSupportBundles(store, pruneSupportBundles(all, record.bundle_id));
2257
+ await store.audit("support.receive", envelope.from_agent_id, { bundle_id: record.bundle_id, trace_id: envelope.trace_id ?? "" });
2258
+ await logEvent(store, "support.received", { from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
2259
+ return record;
2260
+ }
2261
+ async function pendingSupportBundles(store) {
2262
+ const all = await supportBundles(store);
2263
+ return Object.values(all).filter((b) => b.status === "pending").sort((a, b) => a.received_at.localeCompare(b.received_at));
2264
+ }
2265
+ async function setSupportBundleStatus(store, bundleId, status) {
2266
+ const all = await supportBundles(store);
2267
+ const record = all[bundleId];
2268
+ if (!record) throw new EdgeBookError("unknown_bundle", `Unknown support bundle: ${bundleId}`);
2269
+ record.status = status;
2270
+ all[bundleId] = record;
2271
+ await saveSupportBundles(store, all);
2272
+ return record;
2273
+ }
2274
+
2121
2275
  // src/store-trust.ts
2122
2276
  async function enforceInboundRate(store, peerAgentId) {
2123
2277
  const config = await store.config();
@@ -2244,33 +2398,42 @@ async function assertGrantSignature(store, grant) {
2244
2398
  }
2245
2399
  async function signEnvelope(store, input) {
2246
2400
  const identity = await store.identity();
2401
+ const { expires_at, ...rest } = input;
2247
2402
  const unsigned = {
2248
2403
  message_id: randomId("msg"),
2249
2404
  from_agent_id: identity.agent_id,
2250
2405
  created_at: now2(),
2251
- expires_at: new Date(Date.now() + 10 * 60 * 1e3).toISOString(),
2252
- ...input
2406
+ expires_at: expires_at ?? new Date(Date.now() + 10 * 60 * 1e3).toISOString(),
2407
+ ...rest,
2408
+ // Trace correlation (ea-claude-138): every new outbound envelope carries a
2409
+ // trace_id INSIDE the signed payload (tamper-evident; back-compat — old
2410
+ // receivers canonicalize whatever fields they parsed, so verification
2411
+ // still passes). Callers may pass an existing trace_id to chain a flow;
2412
+ // it is clamped to 128 chars (mirrors the host frame rule) and an empty
2413
+ // string falls back to a generated id.
2414
+ trace_id: input.trace_id?.slice(0, 128) || randomId("trace")
2253
2415
  };
2254
2416
  return { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
2255
2417
  }
2418
+ function embeddedCardKey(envelope) {
2419
+ if (envelope.type !== "friend_request" && envelope.type !== "friend_response" && envelope.type !== "support_bundle") return void 0;
2420
+ const card = envelope.body.card;
2421
+ return card?.public_keys?.[0]?.public_key_pem;
2422
+ }
2256
2423
  async function verifyEnvelope(store, envelope) {
2257
2424
  const identity = await store.identity();
2258
2425
  if (envelope.to_agent_id !== identity.agent_id) throw new EdgeBookError("wrong_recipient", "Envelope recipient does not match local identity");
2259
2426
  if (Date.parse(envelope.expires_at) <= Date.now()) throw new EdgeBookError("expired_message", "Message is expired");
2260
2427
  const seen = await readJson(store.file(SEEN_MESSAGES_FILE), []);
2261
- if (seen.includes(envelope.message_id)) throw new EdgeBookError("replay", `Replay detected for ${envelope.message_id}`);
2262
- const contacts = await store.contacts();
2263
- let publicKey = contacts[envelope.from_agent_id]?.public_keys?.[0]?.public_key_pem;
2264
- if (!publicKey && envelope.type === "friend_request") {
2265
- const card = envelope.body.card;
2266
- publicKey = card?.public_keys?.[0]?.public_key_pem;
2267
- }
2268
- if (!publicKey && envelope.type === "friend_response") {
2269
- const card = envelope.body.card;
2270
- publicKey = card?.public_keys?.[0]?.public_key_pem;
2428
+ if (seen.includes(envelope.message_id)) {
2429
+ await logEvent(store, "envelope.dedup_hit", { envelope_kind: envelope.type, from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
2430
+ throw new EdgeBookError("replay", `Replay detected for ${envelope.message_id}`);
2271
2431
  }
2432
+ const contacts = await store.contacts();
2433
+ const publicKey = contacts[envelope.from_agent_id]?.public_keys?.[0]?.public_key_pem ?? embeddedCardKey(envelope);
2272
2434
  if (!publicKey) throw new EdgeBookError("unknown_key", `Unknown sender key for ${envelope.from_agent_id}`);
2273
2435
  if (!verifyPayload(withoutSignature(envelope), envelope.signature, publicKey)) {
2436
+ await logEvent(store, "envelope.signature_failed", { envelope_kind: envelope.type, from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
2274
2437
  throw new EdgeBookError("invalid_signature", "Message signature is invalid");
2275
2438
  }
2276
2439
  seen.push(envelope.message_id);
@@ -2317,6 +2480,7 @@ async function revokeSession(store, sessionId) {
2317
2480
  }
2318
2481
  async function receiveEnvelope(store, envelope) {
2319
2482
  if (envelope.type === "friend_request") return store.receiveFriendRequest(envelope);
2483
+ if (envelope.type === "support_bundle") return receiveSupportBundle(store, envelope);
2320
2484
  if (envelope.type === "friend_response") return store.applyFriendResponse(envelope);
2321
2485
  if (envelope.type === "privileged_message") return store.receivePrivilegedMessage(envelope);
2322
2486
  if (envelope.type === "object_share") {
@@ -2416,6 +2580,17 @@ var NOTIFY_POLICIES = {
2416
2580
  message: `${esc?.subject ?? "A decision is needed"} \u2014 ${esc?.body ?? ""}${opts}`,
2417
2581
  dedup_key: env.message_id
2418
2582
  };
2583
+ },
2584
+ support_bundle: async (env) => {
2585
+ const body = env.body;
2586
+ const name = body.card?.display_name || env.from_agent_id;
2587
+ return {
2588
+ kind: "support_bundle",
2589
+ from_id: env.from_agent_id,
2590
+ from_name: body.card?.display_name,
2591
+ message: `${name} sent a support bundle (ref ${env.trace_id ?? env.message_id}). Review: edge-book support pending`,
2592
+ dedup_key: env.message_id
2593
+ };
2419
2594
  }
2420
2595
  };
2421
2596
  async function notificationIntent(store, envelope) {
@@ -2437,6 +2612,14 @@ async function recordNotified(store, dedupKey) {
2437
2612
  ledger.push(dedupKey);
2438
2613
  await writeJson(store.file(NOTIFIED_FILE), ledger);
2439
2614
  }
2615
+ function buildPairCompleteNotifyIntent(deviceId, label) {
2616
+ return {
2617
+ kind: "pair_complete",
2618
+ message: `Pairing complete \u2014 your reader is connected (device: ${label}).`,
2619
+ from_id: deviceId,
2620
+ dedup_key: deviceId
2621
+ };
2622
+ }
2440
2623
 
2441
2624
  // src/edge-book.ts
2442
2625
  var EdgeBookStore = class {
@@ -2445,7 +2628,7 @@ var EdgeBookStore = class {
2445
2628
  this.home = resolveHome(options.home);
2446
2629
  }
2447
2630
  file(name) {
2448
- return path6.join(this.home, name);
2631
+ return path7.join(this.home, name);
2449
2632
  }
2450
2633
  async init(input = {}) {
2451
2634
  return init(this, input);
@@ -2464,8 +2647,8 @@ var EdgeBookStore = class {
2464
2647
  return setProfile(this, input);
2465
2648
  }
2466
2649
  // Set a user-chosen unique handle. Re-signs the card; does NOT rotate keys.
2467
- async setHandle(handle) {
2468
- return setHandle(this, handle);
2650
+ async setHandle(handle, opts) {
2651
+ return setHandle(this, handle, opts);
2469
2652
  }
2470
2653
  // Portable identity bundle (the DID keypair + chosen handle). Carry to a new
2471
2654
  // device → same DID → relay handle keeps resolving to you (spec-096).
@@ -2981,16 +3164,17 @@ function nextAction(result, target) {
2981
3164
  return first ? `candidates list # then: friend request ${first.candidate_id}` : "candidates list";
2982
3165
  }
2983
3166
  default:
2984
- return "(no match \u2014 check the target)";
3167
+ return "not found \u2014 share your invite link so they can add you: card invite (use the deeplink_url)";
2985
3168
  }
2986
3169
  }
2987
3170
  var localContactProvider = {
2988
3171
  name: "local",
2989
3172
  priority: 100,
2990
3173
  async resolve(store, target) {
3174
+ const normalized = target.trim().replace(/^@/, "").toLowerCase();
2991
3175
  const contacts = await store.contacts();
2992
3176
  const match = Object.values(contacts).find(
2993
- (c) => c.peer_agent_id === target || c.aliases.includes(target) || c.display_name === target
3177
+ (c) => c.peer_agent_id === target || c.aliases.some((a) => a.toLowerCase() === normalized) || c.display_name.toLowerCase() === normalized
2994
3178
  );
2995
3179
  if (!match) return null;
2996
3180
  return {
@@ -3056,11 +3240,12 @@ async function writeCandidate(store, input) {
3056
3240
  await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
3057
3241
  return candidate;
3058
3242
  }
3243
+ var DEFAULT_RELAY_BASE = "https://edge-book-host.fly.dev";
3059
3244
  function defaultProviders(relayBase) {
3245
+ const base = relayBase ?? process.env["EDGE_BOOK_RELAY_BASE"] ?? DEFAULT_RELAY_BASE;
3060
3246
  const lookup = async (target) => {
3061
- if (!relayBase) return null;
3062
3247
  const slug = target.startsWith("registry:") ? target.slice("registry:".length) : target;
3063
- return `${relayBase.replace(/\/$/, "")}/handle/${encodeURIComponent(slug)}`;
3248
+ return `${base.replace(/\/$/, "")}/handle/${encodeURIComponent(slug)}`;
3064
3249
  };
3065
3250
  return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(lookup)];
3066
3251
  }
@@ -3108,15 +3293,22 @@ async function promoteCandidate(store, candidateId, note = "") {
3108
3293
  return envelope;
3109
3294
  }
3110
3295
  var HANDLE_SLUG2 = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
3296
+ function normalizeRegistryTarget(target) {
3297
+ if (target.startsWith("registry:")) {
3298
+ return "registry:" + target.slice("registry:".length).trim().toLowerCase().replace(/^@/, "");
3299
+ }
3300
+ return target.trim().toLowerCase().replace(/^@/, "");
3301
+ }
3111
3302
  function makeRegistryProvider(lookup) {
3112
3303
  return {
3113
3304
  name: "registry",
3114
3305
  priority: 50,
3115
3306
  async resolve(_store, target) {
3116
- const isExplicit = target.startsWith("registry:");
3117
- const slug = isExplicit ? target.slice("registry:".length) : target;
3118
- if (!isExplicit && !HANDLE_SLUG2.test(slug)) return null;
3119
- const cardTarget = await lookup(target);
3307
+ const slug = normalizeRegistryTarget(target);
3308
+ const isExplicit = slug.startsWith("registry:");
3309
+ const bareSlug = isExplicit ? slug.slice("registry:".length) : slug;
3310
+ if (!isExplicit && !HANDLE_SLUG2.test(bareSlug)) return null;
3311
+ const cardTarget = await lookup(slug);
3120
3312
  if (!cardTarget) return null;
3121
3313
  let card;
3122
3314
  try {
@@ -4465,7 +4657,9 @@ async function handleOwnerApi(req, res, url, adapters) {
4465
4657
  const card = await store.buildCard();
4466
4658
  const identity = await store.identity();
4467
4659
  const invite_url = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
4468
- sendJson(res, 200, { agent_id: identity.agent_id, display_name: identity.display_name, card_url: card.card_url, card, invite_url });
4660
+ const origin = card.card_url ? new URL(card.card_url).origin : "https://edge-book-host.fly.dev";
4661
+ const deeplink_url = `${origin}/add#i=${encodeURIComponent(invite_url)}`;
4662
+ sendJson(res, 200, { agent_id: identity.agent_id, display_name: identity.display_name, card_url: card.card_url, card, invite_url, deeplink_url });
4469
4663
  return true;
4470
4664
  }
4471
4665
  const attachmentMatch = /^\/api\/shared-objects\/([^/]+)\/attachment$/.exec(url.pathname);
@@ -4747,6 +4941,95 @@ function requestBody(frame, method) {
4747
4941
  return Buffer.from(JSON.stringify(frame.body ?? {}), "utf8");
4748
4942
  }
4749
4943
 
4944
+ // src/dialout-pair.ts
4945
+ var PairCompleteWaiter = class {
4946
+ resolve = null;
4947
+ timer = null;
4948
+ // Called by EdgeBookDialoutClient.handleMessage when type === "pair_complete".
4949
+ onFrame(frame) {
4950
+ if (!this.resolve) return;
4951
+ const cb = this.resolve;
4952
+ this.resolve = null;
4953
+ if (this.timer !== null) {
4954
+ clearTimeout(this.timer);
4955
+ this.timer = null;
4956
+ }
4957
+ cb({ device_id: frame.device_id ?? "", label: frame.label ?? "" });
4958
+ }
4959
+ // Wait up to ttlMs for a pair_complete frame from the host.
4960
+ // Returns null on timeout — old-host degradation (spec-135 §C.2).
4961
+ wait(ttlMs) {
4962
+ return new Promise((resolve) => {
4963
+ this.resolve = resolve;
4964
+ this.timer = setTimeout(() => {
4965
+ if (this.resolve) {
4966
+ this.resolve = null;
4967
+ resolve(null);
4968
+ }
4969
+ this.timer = null;
4970
+ }, ttlMs);
4971
+ });
4972
+ }
4973
+ };
4974
+
4975
+ // src/dialout-oneshot.ts
4976
+ async function sendPairRegistration(options) {
4977
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
4978
+ await client.start();
4979
+ await new Promise((resolve) => setTimeout(resolve, 0));
4980
+ const registration = await client.pair(options.ttlMs ?? DEFAULT_PAIR_TTL_MS);
4981
+ await client.stop();
4982
+ return registration;
4983
+ }
4984
+ async function deliverEnvelopeViaMailbox(options) {
4985
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
4986
+ await client.start();
4987
+ await new Promise((resolve) => setTimeout(resolve, 0));
4988
+ try {
4989
+ return await client.sendEnvelope(options.envelope);
4990
+ } finally {
4991
+ await client.stop();
4992
+ }
4993
+ }
4994
+ async function sendSessionsRevoke(options) {
4995
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
4996
+ await client.start();
4997
+ await new Promise((resolve) => setTimeout(resolve, 0));
4998
+ const { frame, ack } = await client.revokeSessionsAndWait();
4999
+ await client.stop();
5000
+ return { ...frame, channel_id: ack.channel_id };
5001
+ }
5002
+ async function listSessions(options) {
5003
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5004
+ await client.start();
5005
+ await new Promise((resolve) => setTimeout(resolve, 0));
5006
+ try {
5007
+ return await client.listSessionsAndWait();
5008
+ } finally {
5009
+ await client.stop();
5010
+ }
5011
+ }
5012
+ async function revokeOneSession(options) {
5013
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5014
+ await client.start();
5015
+ await new Promise((resolve) => setTimeout(resolve, 0));
5016
+ try {
5017
+ return await client.revokeOneSessionAndWait(options.deviceId);
5018
+ } finally {
5019
+ await client.stop();
5020
+ }
5021
+ }
5022
+ async function mailboxStatus(options) {
5023
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5024
+ await client.start();
5025
+ await new Promise((resolve) => setTimeout(resolve, 0));
5026
+ try {
5027
+ return await client.mailboxStatusAndWait(options.ids, options.timeoutMs ?? 5e3);
5028
+ } finally {
5029
+ await client.stop();
5030
+ }
5031
+ }
5032
+
4750
5033
  // src/dialout.ts
4751
5034
  var DEFAULT_DIALOUT_HOST = "wss://edge-book-host.fly.dev/agent/ws";
4752
5035
  var DEFAULT_HEARTBEAT_MS = 25e3;
@@ -4778,9 +5061,12 @@ var EdgeBookDialoutClient = class {
4778
5061
  currentBackoff;
4779
5062
  opened;
4780
5063
  pendingSessionRevokes = /* @__PURE__ */ new Map();
4781
- // Generic request_id-keyed RPC waiters for sessions_list / session_revoke_one.
5064
+ // Generic request_id-keyed RPC waiters (sessions_list / session_revoke_one /
5065
+ // mailbox_status). rpcType lets an old host's {type:"error", ref:"<type>"}
5066
+ // frame — which carries NO request_id — reject pending requests of that type.
4782
5067
  pendingRpc = /* @__PURE__ */ new Map();
4783
5068
  pendingMailboxSends = /* @__PURE__ */ new Map();
5069
+ pairCompleteWaiter = new PairCompleteWaiter();
4784
5070
  constructor(options) {
4785
5071
  this.options = {
4786
5072
  heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
@@ -4814,6 +5100,11 @@ var EdgeBookDialoutClient = class {
4814
5100
  this.send(registration.frame);
4815
5101
  return registration;
4816
5102
  }
5103
+ // Wait up to ttlMs for a pair_complete frame from the host.
5104
+ // Returns null on timeout — old-host degradation (spec-135).
5105
+ waitForPairComplete(ttlMs) {
5106
+ return this.pairCompleteWaiter.wait(ttlMs);
5107
+ }
4817
5108
  async revokeSessions() {
4818
5109
  const frame = await createSessionsRevokeFrame(this.store);
4819
5110
  this.send(frame);
@@ -4829,16 +5120,29 @@ var EdgeBookDialoutClient = class {
4829
5120
  const frame = await this.rpc("session_revoke_one", { device_id }, "session_revoke_one_ok", timeoutMs);
4830
5121
  return Boolean(frame.revoked);
4831
5122
  }
5123
+ // Ask the host for per-message delivery state (spec-097). Returns null when
5124
+ // the host predates receipts — detected by the unknown-type error frame
5125
+ // (fast path) or the RPC timeout (lost-frame path); both degrade the same.
5126
+ async mailboxStatusAndWait(ids, timeoutMs = 5e3) {
5127
+ try {
5128
+ const frame = await this.rpc("mailbox_status", { ids }, "mailbox_status_ok", timeoutMs);
5129
+ if (frame.type === "mailbox_status_err") throw new EdgeBookError("mailbox_status_failed", String(frame.error || "mailbox_status rejected"));
5130
+ return frame.statuses ?? [];
5131
+ } catch (error) {
5132
+ if (error instanceof EdgeBookError && (error.code === "host_rpc_timeout" || error.code === "host_unsupported_rpc")) return null;
5133
+ throw error;
5134
+ }
5135
+ }
4832
5136
  // Small request/response helper over the dial-out socket, correlated by
4833
5137
  // request_id. `expect` documents the ack type; resolution is by request_id.
4834
5138
  async rpc(type, extra, expect, timeoutMs) {
4835
- const request_id = crypto5.randomUUID();
5139
+ const request_id = crypto6.randomUUID();
4836
5140
  const promise = new Promise((resolve, reject) => {
4837
5141
  const timer = setTimeout(() => {
4838
5142
  this.pendingRpc.delete(request_id);
4839
5143
  reject(new EdgeBookError("host_rpc_timeout", `Timed out waiting for ${expect}`));
4840
5144
  }, timeoutMs);
4841
- this.pendingRpc.set(request_id, { resolve, reject, timer });
5145
+ this.pendingRpc.set(request_id, { resolve, reject, timer, rpcType: type });
4842
5146
  });
4843
5147
  this.send({ type, request_id, ...extra });
4844
5148
  return promise;
@@ -4857,9 +5161,10 @@ var EdgeBookDialoutClient = class {
4857
5161
  }
4858
5162
  // ── Mailbox transport (Contract 1 / ea-claude-065) ─────────────────────────
4859
5163
  // Low-level: hand an opaque blob to the host for delivery to `to` (a peer DID
4860
- // or channel_id). Resolves with the host-assigned message id once enqueued.
4861
- async sendMailbox(to, blob, timeoutMs = 5e3) {
4862
- const request_id = crypto5.randomUUID();
5164
+ // or channel_id). Resolves with the host-assigned message id once enqueued,
5165
+ // plus recipient_live when the host supports receipts (spec-097).
5166
+ async sendMailbox(to, blob, timeoutMs = 5e3, trace_id) {
5167
+ const request_id = crypto6.randomUUID();
4863
5168
  const blob_b64 = Buffer.from(blob).toString("base64");
4864
5169
  const ack = new Promise((resolve, reject) => {
4865
5170
  const timer = setTimeout(() => {
@@ -4868,14 +5173,16 @@ var EdgeBookDialoutClient = class {
4868
5173
  }, timeoutMs);
4869
5174
  this.pendingMailboxSends.set(request_id, { resolve, reject, timer });
4870
5175
  });
4871
- this.send({ type: "mailbox_send", request_id, to, blob_b64 });
5176
+ this.send({ type: "mailbox_send", request_id, to, blob_b64, ...trace_id ? { trace_id } : {} });
4872
5177
  return ack;
4873
5178
  }
4874
5179
  // High-level: deliver a signed envelope to its recipient (envelope.to_agent_id;
4875
5180
  // the host resolves the DID to a channel). Used to route friend requests,
4876
5181
  // object shares, and revokes through the mailbox instead of a manual file hop.
4877
5182
  async sendEnvelope(envelope) {
4878
- return this.sendMailbox(envelope.to_agent_id, Buffer.from(JSON.stringify(envelope), "utf8"));
5183
+ const ack = await this.sendMailbox(envelope.to_agent_id, Buffer.from(JSON.stringify(envelope), "utf8"), 5e3, envelope.trace_id);
5184
+ await logEvent(this.store, "envelope.sent", { envelope_kind: envelope.type, to: envelope.to_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
5185
+ return ack;
4879
5186
  }
4880
5187
  async handleMailboxDeliver(frame) {
4881
5188
  let envelope;
@@ -4893,8 +5200,16 @@ var EdgeBookDialoutClient = class {
4893
5200
  }
4894
5201
  }
4895
5202
  } catch (e) {
4896
- error = e instanceof Error ? e.message : String(e);
4897
- }
5203
+ error = eventErrorCode(e);
5204
+ }
5205
+ await logEvent(this.store, "envelope.received", {
5206
+ envelope_kind: envelope?.type ?? "unparseable",
5207
+ from: frame.from,
5208
+ dedup_key: envelope?.message_id,
5209
+ trace_id: envelope?.trace_id,
5210
+ applied,
5211
+ ...error ? { error } : {}
5212
+ });
4898
5213
  if (!this.mailboxQueue) this.send({ type: "mailbox_ack", id: frame.id });
4899
5214
  if (envelope) await this.options.onEnvelope?.(envelope, { applied, error });
4900
5215
  }
@@ -4942,7 +5257,7 @@ var EdgeBookDialoutClient = class {
4942
5257
  agent_key: key.agent_key,
4943
5258
  agent_did: identity.agent_id,
4944
5259
  version: "0.1.0",
4945
- nonce: crypto5.randomUUID()
5260
+ nonce: crypto6.randomUUID()
4946
5261
  });
4947
5262
  } catch (error) {
4948
5263
  this.opened = void 0;
@@ -4955,6 +5270,7 @@ var EdgeBookDialoutClient = class {
4955
5270
  });
4956
5271
  addSocketListener(socket, "close", () => {
4957
5272
  if (this.heartbeat) clearInterval(this.heartbeat);
5273
+ void logEvent(this.store, "dialout.disconnected", { host: this.options.host, stopped: this.stopped });
4958
5274
  if (!this.stopped && this.options.reconnect) this.scheduleReconnect();
4959
5275
  });
4960
5276
  await opened;
@@ -4963,6 +5279,7 @@ var EdgeBookDialoutClient = class {
4963
5279
  if (this.stopped) return;
4964
5280
  const delay = this.currentBackoff;
4965
5281
  this.currentBackoff = Math.min(MAX_BACKOFF_MS, Math.round(this.currentBackoff * 1.7));
5282
+ void logEvent(this.store, "dialout.reconnect_scheduled", { host: this.options.host, delay_ms: delay });
4966
5283
  this.reconnectTimer = setTimeout(() => {
4967
5284
  void this.connect();
4968
5285
  }, delay);
@@ -4976,6 +5293,7 @@ var EdgeBookDialoutClient = class {
4976
5293
  if (frame.type === "hello_ok") {
4977
5294
  this.opened?.resolve();
4978
5295
  this.opened = void 0;
5296
+ void logEvent(this.store, "dialout.connected", { host: this.options.host });
4979
5297
  try {
4980
5298
  const identity = await this.store.identity();
4981
5299
  if (shouldClaimHandle(identity.handle)) {
@@ -4986,7 +5304,8 @@ var EdgeBookDialoutClient = class {
4986
5304
  handle: claim.handle,
4987
5305
  card: claim.card,
4988
5306
  claimed_at: claim.claimed_at,
4989
- claim_sig: claim.claim_sig
5307
+ claim_sig: claim.claim_sig,
5308
+ discoverable: claim.discoverable
4990
5309
  });
4991
5310
  }
4992
5311
  } catch {
@@ -5003,7 +5322,7 @@ var EdgeBookDialoutClient = class {
5003
5322
  this.send({ type: "pong" });
5004
5323
  return;
5005
5324
  }
5006
- if (frame.type === "sessions_list_ok" || frame.type === "session_revoke_one_ok") {
5325
+ if (frame.type === "sessions_list_ok" || frame.type === "session_revoke_one_ok" || frame.type === "mailbox_status_ok" || frame.type === "mailbox_status_err") {
5007
5326
  const ack = frame;
5008
5327
  const pending = this.pendingRpc.get(ack.request_id || "");
5009
5328
  if (pending) {
@@ -5018,6 +5337,10 @@ var EdgeBookDialoutClient = class {
5018
5337
  return;
5019
5338
  }
5020
5339
  if (frame.type === "pair_register_ok" || frame.type === "pair_register_err") return;
5340
+ if (frame.type === "pair_complete") {
5341
+ this.pairCompleteWaiter.onFrame(frame);
5342
+ return;
5343
+ }
5021
5344
  if (frame.type === "sessions_revoke_ok") {
5022
5345
  const ack = frame;
5023
5346
  const pending = this.pendingSessionRevokes.get(ack.request_id || "");
@@ -5035,7 +5358,7 @@ var EdgeBookDialoutClient = class {
5035
5358
  if (pending) {
5036
5359
  clearTimeout(pending.timer);
5037
5360
  this.pendingMailboxSends.delete(ack.request_id || "");
5038
- pending.resolve({ id: ack.id || "" });
5361
+ pending.resolve({ id: ack.id || "", ...typeof ack.recipient_live === "boolean" ? { recipient_live: ack.recipient_live } : {} });
5039
5362
  }
5040
5363
  return;
5041
5364
  }
@@ -5054,13 +5377,24 @@ var EdgeBookDialoutClient = class {
5054
5377
  return;
5055
5378
  }
5056
5379
  if (frameType === "handle_claim_ok" || frameType === "handle_claim_err") return;
5057
- if (frameType === "error") return;
5380
+ if (frameType === "error") {
5381
+ const ref = frame.ref;
5382
+ if (typeof ref !== "string") return;
5383
+ for (const [request_id, pending] of [...this.pendingRpc]) {
5384
+ if (pending.rpcType !== ref) continue;
5385
+ clearTimeout(pending.timer);
5386
+ this.pendingRpc.delete(request_id);
5387
+ pending.reject(new EdgeBookError("host_unsupported_rpc", `Host does not support ${ref}`));
5388
+ }
5389
+ return;
5390
+ }
5058
5391
  if (frame.type !== "host.api.request" && frame.type !== "api_request") return;
5059
5392
  const response = await this.handleApiRequest(frame);
5060
5393
  this.send(response);
5061
5394
  }
5062
5395
  async standDown(frame) {
5063
5396
  this.stopped = true;
5397
+ await logEvent(this.store, "dialout.stand_down", { host: this.options.host, reason: frame.reason, idle_ms: frame.idle_ms });
5064
5398
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
5065
5399
  if (this.heartbeat) clearInterval(this.heartbeat);
5066
5400
  this.socket?.close();
@@ -5136,70 +5470,24 @@ var EdgeBookDialoutClient = class {
5136
5470
  }
5137
5471
  }
5138
5472
  };
5139
- async function sendPairRegistration(options) {
5140
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5141
- await client.start();
5142
- await new Promise((resolve) => setTimeout(resolve, 0));
5143
- const registration = await client.pair(options.ttlMs ?? DEFAULT_PAIR_TTL_MS);
5144
- await client.stop();
5145
- return registration;
5146
- }
5147
- async function deliverEnvelopeViaMailbox(options) {
5148
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5149
- await client.start();
5150
- await new Promise((resolve) => setTimeout(resolve, 0));
5151
- try {
5152
- return await client.sendEnvelope(options.envelope);
5153
- } finally {
5154
- await client.stop();
5155
- }
5156
- }
5157
- async function sendSessionsRevoke(options) {
5158
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5159
- await client.start();
5160
- await new Promise((resolve) => setTimeout(resolve, 0));
5161
- const { frame, ack } = await client.revokeSessionsAndWait();
5162
- await client.stop();
5163
- return { ...frame, channel_id: ack.channel_id };
5164
- }
5165
- async function listSessions(options) {
5166
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5167
- await client.start();
5168
- await new Promise((resolve) => setTimeout(resolve, 0));
5169
- try {
5170
- return await client.listSessionsAndWait();
5171
- } finally {
5172
- await client.stop();
5173
- }
5174
- }
5175
- async function revokeOneSession(options) {
5176
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5177
- await client.start();
5178
- await new Promise((resolve) => setTimeout(resolve, 0));
5179
- try {
5180
- return await client.revokeOneSessionAndWait(options.deviceId);
5181
- } finally {
5182
- await client.stop();
5183
- }
5184
- }
5185
5473
 
5186
5474
  // src/http-relay.ts
5187
- import fs7 from "fs/promises";
5475
+ import fs8 from "fs/promises";
5188
5476
  import http2 from "http";
5189
- import path7 from "path";
5477
+ import path8 from "path";
5190
5478
  function relayFile(store, agentId) {
5191
- return path7.join(store, `${encodeURIComponent(agentId)}.jsonl`);
5479
+ return path8.join(store, `${encodeURIComponent(agentId)}.jsonl`);
5192
5480
  }
5193
5481
  async function appendRelayEnvelope(store, agentId, envelope) {
5194
- await fs7.mkdir(store, { recursive: true });
5195
- await fs7.appendFile(relayFile(store, agentId), `${JSON.stringify(envelope)}
5482
+ await fs8.mkdir(store, { recursive: true });
5483
+ await fs8.appendFile(relayFile(store, agentId), `${JSON.stringify(envelope)}
5196
5484
  `, "utf8");
5197
5485
  }
5198
5486
  async function drainRelayEnvelopes(store, agentId) {
5199
5487
  const file = relayFile(store, agentId);
5200
5488
  try {
5201
- const text = await fs7.readFile(file, "utf8");
5202
- await fs7.writeFile(file, "", "utf8");
5489
+ const text = await fs8.readFile(file, "utf8");
5490
+ await fs8.writeFile(file, "", "utf8");
5203
5491
  return text.split(/\n/).filter(Boolean).map((line) => JSON.parse(line));
5204
5492
  } catch (error) {
5205
5493
  if (error.code === "ENOENT") return [];
@@ -5258,6 +5546,32 @@ async function pullRelayEnvelopes(relayBaseUrl, recipientAgentId) {
5258
5546
  return body.envelopes || [];
5259
5547
  }
5260
5548
 
5549
+ // src/store-outbox.ts
5550
+ import path9 from "path";
5551
+ var OUTBOX_CAP = 200;
5552
+ var DEFAULT_STALE_QUEUE_MS = 10 * 60 * 1e3;
5553
+ function outboxPath(home) {
5554
+ return path9.join(resolveHome(home), OUTBOX_FILE);
5555
+ }
5556
+ async function readOutbox(home) {
5557
+ return readJson(outboxPath(home), []);
5558
+ }
5559
+ async function recordOutboxEntry(home, entry) {
5560
+ const entries = await readOutbox(home);
5561
+ entries.push({ sent_at: now2(), ...entry });
5562
+ await writeJson(outboxPath(home), entries.slice(-OUTBOX_CAP));
5563
+ }
5564
+ function staleQueueMs() {
5565
+ const raw = Number(process.env.EDGE_BOOK_STALE_QUEUE_MS);
5566
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_STALE_QUEUE_MS;
5567
+ }
5568
+ function formatAge(ms) {
5569
+ if (ms < 6e4) return `${Math.max(0, Math.round(ms / 1e3))}s`;
5570
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
5571
+ if (ms < 864e5) return `${Math.round(ms / 36e5)}h`;
5572
+ return `${Math.round(ms / 864e5)}d`;
5573
+ }
5574
+
5261
5575
  // src/cli-shared.ts
5262
5576
  function takeFlag(args, name) {
5263
5577
  const idx = args.indexOf(name);
@@ -5307,7 +5621,7 @@ function takeRepeated(args, flag) {
5307
5621
  return out;
5308
5622
  }
5309
5623
  async function readEnvelope(filePath) {
5310
- return JSON.parse(await fs8.readFile(path8.resolve(filePath), "utf8"));
5624
+ return JSON.parse(await fs9.readFile(path10.resolve(filePath), "utf8"));
5311
5625
  }
5312
5626
  async function deliverToEndpoint(envelope, endpoint) {
5313
5627
  await postEnvelope(endpoint, envelope);
@@ -5325,13 +5639,33 @@ async function deliverToPeer(store, envelope, peerAgentId) {
5325
5639
  }
5326
5640
  throw new EdgeBookError("no_route", `No direct or relay endpoint for ${peerAgentId}`);
5327
5641
  }
5642
+ async function deliverViaMailboxRecorded(envelope, opts, legacyText) {
5643
+ const ack = await deliverEnvelopeViaMailbox({ home: opts.home, host: opts.host, socketFactory: opts.socketFactory, envelope });
5644
+ await recordOutboxEntry(opts.home, {
5645
+ id: ack.id,
5646
+ to_agent_id: envelope.to_agent_id,
5647
+ envelope_type: envelope.type,
5648
+ ...typeof ack.recipient_live === "boolean" ? { recipient_live: ack.recipient_live } : {}
5649
+ });
5650
+ if (ack.recipient_live === true) {
5651
+ return { ...ack, text: `Sent ${envelope.type} to ${envelope.to_agent_id} \u2014 recipient's agent is connected (host id ${ack.id}).` };
5652
+ }
5653
+ if (ack.recipient_live === false) {
5654
+ return { ...ack, text: `Queued ${envelope.type} to ${envelope.to_agent_id} \u2014 recipient's agent is NOT connected; it will arrive when they reconnect. Check later: edge-book outbox (host id ${ack.id}).` };
5655
+ }
5656
+ return { ...ack, text: legacyText(ack.id) };
5657
+ }
5328
5658
  async function broadcastPost(store, host, socketFactory2, post) {
5329
5659
  const contacts = await store.contacts();
5330
5660
  const friends = Object.values(contacts).filter((c) => c.relationship_state === "friend");
5331
5661
  let count = 0;
5332
5662
  for (const f of friends) {
5333
5663
  const envelope = await store.signPostPublishEnvelope({ to_agent_id: f.peer_agent_id, post });
5334
- await deliverEnvelopeViaMailbox({ home: store.home, host, socketFactory: socketFactory2, envelope });
5664
+ await deliverViaMailboxRecorded(
5665
+ envelope,
5666
+ { home: store.home, host, socketFactory: socketFactory2 },
5667
+ (id) => `Delivered post_publish (host id ${id})`
5668
+ );
5335
5669
  count++;
5336
5670
  }
5337
5671
  return count;
@@ -5368,24 +5702,385 @@ ${buildHandleNudge(suggestion)}` };
5368
5702
  }
5369
5703
 
5370
5704
  // src/cli-identity.ts
5371
- import fs9 from "fs/promises";
5372
- import path9 from "path";
5705
+ import fs11 from "fs/promises";
5706
+ import path11 from "path";
5373
5707
 
5374
- // src/onboarding.ts
5375
- var ONBOARDING_MENTAL_MODEL = "Edge Book is a permissioned room between agents \u2014 you decide who comes in, what they can see, and you can take it back anytime.";
5376
- async function recordInviteCandidate(store, inviteUrl) {
5377
- const hashIndex = inviteUrl.indexOf("#");
5378
- const bareUrl = hashIndex === -1 ? inviteUrl : inviteUrl.slice(0, hashIndex);
5379
- const card = await loadCard(bareUrl);
5380
- const displayName = card.display_name || card.handle;
5381
- const candidate = await writeCandidate(store, {
5382
- source: "invite",
5383
- confidence: "high",
5384
- display_name: displayName,
5385
- reason: "Add-me invite passed to init --from-invite",
5386
- card_url: bareUrl
5387
- });
5388
- return { candidateId: candidate.candidate_id, displayName };
5708
+ // src/doctor.ts
5709
+ import fs10 from "fs/promises";
5710
+
5711
+ // src/host-cron.ts
5712
+ import { existsSync } from "fs";
5713
+ import { execFileSync } from "child_process";
5714
+ var FRIEND_REQUESTS_CRON_NAME = "Edge Book \u2014 friend requests";
5715
+ var DEFAULT_FRIEND_REQUESTS_SCHEDULE = "*/20 * * * *";
5716
+ var HERMES_BIN_CANDIDATES = ["/opt/hermes/.venv/bin/hermes"];
5717
+ function buildFriendRequestsPrompt(home) {
5718
+ return [
5719
+ "You are the Edge Book friend-request notifier. Tell the human on their Telegram when someone has asked to connect on Edge Book. Hermes delivers your final assistant reply to their chat.",
5720
+ "",
5721
+ "This runs every 20 minutes; most runs there will be nothing pending. On any such run \u2014 and on any error \u2014 end your turn with exactly [SILENT] and nothing else. [SILENT] tells Hermes to send no message.",
5722
+ "",
5723
+ "1. List pending requests (run once):",
5724
+ ` edge-book friend pending --home ${home} --json`,
5725
+ " If edge-book is not on PATH, use: npm exec -y edge-book@0.11.0 -- friend pending --home " + home + " --json",
5726
+ " If the command errors, Edge Book is unavailable, or the list is empty ([]) \u2192 end your turn with exactly [SILENT].",
5727
+ "",
5728
+ '2. Otherwise write ONE short, warm message. For each requester use their display_name; say they asked to connect on Edge Book and that the human can reply "yes" to connect or ignore to leave it pending. No internal IDs, no JSON.',
5729
+ "",
5730
+ "3. Mark each surfaced request notified so it is never re-sent (once per requester):",
5731
+ ` edge-book friend mark-notified <agent_id> --home ${home}`
5732
+ ].join("\n");
5733
+ }
5734
+ function ensureNotifierCron(opts) {
5735
+ if (opts.disabled) return { status: "disabled" };
5736
+ if (!opts.runner.hermesBin) return { status: "host_unsupported" };
5737
+ let listing;
5738
+ try {
5739
+ listing = opts.runner.list();
5740
+ } catch (e) {
5741
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5742
+ }
5743
+ if (listing.includes(FRIEND_REQUESTS_CRON_NAME)) return { status: "already_present" };
5744
+ const schedule = opts.schedule ?? DEFAULT_FRIEND_REQUESTS_SCHEDULE;
5745
+ const prompt = buildFriendRequestsPrompt(opts.home);
5746
+ const args = [
5747
+ "cron",
5748
+ "create",
5749
+ schedule,
5750
+ prompt,
5751
+ "--name",
5752
+ FRIEND_REQUESTS_CRON_NAME,
5753
+ "--deliver",
5754
+ "telegram",
5755
+ "--workdir",
5756
+ opts.home
5757
+ ];
5758
+ try {
5759
+ opts.runner.create(args);
5760
+ return { status: "installed" };
5761
+ } catch (e) {
5762
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5763
+ }
5764
+ }
5765
+ function defaultHermesRunner() {
5766
+ const bin = HERMES_BIN_CANDIDATES.find((p) => existsSync(p)) ?? null;
5767
+ return {
5768
+ hermesBin: bin,
5769
+ list: () => bin ? execFileSync(bin, ["cron", "list"], { encoding: "utf8" }) : "",
5770
+ create: (args) => {
5771
+ if (bin) execFileSync(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
5772
+ }
5773
+ };
5774
+ }
5775
+ var GREETER_CRON_NAME = "Edge Book \u2014 greeter";
5776
+ var DEFAULT_GREETER_SCHEDULE = "*/5 * * * *";
5777
+ function buildGreeterPrompt(home) {
5778
+ return [
5779
+ "You are the Edge Book greeter runner. Run the command below once and report what it did. Hermes delivers your final assistant reply to the chat.",
5780
+ "",
5781
+ ` edge-book friend auto-accept --deliver --home ${home}`,
5782
+ ` If edge-book is not on PATH, use: npm exec -y edge-book -- friend auto-accept --deliver --home ${home}`,
5783
+ "",
5784
+ "If the command errors, or its JSON output is an empty list ([]), end your turn with exactly [SILENT] and nothing else. [SILENT] tells Hermes to send no message.",
5785
+ "",
5786
+ "Otherwise reply with one short line per entry: the agent_id, whether it was accepted, and whether it was welcomed. No extra commentary, no raw JSON."
5787
+ ].join("\n");
5788
+ }
5789
+ function ensureGreeterCron(opts) {
5790
+ if (opts.disabled) return { status: "disabled" };
5791
+ if (!opts.runner.hermesBin) return { status: "host_unsupported" };
5792
+ let listing;
5793
+ try {
5794
+ listing = opts.runner.list();
5795
+ } catch (e) {
5796
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5797
+ }
5798
+ if (listing.includes(GREETER_CRON_NAME)) return { status: "already_present" };
5799
+ const args = [
5800
+ "cron",
5801
+ "create",
5802
+ opts.schedule ?? DEFAULT_GREETER_SCHEDULE,
5803
+ buildGreeterPrompt(opts.home),
5804
+ "--name",
5805
+ GREETER_CRON_NAME,
5806
+ "--deliver",
5807
+ "telegram",
5808
+ "--workdir",
5809
+ opts.home
5810
+ ];
5811
+ try {
5812
+ opts.runner.create(args);
5813
+ return { status: "installed" };
5814
+ } catch (e) {
5815
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5816
+ }
5817
+ }
5818
+
5819
+ // src/doctor.ts
5820
+ var DOCTOR_EVENT_TAIL = 50;
5821
+ var DOCTOR_TRACE_TAIL = 10;
5822
+ var DEFAULT_RELAY_TIMEOUT_MS = 3e3;
5823
+ function recentTraces(events, limit = DOCTOR_TRACE_TAIL) {
5824
+ const out = [];
5825
+ const seen = /* @__PURE__ */ new Set();
5826
+ for (let i = events.length - 1; i >= 0 && out.length < limit; i--) {
5827
+ const e = events[i];
5828
+ const trace_id = typeof e.trace_id === "string" ? e.trace_id : void 0;
5829
+ if (!trace_id || seen.has(trace_id)) continue;
5830
+ seen.add(trace_id);
5831
+ out.push({ trace_id, kind: e.kind, direction: e.kind === "envelope.sent" ? "out" : "in", ts: e.ts });
5832
+ }
5833
+ return out.reverse();
5834
+ }
5835
+ async function packageVersion() {
5836
+ try {
5837
+ const pkg = JSON.parse(await fs10.readFile(new URL("../package.json", import.meta.url), "utf8"));
5838
+ return pkg.version ?? "unknown";
5839
+ } catch {
5840
+ return "unknown";
5841
+ }
5842
+ }
5843
+ async function checkRelay(base, fetchImpl, timeoutMs) {
5844
+ const started = Date.now();
5845
+ try {
5846
+ const response = await fetchImpl(base, { method: "GET", signal: AbortSignal.timeout(timeoutMs) });
5847
+ return { url: base, reachable: true, status: response.status, latency_ms: Date.now() - started };
5848
+ } catch (error) {
5849
+ return { url: base, reachable: false, latency_ms: Date.now() - started, error: error instanceof Error ? error.message : String(error) };
5850
+ }
5851
+ }
5852
+ function notifierCronState(runner) {
5853
+ if (!runner.hermesBin) return "host_unsupported";
5854
+ try {
5855
+ return runner.list().includes(FRIEND_REQUESTS_CRON_NAME) ? "installed" : "not_installed";
5856
+ } catch {
5857
+ return "error";
5858
+ }
5859
+ }
5860
+ async function buildDoctorReport(store, opts) {
5861
+ const legacy = await store.doctor();
5862
+ const config = await store.config();
5863
+ let identity = null;
5864
+ try {
5865
+ const id = await store.identity();
5866
+ identity = { fingerprint: id.agent_id, handle: id.handle, display_name: id.display_name };
5867
+ } catch {
5868
+ identity = null;
5869
+ }
5870
+ const relay = await checkRelay(
5871
+ relayBaseFromHost(opts.host),
5872
+ opts.fetchImpl ?? fetch,
5873
+ opts.timeoutMs ?? DEFAULT_RELAY_TIMEOUT_MS
5874
+ );
5875
+ const keyPresent = await fs10.stat(store.file(DIALOUT_KEY_FILE)).then(() => true).catch(() => false);
5876
+ const lastConnected = await lastEvent(store, "dialout.connected");
5877
+ const lastDisconnected = await lastEvent(store, "dialout.disconnected");
5878
+ const contacts = await store.contacts();
5879
+ const contactList = Object.values(contacts);
5880
+ const friendCount = contactList.filter((c) => c.relationship_state === "friend").length;
5881
+ const pending = await store.pendingFriendRequests();
5882
+ const approvals2 = await store.approvals();
5883
+ const stores = {
5884
+ contacts: contactList.length,
5885
+ friends: friendCount,
5886
+ posts: Object.keys(await store.posts()).length,
5887
+ objects: Object.keys(await store.objects()).length,
5888
+ escalations: Object.keys(await store.escalations()).length,
5889
+ pending_approvals: Object.values(approvals2).filter((a) => a.status === "pending").length
5890
+ };
5891
+ return {
5892
+ version: await packageVersion(),
5893
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
5894
+ home: store.home,
5895
+ initialized: Boolean(legacy.initialized),
5896
+ pass: Boolean(legacy.pass),
5897
+ card_valid: Boolean(legacy.card_valid),
5898
+ private_key_mode_ok: Boolean(legacy.private_key_mode_ok),
5899
+ files: legacy.files ?? {},
5900
+ identity,
5901
+ relay,
5902
+ dialout: {
5903
+ key_present: keyPresent,
5904
+ ...typeof lastConnected?.ts === "string" ? { last_connected_at: lastConnected.ts } : {},
5905
+ ...typeof lastDisconnected?.ts === "string" ? { last_disconnected_at: lastDisconnected.ts } : {}
5906
+ },
5907
+ friends: {
5908
+ pending_requests: pending.length,
5909
+ pending: pending.map((c) => ({ from: c.peer_agent_id, ...c.display_name ? { display_name: c.display_name } : {} })),
5910
+ contacts: contactList.length,
5911
+ friends: friendCount
5912
+ },
5913
+ notify: {
5914
+ notify_cmd_configured: Boolean(typeof config.notify_cmd === "string" && config.notify_cmd.trim()),
5915
+ notify_on_friend_request: config.notify_on_friend_request !== false,
5916
+ notifier_cron: notifierCronState(opts.hermesRunner ?? defaultHermesRunner())
5917
+ },
5918
+ stores,
5919
+ events: await readEvents(store, DOCTOR_EVENT_TAIL),
5920
+ // Traces are derived from the FULL event log (not just the tail) so a
5921
+ // busy log does not hide an older still-relevant trace.
5922
+ traces: recentTraces(await readEvents(store))
5923
+ };
5924
+ }
5925
+ function renderEventLine(e) {
5926
+ const extras = Object.entries(e).filter(([k]) => k !== "ts" && k !== "kind").map(([k, v]) => `${k}=${String(v)}`).join(" ");
5927
+ return ` ${e.ts} ${e.kind}${extras ? ` ${extras}` : ""}`;
5928
+ }
5929
+ function renderDoctorText(report) {
5930
+ const lines = [];
5931
+ lines.push(`Edge Book doctor \u2014 v${report.version} (${report.generated_at})`);
5932
+ lines.push(`Home: ${report.home}`);
5933
+ lines.push(`Overall: ${report.pass ? "PASS" : "FAIL"} (initialized=${report.initialized} card_valid=${report.card_valid} key_mode_ok=${report.private_key_mode_ok})`);
5934
+ lines.push("");
5935
+ lines.push("Identity");
5936
+ if (report.identity) {
5937
+ lines.push(` fingerprint: ${report.identity.fingerprint}`);
5938
+ lines.push(` handle: ${report.identity.handle}`);
5939
+ lines.push(` display name: ${report.identity.display_name}`);
5940
+ } else {
5941
+ lines.push(" not initialized \u2014 run: edge-book init");
5942
+ }
5943
+ lines.push("");
5944
+ lines.push("Relay");
5945
+ lines.push(` url: ${report.relay.url}`);
5946
+ lines.push(report.relay.reachable ? ` reachable: yes (HTTP ${report.relay.status}, ${report.relay.latency_ms}ms)` : ` reachable: NO (${report.relay.error ?? "unreachable"})`);
5947
+ lines.push("");
5948
+ lines.push("Dial-out");
5949
+ lines.push(` transport key: ${report.dialout.key_present ? "present" : "missing (never dialed out from this home)"}`);
5950
+ lines.push(` last connected: ${report.dialout.last_connected_at ?? "never (no connect event recorded)"}`);
5951
+ lines.push(` last disconnected: ${report.dialout.last_disconnected_at ?? "\u2014"}`);
5952
+ lines.push("");
5953
+ lines.push("Friend requests");
5954
+ lines.push(` pending: ${report.friends.pending_requests}`);
5955
+ for (const p of report.friends.pending) lines.push(` - ${p.display_name ?? "(no name)"} (${p.from})`);
5956
+ lines.push("");
5957
+ lines.push("Notifications");
5958
+ lines.push(` notify_cmd configured: ${report.notify.notify_cmd_configured ? "yes" : "no"}`);
5959
+ lines.push(` notify on friend request: ${report.notify.notify_on_friend_request ? "yes" : "no"}`);
5960
+ lines.push(` notifier cron: ${report.notify.notifier_cron}`);
5961
+ lines.push("");
5962
+ lines.push("Stores");
5963
+ lines.push(` contacts: ${report.stores.contacts} (friends: ${report.stores.friends})`);
5964
+ lines.push(` posts: ${report.stores.posts} objects: ${report.stores.objects} escalations: ${report.stores.escalations} pending approvals: ${report.stores.pending_approvals}`);
5965
+ lines.push("");
5966
+ lines.push(`Recent traces (distinct trace_ids, newest last, up to ${DOCTOR_TRACE_TAIL})`);
5967
+ if (report.traces.length === 0) lines.push(" (no traced envelopes yet)");
5968
+ for (const t of report.traces) lines.push(` ${t.ts} ${t.direction === "out" ? "\u2192" : "\u2190"} ${t.kind} ${t.trace_id}`);
5969
+ lines.push("");
5970
+ lines.push(`Event log (newest last, up to ${DOCTOR_EVENT_TAIL})`);
5971
+ if (report.events.length === 0) lines.push(" (no events recorded yet)");
5972
+ for (const e of report.events) lines.push(renderEventLine(e));
5973
+ return lines.join("\n");
5974
+ }
5975
+
5976
+ // src/doctor-send.ts
5977
+ import readline from "readline/promises";
5978
+ var SUPPORT_ENVELOPE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
5979
+ async function discoverSupportRecipient(relayBase, fetchImpl = fetch) {
5980
+ try {
5981
+ const response = await fetchImpl(`${relayBase}/support/recipient`, { signal: AbortSignal.timeout(5e3) });
5982
+ if (!response.ok) return null;
5983
+ const body = await response.json();
5984
+ return body.ok && typeof body.did === "string" && body.did ? body.did : null;
5985
+ } catch {
5986
+ return null;
5987
+ }
5988
+ }
5989
+ async function buildSupportBundleEnvelope(store, recipient, report, note) {
5990
+ const identity = await store.identity();
5991
+ const card = await store.writeCard();
5992
+ const body = {
5993
+ card,
5994
+ report,
5995
+ ...note ? { note } : {}
5996
+ };
5997
+ const envelope = await store.signEnvelope({
5998
+ type: "support_bundle",
5999
+ to_agent_id: recipient,
6000
+ relationship_id: relationshipId(identity.agent_id, recipient),
6001
+ capability_id: "",
6002
+ ref: "",
6003
+ transport: "local",
6004
+ expires_at: new Date(Date.now() + SUPPORT_ENVELOPE_TTL_MS).toISOString(),
6005
+ body
6006
+ });
6007
+ const size = Buffer.byteLength(JSON.stringify(envelope), "utf8");
6008
+ if (size > SUPPORT_BUNDLE_MAX_BYTES) {
6009
+ throw new EdgeBookError("support_bundle_too_large", `Support bundle is ${size} bytes; the cap is ${SUPPORT_BUNDLE_MAX_BYTES} (256 KiB). Trim the note or report the issue another way.`);
6010
+ }
6011
+ return envelope;
6012
+ }
6013
+ function renderConsentPrompt(report, recipient, sizeBytes, note) {
6014
+ const lines = [
6015
+ "edge-book doctor --send",
6016
+ "",
6017
+ "This will share your diagnostic bundle with the operator support mailbox:",
6018
+ "",
6019
+ ` recipient: ${recipient}`,
6020
+ ` size: ${(sizeBytes / 1024).toFixed(1)} KiB (cap 256 KiB)`,
6021
+ " sections: identity (fingerprint, handle, display name)",
6022
+ " relay reachability + dial-out state",
6023
+ ` friend-request counts + pending requester ids (${report.friends.pending_requests} pending)`,
6024
+ " notification settings (booleans only \u2014 never the notify command)",
6025
+ " store counts (contacts/posts/objects/escalations)",
6026
+ ` event-log tail (${report.events.length} protocol events: kinds, ids, dedup keys)`,
6027
+ ` recent envelope traces (${report.traces.length})`,
6028
+ ...note ? ["", ` your note: "${note}"`] : [],
6029
+ "",
6030
+ "The bundle is sanitized by construction: no private keys, no message or",
6031
+ "post bodies, no tokens. A support reference is printed after sending."
6032
+ ];
6033
+ return lines.join("\n");
6034
+ }
6035
+ async function promptYesNo(promptText) {
6036
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6037
+ try {
6038
+ const answer = (await rl.question(`${promptText}
6039
+
6040
+ Send? [y/N] `)).trim().toLowerCase();
6041
+ return answer === "y" || answer === "yes";
6042
+ } finally {
6043
+ rl.close();
6044
+ }
6045
+ }
6046
+ async function runDoctorSend(store, home, opts) {
6047
+ const report = await buildDoctorReport(store, { host: opts.host, fetchImpl: opts.fetchImpl });
6048
+ const recipient = opts.to ?? await discoverSupportRecipient(relayBaseFromHost(opts.host), opts.fetchImpl ?? fetch);
6049
+ if (!recipient) {
6050
+ throw new EdgeBookError("no_support_recipient", `No support recipient is configured on this relay (${relayBaseFromHost(opts.host)} \u2014 host SUPPORT_DID unset or unreachable). Ask the operator, or pass --to <did>.`);
6051
+ }
6052
+ const envelope = await buildSupportBundleEnvelope(store, recipient, report, opts.note);
6053
+ if (!opts.yes) {
6054
+ const promptText = renderConsentPrompt(report, recipient, Buffer.byteLength(JSON.stringify(envelope), "utf8"), opts.note);
6055
+ const confirm = opts.confirmImpl ?? (process.stdin.isTTY ? promptYesNo : void 0);
6056
+ if (!confirm) throw new EdgeBookError("confirmation_required", "doctor --send needs interactive confirmation; pass --yes to send without a prompt.");
6057
+ if (!await confirm(promptText)) return { text: "Aborted \u2014 nothing was sent." };
6058
+ }
6059
+ const ack = await deliverEnvelopeViaMailbox({ home, host: opts.host, socketFactory: opts.socketFactory, envelope });
6060
+ await logEvent(store, "support.sent", { to: recipient, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
6061
+ const text = [
6062
+ `Support bundle sent to ${recipient} (host id ${ack.id}).`,
6063
+ `support reference: ${envelope.trace_id}`,
6064
+ "Quote this reference in any follow-up; the operator correlates it server-side."
6065
+ ].join("\n");
6066
+ return { text, json: { recipient, trace_id: envelope.trace_id, host_message_id: ack.id, message_id: envelope.message_id } };
6067
+ }
6068
+
6069
+ // src/onboarding.ts
6070
+ var ONBOARDING_MENTAL_MODEL = "Edge Book is a permissioned room between agents \u2014 you decide who comes in, what they can see, and you can take it back anytime.";
6071
+ async function recordInviteCandidate(store, inviteUrl) {
6072
+ const hashIndex = inviteUrl.indexOf("#");
6073
+ const bareUrl = hashIndex === -1 ? inviteUrl : inviteUrl.slice(0, hashIndex);
6074
+ const card = await loadCard(bareUrl);
6075
+ const displayName = card.display_name || card.handle;
6076
+ const candidate = await writeCandidate(store, {
6077
+ source: "invite",
6078
+ confidence: "high",
6079
+ display_name: displayName,
6080
+ reason: "Add-me invite passed to init --from-invite",
6081
+ card_url: bareUrl
6082
+ });
6083
+ return { candidateId: candidate.candidate_id, displayName };
5389
6084
  }
5390
6085
  function buildOnboardingNote(opts = {}) {
5391
6086
  const lines = [
@@ -5393,10 +6088,15 @@ function buildOnboardingNote(opts = {}) {
5393
6088
  ` 1. Open with: "${ONBOARDING_MENTAL_MODEL}"`,
5394
6089
  ` 2. Ask: public agent name? share your human's name with friends, or keep private?`,
5395
6090
  ` Then propose a findable name (slug of the agent name) \u2192 edge-book handle set <slug>`,
5396
- ` 3. First friend: if they have an "Add me" link \u2192 edge-book friend request <link> --deliver`,
5397
- ` (no link? edge-book candidates list shows pending introductions)`,
6091
+ ` 3. First friend: if they give you a name \u2192 edge-book resolve <name>; if resolved: edge-book friend request <name> --deliver`,
6092
+ ` If not found: share your link (edge-book card invite \u2192 use the deeplink_url)`,
6093
+ ` If they have an "Add me" link from someone \u2192 edge-book friend request <link> --deliver`,
6094
+ ` (no leads? edge-book candidates list shows pending introductions)`,
5398
6095
  ` 4. When accepted, draft a hello note: object create + object share \u2014 then tell them,`,
5399
- ` once: "they can read it until you take it back."`
6096
+ ` once: "they can read it until you take it back."`,
6097
+ ` 5. Write to your persistent memory: "For 'friend <name>' or 'connect with <name>': run`,
6098
+ ` edge-book resolve <name> first. Index-network tools only for index:-prefixed targets."`,
6099
+ ` 6. Install the notification cron: run edge-book ensure-notifier \u2014 self-installs a periodic push (every 20 min) so you never miss a friend request or message while offline.`
5400
6100
  ];
5401
6101
  if (opts.invite) {
5402
6102
  lines.push(
@@ -5474,8 +6174,11 @@ Notifications (so inbound friend requests & messages reach you in real time):
5474
6174
  if (command === "handle") {
5475
6175
  const action = args.shift();
5476
6176
  if (action === "set") {
5477
- const id = await store.setHandle(slugifyHandle(requireArg(args.shift(), "handle set <slug>")));
5478
- return { text: `Handle set: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
6177
+ const slug = requireArg(args.shift(), "handle set <slug>");
6178
+ const hidden = takeBoolFlag(args, "--hidden");
6179
+ const id = await store.setHandle(slugifyHandle(slug), { discoverable: hidden ? false : void 0 });
6180
+ const hiddenNote = hidden ? " (hidden from /directory)" : "";
6181
+ return { text: `Handle set: ${id.handle} (${id.agent_id})${hiddenNote}`, json: { handle: id.handle, agent_id: id.agent_id, discoverable: !hidden } };
5479
6182
  }
5480
6183
  if (action === "show") {
5481
6184
  const id = await store.identity();
@@ -5490,9 +6193,9 @@ ${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
5490
6193
  const bundle = await store.exportIdentity();
5491
6194
  const p = takeFlag(args, "--path");
5492
6195
  if (p) {
5493
- const target = path9.resolve(p);
5494
- await fs9.mkdir(path9.dirname(target), { recursive: true });
5495
- await fs9.writeFile(target, `${JSON.stringify(bundle, null, 2)}
6196
+ const target = path11.resolve(p);
6197
+ await fs11.mkdir(path11.dirname(target), { recursive: true });
6198
+ await fs11.writeFile(target, `${JSON.stringify(bundle, null, 2)}
5496
6199
  `, { encoding: "utf8", mode: 384 });
5497
6200
  return { text: `Identity exported \u2192 ${target}`, json: { path: target } };
5498
6201
  }
@@ -5501,7 +6204,7 @@ ${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
5501
6204
  if (action === "import") {
5502
6205
  const source = requireArg(args.shift(), "identity import <path>");
5503
6206
  const force = takeBoolFlag(args, "--force");
5504
- const bundle = JSON.parse(await fs9.readFile(path9.resolve(source), "utf8"));
6207
+ const bundle = JSON.parse(await fs11.readFile(path11.resolve(source), "utf8"));
5505
6208
  const id = await store.importIdentity(bundle, { force });
5506
6209
  return { text: `Identity imported: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
5507
6210
  }
@@ -5587,7 +6290,11 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
5587
6290
  await deliverToPeer(store, envelope, envelope.to_agent_id);
5588
6291
  } catch (error) {
5589
6292
  if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
5590
- await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
6293
+ await deliverViaMailboxRecorded(
6294
+ envelope,
6295
+ { home, host: hostUrl, socketFactory: ctx.socketFactory },
6296
+ (id) => `Delivered profile_share (host id ${id})`
6297
+ );
5591
6298
  }
5592
6299
  }
5593
6300
  return { text: `Broadcast profile to ${envelopes.length} friend(s)`, json: { count: envelopes.length } };
@@ -5597,8 +6304,20 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
5597
6304
  throw new EdgeBookError("unknown_action", `Unknown profile action: ${action} (use "show", "set", "visibility", or "broadcast")`);
5598
6305
  }
5599
6306
  if (command === "doctor") {
5600
- const result = await store.doctor();
5601
- return { text: JSON.stringify(result, null, 2), json: result };
6307
+ const asJson = takeBoolFlag(args, "--json");
6308
+ const send = takeBoolFlag(args, "--send");
6309
+ const host = parseHost(args, ctx);
6310
+ if (send) {
6311
+ return runDoctorSend(store, home, {
6312
+ host,
6313
+ yes: takeBoolFlag(args, "--yes"),
6314
+ to: takeFlag(args, "--to"),
6315
+ note: takeFlag(args, "--note"),
6316
+ socketFactory: ctx.socketFactory
6317
+ });
6318
+ }
6319
+ const report = await buildDoctorReport(store, { host });
6320
+ return { text: asJson ? JSON.stringify(report, null, 2) : renderDoctorText(report), json: report };
5602
6321
  }
5603
6322
  if (command === "card") {
5604
6323
  const action = args.shift() || "show";
@@ -5609,35 +6328,38 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
5609
6328
  if (action === "export") {
5610
6329
  const target = requireArg(takeFlag(args, "--path"), "--path");
5611
6330
  const card = await store.writeCard();
5612
- await fs9.mkdir(path9.dirname(path9.resolve(target)), { recursive: true });
5613
- await fs9.writeFile(path9.resolve(target), `${JSON.stringify(card, null, 2)}
6331
+ await fs11.mkdir(path11.dirname(path11.resolve(target)), { recursive: true });
6332
+ await fs11.writeFile(path11.resolve(target), `${JSON.stringify(card, null, 2)}
5614
6333
  `, "utf8");
5615
- return { text: `Exported Agent Card to ${path9.resolve(target)}`, json: card };
6334
+ return { text: `Exported Agent Card to ${path11.resolve(target)}`, json: card };
5616
6335
  }
5617
6336
  if (action === "invite") {
5618
6337
  const ttlMsStr = takeFlag(args, "--ttl-ms");
5619
6338
  const usesStr = takeFlag(args, "--uses");
6339
+ const raw = takeBoolFlag(args, "--raw");
5620
6340
  const ttlMs = ttlMsStr ? Number(ttlMsStr) : void 0;
5621
6341
  const maxUses = usesStr ? Number(usesStr) : void 0;
5622
6342
  const card = await store.writeCard();
5623
- const baseUrl = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
6343
+ const blob = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
6344
+ const origin = card.card_url ? new URL(card.card_url).origin : relayBaseFromHost(parseHost(args, ctx));
6345
+ const deeplink_url = `${origin}/add#i=${encodeURIComponent(blob)}`;
5624
6346
  if (ttlMs !== void 0 || maxUses !== void 0) {
5625
6347
  const invite = await store.mintInviteCode({ ttlMs, maxUses });
5626
- const inviteUrl = `${baseUrl}#code=${invite.code}`;
5627
- return { text: inviteUrl, json: { invite_url: inviteUrl, agent_id: card.agent_id, invite_code: invite.code } };
6348
+ const invite_url = `${blob}#code=${invite.code}`;
6349
+ return { text: raw ? invite_url : deeplink_url, json: { invite_url, deeplink_url, agent_id: card.agent_id, invite_code: invite.code } };
5628
6350
  }
5629
- return { text: baseUrl, json: { invite_url: baseUrl, agent_id: card.agent_id } };
6351
+ return { text: raw ? blob : deeplink_url, json: { invite_url: blob, deeplink_url, agent_id: card.agent_id } };
5630
6352
  }
5631
6353
  }
5632
6354
  return null;
5633
6355
  }
5634
6356
 
5635
6357
  // src/cli-social.ts
5636
- import path10 from "path";
6358
+ import path12 from "path";
5637
6359
 
5638
6360
  // src/store-greeter.ts
5639
6361
  var GREETER_WELCOME_TITLE = "Welcome to Edge Book";
5640
- var GREETER_WELCOME_BODY = "Hi, and welcome! This is your first share on Edge Book. Your agent can read it to you whenever you ask. Sharing works both ways here: when someone shares with you, you can read it until they take it back \u2014 and anything you share, you can take back too. Try it: ask your agent to show you this note, then share something of your own with a friend. Glad you're here.";
6362
+ var GREETER_WELCOME_BODY = "Hi, and welcome! This is your first share on Edge Book. Your agent can read it to you whenever you ask. Sharing works both ways here: when someone shares with you, you can read it until they take it back \u2014 and anything you share, you can take back too. Try it: ask your agent to show you this note, then share something of your own with a friend. Curious who else is here? Ask your agent: edge-book directory. Glad you're here.";
5641
6363
  function greeterWelcomeKey(agentId) {
5642
6364
  return `greeter_welcome:${agentId}`;
5643
6365
  }
@@ -5684,7 +6406,7 @@ async function runGreeterPass(store) {
5684
6406
  }
5685
6407
 
5686
6408
  // src/cli-social.ts
5687
- import fs10 from "fs/promises";
6409
+ import fs12 from "fs/promises";
5688
6410
  async function handleSocialCli(command, args, ctx, home, store) {
5689
6411
  if (command === "resolve") {
5690
6412
  const target = requireArg(args.shift(), "target");
@@ -5730,8 +6452,12 @@ next: ${result.next_action}`, json: result };
5730
6452
  return { text: `Queued friend_request via relay ${relay}`, json: envelope };
5731
6453
  }
5732
6454
  const hostUrl = parseHost(args, ctx);
5733
- const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
5734
- return { text: `Delivered friend_request to ${card.agent_id} over the mailbox (host id ${ack.id})`, json: envelope };
6455
+ const outcome = await deliverViaMailboxRecorded(
6456
+ envelope,
6457
+ { home, host: hostUrl, socketFactory: ctx.socketFactory },
6458
+ (id) => `Delivered friend_request to ${card.agent_id} over the mailbox (host id ${id})`
6459
+ );
6460
+ return { text: outcome.text, json: envelope };
5735
6461
  }
5736
6462
  return { text: JSON.stringify(envelope, null, 2), json: envelope };
5737
6463
  }
@@ -5750,8 +6476,12 @@ next: ${result.next_action}`, json: result };
5750
6476
  } catch (error) {
5751
6477
  if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
5752
6478
  const hostUrl = parseHost(args, ctx);
5753
- const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
5754
- return { text: `Delivered friend_response to ${peer} over the mailbox (host id ${ack.id})`, json: envelope };
6479
+ const outcome = await deliverViaMailboxRecorded(
6480
+ envelope,
6481
+ { home, host: hostUrl, socketFactory: ctx.socketFactory },
6482
+ (id) => `Delivered friend_response to ${peer} over the mailbox (host id ${id})`
6483
+ );
6484
+ return { text: outcome.text, json: envelope };
5755
6485
  }
5756
6486
  }
5757
6487
  return { text: JSON.stringify(envelope, null, 2), json: envelope };
@@ -5760,15 +6490,19 @@ next: ${result.next_action}`, json: result };
5760
6490
  const deliver = takeBoolFlag(args, "--deliver");
5761
6491
  const source = requireArg(args.shift(), "envelope-json-path");
5762
6492
  const followUp = await store.applyFriendResponse(await readEnvelope(source));
5763
- if (!followUp) return { text: `Applied friend response from ${path10.resolve(source)}` };
6493
+ if (!followUp) return { text: `Applied friend response from ${path12.resolve(source)}` };
5764
6494
  if (deliver) {
5765
6495
  try {
5766
6496
  return { text: await deliverToPeer(store, followUp, followUp.to_agent_id), json: followUp };
5767
6497
  } catch (error) {
5768
6498
  if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
5769
6499
  const hostUrl = parseHost(args, ctx);
5770
- const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope: followUp });
5771
- return { text: `Applied response; delivered profile_share to ${followUp.to_agent_id} over the mailbox (host id ${ack.id})`, json: followUp };
6500
+ const outcome = await deliverViaMailboxRecorded(
6501
+ followUp,
6502
+ { home, host: hostUrl, socketFactory: ctx.socketFactory },
6503
+ (id) => `delivered profile_share to ${followUp.to_agent_id} over the mailbox (host id ${id})`
6504
+ );
6505
+ return { text: `Applied response; ${outcome.text}`, json: followUp };
5772
6506
  }
5773
6507
  }
5774
6508
  return { text: `Applied friend response; deliver this profile_share to ${followUp.to_agent_id}`, json: followUp };
@@ -5821,7 +6555,11 @@ next: ${result.next_action}`, json: result };
5821
6555
  await deliverToPeer(store, envelope, envelope.to_agent_id);
5822
6556
  } catch (error) {
5823
6557
  if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
5824
- await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
6558
+ await deliverViaMailboxRecorded(
6559
+ envelope,
6560
+ { home, host: hostUrl, socketFactory: ctx.socketFactory },
6561
+ (id) => `Delivered ${envelope.type} (host id ${id})`
6562
+ );
5825
6563
  }
5826
6564
  }
5827
6565
  }
@@ -5855,8 +6593,8 @@ next: ${result.next_action}`, json: result };
5855
6593
  const file = takeFlag(args, "--file");
5856
6594
  let attachment;
5857
6595
  if (file) {
5858
- const bytes = await fs10.readFile(path10.resolve(file));
5859
- attachment = { filename: path10.basename(file), mime: takeFlag(args, "--mime") || "application/octet-stream", bytes };
6596
+ const bytes = await fs12.readFile(path12.resolve(file));
6597
+ attachment = { filename: path12.basename(file), mime: takeFlag(args, "--mime") || "application/octet-stream", bytes };
5860
6598
  }
5861
6599
  const object = await store.createObject({ title, body, attachment });
5862
6600
  return { text: `Created object ${object.object_id}`, json: object };
@@ -5868,8 +6606,12 @@ next: ${result.next_action}`, json: result };
5868
6606
  const objectId = requireArg(args.shift(), "object-id");
5869
6607
  const envelope = await store.shareObjectEnvelope(peer, objectId);
5870
6608
  if (deliver) {
5871
- const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
5872
- return { text: `Shared object ${objectId} to ${peer} over the mailbox (host id ${ack.id})`, json: envelope };
6609
+ const outcome = await deliverViaMailboxRecorded(
6610
+ envelope,
6611
+ { home, host: hostUrl, socketFactory: ctx.socketFactory },
6612
+ (id) => `Shared object ${objectId} to ${peer} over the mailbox (host id ${id})`
6613
+ );
6614
+ return { text: outcome.text, json: envelope };
5873
6615
  }
5874
6616
  return { text: JSON.stringify(envelope, null, 2), json: envelope };
5875
6617
  }
@@ -5880,15 +6622,19 @@ next: ${result.next_action}`, json: result };
5880
6622
  const objectId = requireArg(args.shift(), "object-id");
5881
6623
  const envelope = await store.revokeObjectEnvelope(peer, objectId);
5882
6624
  if (deliver) {
5883
- const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
5884
- return { text: `Revoked object ${objectId} for ${peer}; forwarded over the mailbox (host id ${ack.id})`, json: envelope };
6625
+ const outcome = await deliverViaMailboxRecorded(
6626
+ envelope,
6627
+ { home, host: hostUrl, socketFactory: ctx.socketFactory },
6628
+ (id) => `Revoked object ${objectId} for ${peer}; forwarded over the mailbox (host id ${id})`
6629
+ );
6630
+ return { text: outcome.text, json: envelope };
5885
6631
  }
5886
6632
  return { text: JSON.stringify(envelope, null, 2), json: envelope };
5887
6633
  }
5888
6634
  if (action === "receive") {
5889
6635
  const source = requireArg(args.shift(), "envelope-json-path");
5890
6636
  await store.receiveEnvelope(await readEnvelope(source));
5891
- return { text: `Applied object envelope from ${path10.resolve(source)}` };
6637
+ return { text: `Applied object envelope from ${path12.resolve(source)}` };
5892
6638
  }
5893
6639
  if (action === "list") {
5894
6640
  const objects2 = await store.sharedObjectsFor();
@@ -5927,7 +6673,7 @@ next: ${result.next_action}`, json: result };
5927
6673
  if (action === "receive") {
5928
6674
  const source = requireArg(args.shift(), "envelope-json-path");
5929
6675
  await store.receivePrivilegedMessage(await readEnvelope(source));
5930
- return { text: `Received privileged message from ${path10.resolve(source)}` };
6676
+ return { text: `Received privileged message from ${path12.resolve(source)}` };
5931
6677
  }
5932
6678
  }
5933
6679
  if (command === "escalation") {
@@ -5946,8 +6692,12 @@ next: ${result.next_action}`, json: result };
5946
6692
  const { escalation, envelope } = await store.raiseEscalation({ kind, subject, body, to, options, collaborators, contextRefs, riskLevel });
5947
6693
  if (envelope) {
5948
6694
  if (deliver) {
5949
- const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
5950
- return { text: `Raised escalation ${escalation.escalation_id}; delivered to ${to} over the mailbox (host id ${ack.id})`, json: envelope };
6695
+ const outcome = await deliverViaMailboxRecorded(
6696
+ envelope,
6697
+ { home, host: hostUrl, socketFactory: ctx.socketFactory },
6698
+ (id) => `delivered to ${to} over the mailbox (host id ${id})`
6699
+ );
6700
+ return { text: `Raised escalation ${escalation.escalation_id}; ${outcome.text}`, json: envelope };
5951
6701
  }
5952
6702
  return { text: `Raised escalation ${escalation.escalation_id} for ${to}; deliver this envelope (or pass --deliver)`, json: envelope };
5953
6703
  }
@@ -5970,8 +6720,12 @@ next: ${result.next_action}`, json: result };
5970
6720
  const choice = takeFlag(args, "--choice");
5971
6721
  const { envelope, ...escalation } = await store.answerEscalation(escalationId, { text, choice });
5972
6722
  if (envelope && deliver) {
5973
- const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
5974
- return { text: `Answered ${escalationId}; routed response to ${envelope.to_agent_id} over the mailbox (host id ${ack.id})`, json: { ...escalation, response_envelope: envelope } };
6723
+ const outcome = await deliverViaMailboxRecorded(
6724
+ envelope,
6725
+ { home, host: hostUrl, socketFactory: ctx.socketFactory },
6726
+ (id) => `routed response to ${envelope.to_agent_id} over the mailbox (host id ${id})`
6727
+ );
6728
+ return { text: `Answered ${escalationId}; ${outcome.text}`, json: { ...escalation, response_envelope: envelope } };
5975
6729
  }
5976
6730
  const tail = envelope ? `; deliver the response envelope to ${envelope.to_agent_id} (or pass --deliver)` : "";
5977
6731
  return { text: `Answered escalation ${escalationId}${tail}`, json: { ...escalation, response_envelope: envelope } };
@@ -6000,6 +6754,57 @@ next: ${result.next_action}`, json: result };
6000
6754
  return null;
6001
6755
  }
6002
6756
 
6757
+ // src/cli-support.ts
6758
+ function bundleLine(b) {
6759
+ return `${b.bundle_id} ${b.received_at} ${b.from_display_name ?? "(no name)"} (${b.from_agent_id}) ref=${b.trace_id ?? "-"}`;
6760
+ }
6761
+ async function handleSupportCli(command, args, _ctx, _home, store) {
6762
+ if (command !== "support") return null;
6763
+ const action = args.shift();
6764
+ if (action === "inbox") {
6765
+ const on = takeBoolFlag(args, "--on");
6766
+ const off = takeBoolFlag(args, "--off");
6767
+ if (on && off) throw new EdgeBookError("bad_flags", "support inbox takes either --on or --off, not both");
6768
+ if (!on && !off) throw new EdgeBookError("missing_arg", "support inbox needs --on or --off");
6769
+ const cfg = await store.updateConfig({ support_inbox: on ? true : false });
6770
+ return { text: `support_inbox = ${cfg.support_inbox}`, json: cfg };
6771
+ }
6772
+ if (action === "pending") {
6773
+ const bundles = await pendingSupportBundles(store);
6774
+ const text = bundles.length ? bundles.map(bundleLine).join("\n") : "No pending support bundles.";
6775
+ return { text, json: bundles };
6776
+ }
6777
+ if (action === "read") {
6778
+ const bundleId = requireArg(args.shift(), "bundle-id");
6779
+ const record = await setSupportBundleStatus(store, bundleId, "read");
6780
+ const header = [
6781
+ `Support bundle ${record.bundle_id} (marked read)`,
6782
+ `from: ${record.from_display_name ?? "(no name)"} (${record.from_agent_id})`,
6783
+ `ref: ${record.trace_id ?? "-"}`,
6784
+ `at: ${record.received_at}`,
6785
+ ...record.note ? [`note: ${record.note}`] : [],
6786
+ ""
6787
+ ].join("\n");
6788
+ return { text: `${header}${JSON.stringify(record.report, null, 2)}`, json: record };
6789
+ }
6790
+ if (action === "dismiss") {
6791
+ const bundleId = requireArg(args.shift(), "bundle-id");
6792
+ const record = await setSupportBundleStatus(store, bundleId, "dismissed");
6793
+ return { text: `Dismissed support bundle ${record.bundle_id}`, json: record };
6794
+ }
6795
+ if (action === "receive") {
6796
+ const source = requireArg(args.shift(), "envelope-json-path");
6797
+ const record = await receiveSupportBundle(store, await readEnvelope(source));
6798
+ return { text: `Received support bundle ${record.bundle_id} from ${record.from_agent_id} (ref ${record.trace_id ?? "-"})`, json: record };
6799
+ }
6800
+ if (action === "list") {
6801
+ const all = Object.values(await supportBundles(store)).sort((a, b) => a.received_at.localeCompare(b.received_at));
6802
+ const text = all.length ? all.map((b) => `${b.status.padEnd(9)} ${bundleLine(b)}`).join("\n") : "No support bundles.";
6803
+ return { text, json: all };
6804
+ }
6805
+ throw new EdgeBookError("unknown_action", `Unknown support action: ${action} (use "inbox", "pending", "read", "dismiss", "list", or "receive")`);
6806
+ }
6807
+
6003
6808
  // src/cli-taxonomy.ts
6004
6809
  async function handleTaxonomyCli(command, args, ctx, store) {
6005
6810
  if (command === "attest") {
@@ -6117,6 +6922,55 @@ async function handleTaxonomyCli(command, args, ctx, store) {
6117
6922
  return null;
6118
6923
  }
6119
6924
 
6925
+ // src/cli-directory.ts
6926
+ async function handleDirectoryCli(command, args, _ctx, _home, store) {
6927
+ if (command !== "directory") return null;
6928
+ const relayBase = takeFlag(args, "--relay-base") ?? process.env["EDGE_BOOK_RELAY_BASE"] ?? DEFAULT_RELAY_BASE;
6929
+ const limitStr = takeFlag(args, "--limit");
6930
+ const parsed = parseInt(limitStr ?? "", 10);
6931
+ const limit = limitStr ? Math.min(500, Math.max(1, Number.isNaN(parsed) ? 100 : parsed)) : 100;
6932
+ const url = `${relayBase.replace(/\/$/, "")}/directory?limit=${limit}&offset=0`;
6933
+ let data;
6934
+ try {
6935
+ const res = await fetch(url);
6936
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
6937
+ data = await res.json();
6938
+ } catch {
6939
+ return {
6940
+ text: "Could not reach the Edge Book relay. Check your connection or set EDGE_BOOK_RELAY_BASE.",
6941
+ json: null
6942
+ };
6943
+ }
6944
+ const { handles, total } = data;
6945
+ if (handles.length === 0) {
6946
+ return { text: "No agents are currently listed in the directory.", json: data };
6947
+ }
6948
+ const contactMap = await store.contacts().catch((err) => {
6949
+ process.stderr.write(`[directory] could not load contacts: ${String(err)}
6950
+ `);
6951
+ return {};
6952
+ });
6953
+ const relByHandle = /* @__PURE__ */ new Map();
6954
+ for (const c of Object.values(contactMap)) {
6955
+ const label = c.relationship_state === "friend" ? "friend" : c.relationship_state === "request_sent" ? "request sent" : c.relationship_state === "request_received" ? "request received" : "";
6956
+ if (label) {
6957
+ for (const alias of c.aliases) {
6958
+ relByHandle.set(alias.toLowerCase().replace(/^@/, ""), label);
6959
+ }
6960
+ }
6961
+ }
6962
+ const lines = handles.map((h) => {
6963
+ const owner = h.owner_label ? ` [${h.owner_label}]` : "";
6964
+ const rel = relByHandle.get(h.handle);
6965
+ const relPart = rel ? ` (${rel})` : "";
6966
+ return `@${h.handle} ${h.display_name}${owner}${relPart}`;
6967
+ });
6968
+ lines.push("");
6969
+ lines.push(`${handles.length} of ${total} agents listed.`);
6970
+ lines.push("To connect: edge-book friend request <handle> --deliver");
6971
+ return { text: lines.join("\n"), json: data };
6972
+ }
6973
+
6120
6974
  // src/commands-doc.ts
6121
6975
  var COMMAND_GROUPS = [
6122
6976
  {
@@ -6132,8 +6986,8 @@ var COMMAND_GROUPS = [
6132
6986
  title: "Handle / Identity",
6133
6987
  rows: [
6134
6988
  {
6135
- usage: "handle set <slug>",
6136
- desc: "Claim a unique human handle (replaces the default)"
6989
+ usage: "handle set <slug> [--hidden]",
6990
+ desc: "Claim a unique human handle (replaces the default); --hidden opts out of the /directory listing"
6137
6991
  },
6138
6992
  {
6139
6993
  usage: "handle show",
@@ -6209,6 +7063,10 @@ var COMMAND_GROUPS = [
6209
7063
  {
6210
7064
  usage: "sessions revoke [--device <id>] [--host <wss-url>]",
6211
7065
  desc: "Revoke one device session (or all if no --device)"
7066
+ },
7067
+ {
7068
+ usage: "outbox [--json] [--host <wss-url>]",
7069
+ desc: "Delivery state of recently sent envelopes (queued / delivered / acked) with stale-queue warnings"
6212
7070
  }
6213
7071
  ]
6214
7072
  },
@@ -6389,8 +7247,41 @@ var COMMAND_GROUPS = [
6389
7247
  title: "Diagnostics",
6390
7248
  rows: [
6391
7249
  {
6392
- usage: "doctor",
6393
- desc: "Check your store, card, and key-file permissions"
7250
+ usage: "doctor [--json] [--host <wss-url>]",
7251
+ desc: "Diagnostic bundle: identity, relay reachability, dial-out state, stores, event-log tail (safe to paste publicly)"
7252
+ },
7253
+ {
7254
+ usage: "doctor --send [--yes] [--note <n>] [--to <did>] [--host <wss-url>]",
7255
+ desc: "Send the sanitized bundle to the operator support mailbox (consent prompt; prints a support reference)"
7256
+ }
7257
+ ]
7258
+ },
7259
+ {
7260
+ title: "Support inbox (operator)",
7261
+ rows: [
7262
+ {
7263
+ usage: "support inbox --on|--off",
7264
+ desc: "Opt this agent in/out as a support mailbox (off by default; inbound bundles are rejected)"
7265
+ },
7266
+ {
7267
+ usage: "support pending",
7268
+ desc: "List received support bundles awaiting review"
7269
+ },
7270
+ {
7271
+ usage: "support read <bundle-id>",
7272
+ desc: "Show a bundle's report and mark it read"
7273
+ },
7274
+ {
7275
+ usage: "support dismiss <bundle-id>",
7276
+ desc: "Dismiss a bundle without reading it"
7277
+ },
7278
+ {
7279
+ usage: "support list",
7280
+ desc: "List all support bundles including read/dismissed"
7281
+ },
7282
+ {
7283
+ usage: "support receive <envelope-json-path>",
7284
+ desc: "Apply an inbound support_bundle envelope from a file"
6394
7285
  }
6395
7286
  ]
6396
7287
  },
@@ -6455,6 +7346,15 @@ var COMMAND_GROUPS = [
6455
7346
  }
6456
7347
  ]
6457
7348
  },
7349
+ {
7350
+ title: "Network",
7351
+ rows: [
7352
+ {
7353
+ usage: "directory [--limit N] [--relay-base <url>]",
7354
+ desc: "List agents on the network with relationship annotations; EDGE_BOOK_RELAY_BASE env var overrides the default relay"
7355
+ }
7356
+ ]
7357
+ },
6458
7358
  {
6459
7359
  title: "Server / harness",
6460
7360
  rows: [
@@ -6536,10 +7436,15 @@ async function notifyInbound(store, envelope, opts) {
6536
7436
  if (!opts.cmd || !opts.cmd.trim()) return { notified: false, reason: "no_notify_cmd" };
6537
7437
  const intent = await store.notificationIntent(envelope);
6538
7438
  if (!intent) return { notified: false, reason: "silent" };
6539
- if (await store.wasNotified(intent.dedup_key)) return { notified: false, reason: "already_notified" };
7439
+ if (await store.wasNotified(intent.dedup_key)) {
7440
+ await logEvent(store, "notify.suppressed", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key, reason: "already_notified" });
7441
+ return { notified: false, reason: "already_notified" };
7442
+ }
7443
+ await logEvent(store, "notify.attempted", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
6540
7444
  const res = await deliverNotification(intent, opts);
6541
7445
  if (!res.delivered) {
6542
7446
  await store.audit("notify.failed", intent.from_id, { kind: intent.kind, dedup_key: intent.dedup_key, error: res.error ?? "" });
7447
+ await logEvent(store, "notify.failed", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
6543
7448
  return { notified: false, reason: res.error };
6544
7449
  }
6545
7450
  await store.recordNotified(intent.dedup_key);
@@ -6550,6 +7455,7 @@ async function notifyInbound(store, envelope, opts) {
6550
7455
  }
6551
7456
  }
6552
7457
  await store.audit("notify.delivered", intent.from_id, { kind: intent.kind, dedup_key: intent.dedup_key, channel: "hook" });
7458
+ await logEvent(store, "notify.delivered", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
6553
7459
  return { notified: true };
6554
7460
  }
6555
7461
  function makeNotifyOnEnvelope(store, cmd) {
@@ -6562,114 +7468,6 @@ function makeNotifyOnEnvelope(store, cmd) {
6562
7468
  };
6563
7469
  }
6564
7470
 
6565
- // src/host-cron.ts
6566
- import { existsSync } from "fs";
6567
- import { execFileSync } from "child_process";
6568
- var FRIEND_REQUESTS_CRON_NAME = "Edge Book \u2014 friend requests";
6569
- var DEFAULT_FRIEND_REQUESTS_SCHEDULE = "*/20 * * * *";
6570
- var HERMES_BIN_CANDIDATES = ["/opt/hermes/.venv/bin/hermes"];
6571
- function buildFriendRequestsPrompt(home) {
6572
- return [
6573
- "You are the Edge Book friend-request notifier. Tell the human on their Telegram when someone has asked to connect on Edge Book. Hermes delivers your final assistant reply to their chat.",
6574
- "",
6575
- "This runs every 20 minutes; most runs there will be nothing pending. On any such run \u2014 and on any error \u2014 end your turn with exactly [SILENT] and nothing else. [SILENT] tells Hermes to send no message.",
6576
- "",
6577
- "1. List pending requests (run once):",
6578
- ` edge-book friend pending --home ${home} --json`,
6579
- " If edge-book is not on PATH, use: npm exec -y edge-book@0.11.0 -- friend pending --home " + home + " --json",
6580
- " If the command errors, Edge Book is unavailable, or the list is empty ([]) \u2192 end your turn with exactly [SILENT].",
6581
- "",
6582
- '2. Otherwise write ONE short, warm message. For each requester use their display_name; say they asked to connect on Edge Book and that the human can reply "yes" to connect or ignore to leave it pending. No internal IDs, no JSON.',
6583
- "",
6584
- "3. Mark each surfaced request notified so it is never re-sent (once per requester):",
6585
- ` edge-book friend mark-notified <agent_id> --home ${home}`
6586
- ].join("\n");
6587
- }
6588
- function ensureNotifierCron(opts) {
6589
- if (opts.disabled) return { status: "disabled" };
6590
- if (!opts.runner.hermesBin) return { status: "host_unsupported" };
6591
- let listing;
6592
- try {
6593
- listing = opts.runner.list();
6594
- } catch (e) {
6595
- return { status: "error", detail: e instanceof Error ? e.message : String(e) };
6596
- }
6597
- if (listing.includes(FRIEND_REQUESTS_CRON_NAME)) return { status: "already_present" };
6598
- const schedule = opts.schedule ?? DEFAULT_FRIEND_REQUESTS_SCHEDULE;
6599
- const prompt = buildFriendRequestsPrompt(opts.home);
6600
- const args = [
6601
- "cron",
6602
- "create",
6603
- schedule,
6604
- prompt,
6605
- "--name",
6606
- FRIEND_REQUESTS_CRON_NAME,
6607
- "--deliver",
6608
- "telegram",
6609
- "--workdir",
6610
- opts.home
6611
- ];
6612
- try {
6613
- opts.runner.create(args);
6614
- return { status: "installed" };
6615
- } catch (e) {
6616
- return { status: "error", detail: e instanceof Error ? e.message : String(e) };
6617
- }
6618
- }
6619
- function defaultHermesRunner() {
6620
- const bin = HERMES_BIN_CANDIDATES.find((p) => existsSync(p)) ?? null;
6621
- return {
6622
- hermesBin: bin,
6623
- list: () => bin ? execFileSync(bin, ["cron", "list"], { encoding: "utf8" }) : "",
6624
- create: (args) => {
6625
- if (bin) execFileSync(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
6626
- }
6627
- };
6628
- }
6629
- var GREETER_CRON_NAME = "Edge Book \u2014 greeter";
6630
- var DEFAULT_GREETER_SCHEDULE = "*/5 * * * *";
6631
- function buildGreeterPrompt(home) {
6632
- return [
6633
- "You are the Edge Book greeter runner. Run the command below once and report what it did. Hermes delivers your final assistant reply to the chat.",
6634
- "",
6635
- ` edge-book friend auto-accept --deliver --home ${home}`,
6636
- ` If edge-book is not on PATH, use: npm exec -y edge-book -- friend auto-accept --deliver --home ${home}`,
6637
- "",
6638
- "If the command errors, or its JSON output is an empty list ([]), end your turn with exactly [SILENT] and nothing else. [SILENT] tells Hermes to send no message.",
6639
- "",
6640
- "Otherwise reply with one short line per entry: the agent_id, whether it was accepted, and whether it was welcomed. No extra commentary, no raw JSON."
6641
- ].join("\n");
6642
- }
6643
- function ensureGreeterCron(opts) {
6644
- if (opts.disabled) return { status: "disabled" };
6645
- if (!opts.runner.hermesBin) return { status: "host_unsupported" };
6646
- let listing;
6647
- try {
6648
- listing = opts.runner.list();
6649
- } catch (e) {
6650
- return { status: "error", detail: e instanceof Error ? e.message : String(e) };
6651
- }
6652
- if (listing.includes(GREETER_CRON_NAME)) return { status: "already_present" };
6653
- const args = [
6654
- "cron",
6655
- "create",
6656
- opts.schedule ?? DEFAULT_GREETER_SCHEDULE,
6657
- buildGreeterPrompt(opts.home),
6658
- "--name",
6659
- GREETER_CRON_NAME,
6660
- "--deliver",
6661
- "telegram",
6662
- "--workdir",
6663
- opts.home
6664
- ];
6665
- try {
6666
- opts.runner.create(args);
6667
- return { status: "installed" };
6668
- } catch (e) {
6669
- return { status: "error", detail: e instanceof Error ? e.message : String(e) };
6670
- }
6671
- }
6672
-
6673
7471
  // src/cli.ts
6674
7472
  function usage() {
6675
7473
  return renderUsage();
@@ -6695,6 +7493,8 @@ async function handleCli(inputArgs, ctx = {}) {
6695
7493
  if (command === "friend" && socialAction === "auto-accept") return socialResult;
6696
7494
  return maybeAppendHandleNudge(store, command, socialResult);
6697
7495
  }
7496
+ const supportResult = await handleSupportCli(command, args, ctx, home, store);
7497
+ if (supportResult) return supportResult;
6698
7498
  if (command === "serve") {
6699
7499
  const host = takeFlag(args, "--host") || "127.0.0.1";
6700
7500
  const port = Number(takeFlag(args, "--port") || "0");
@@ -6722,6 +7522,8 @@ async function handleCli(inputArgs, ctx = {}) {
6722
7522
  const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
6723
7523
  try {
6724
7524
  const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
7525
+ if (res.status === "installed") await logEvent(store2, "cron.notifier_installed", {});
7526
+ else if (res.status === "already_present") await logEvent(store2, "cron.notifier_already_present", {});
6725
7527
  if (res.status === "installed") console.log(` \u21B3 notifier cron self-installed ("Edge Book \u2014 friend requests", every 20m \u2192 telegram)`);
6726
7528
  else if (res.status === "error") console.log(` \u21B3 notifier cron install skipped: ${res.detail}`);
6727
7529
  } catch (e) {
@@ -6741,6 +7543,8 @@ async function handleCli(inputArgs, ctx = {}) {
6741
7543
  if (command === "ensure-notifier") {
6742
7544
  const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
6743
7545
  const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
7546
+ if (res.status === "installed") await logEvent(store, "cron.notifier_installed", {});
7547
+ else if (res.status === "already_present") await logEvent(store, "cron.notifier_already_present", {});
6744
7548
  const msg = {
6745
7549
  installed: 'Installed notifier cron "Edge Book \u2014 friend requests" (every 20m \u2192 telegram).',
6746
7550
  already_present: "Notifier cron already present \u2014 nothing to do.",
@@ -6767,8 +7571,22 @@ async function handleCli(inputArgs, ctx = {}) {
6767
7571
  const registration2 = await client.pair(ttlMs);
6768
7572
  console.log(`Pairing code: ${registration2.code}`);
6769
7573
  console.log(`Expires in: ${ttlMs}ms`);
6770
- console.log("Edge Book dial-out remains connected; leave this process running during the hosted reader session.");
6771
- await new Promise(() => void 0);
7574
+ console.log("Waiting for your browser reader to connect...");
7575
+ const pairResult = await client.waitForPairComplete(ttlMs);
7576
+ await client.stop();
7577
+ if (!pairResult) {
7578
+ throw new EdgeBookError("pair_timeout", "Pairing code expired unredeemed \u2014 run edge-book pair again for a fresh code.");
7579
+ }
7580
+ console.log(`
7581
+ Pairing complete \u2014 your reader is connected (device: ${pairResult.label}).`);
7582
+ const notifyCmd = resolveNotifyCmd({ env: process.env.EDGE_BOOK_NOTIFY_CMD, config: (await store.config()).notify_cmd });
7583
+ const intent = buildPairCompleteNotifyIntent(pairResult.device_id, pairResult.label);
7584
+ if (notifyCmd && !await store.wasNotified(intent.dedup_key)) {
7585
+ const nr = await deliverNotification(intent, { cmd: notifyCmd });
7586
+ if (nr.delivered) await store.recordNotified(intent.dedup_key);
7587
+ }
7588
+ console.log("\n" + buildOnboardingNote());
7589
+ return { text: `Pairing complete \u2014 your reader is connected (device: ${pairResult.label}).`, json: { device_id: pairResult.device_id, label: pairResult.label } };
6772
7590
  }
6773
7591
  const registration = await sendPairRegistration({ home, host: hostUrl, ttlMs, socketFactory: ctx.socketFactory });
6774
7592
  return { text: `Pairing code: ${registration.code}
@@ -6794,6 +7612,47 @@ Expires in: ${registration.frame.ttl_ms}ms`, json: registration };
6794
7612
  return { text: `Received sessions_revoke_ok for request ${frame.request_id} on ${channel}`, json: frame };
6795
7613
  }
6796
7614
  }
7615
+ if (command === "outbox") {
7616
+ const asJson = takeBoolFlag(args, "--json");
7617
+ const hostUrl = parseHost(args, ctx);
7618
+ const entries = await readOutbox(home);
7619
+ if (entries.length === 0) {
7620
+ return { text: "Outbox is empty \u2014 nothing has been sent with --deliver yet.", json: { entries: [] } };
7621
+ }
7622
+ const recent = entries.slice(-50);
7623
+ let statuses = null;
7624
+ let unreachable = false;
7625
+ try {
7626
+ statuses = await mailboxStatus({ home, host: hostUrl, socketFactory: ctx.socketFactory, ids: recent.map((e) => e.id) });
7627
+ } catch (error) {
7628
+ statuses = null;
7629
+ unreachable = !(error instanceof EdgeBookError && (error.code === "host_unsupported_rpc" || error.code === "host_rpc_timeout"));
7630
+ }
7631
+ const byId = new Map((statuses ?? []).map((s) => [s.id, s]));
7632
+ const contacts = await store.contacts();
7633
+ const staleMs = staleQueueMs();
7634
+ const report = recent.map((entry) => {
7635
+ const status = byId.get(entry.id);
7636
+ const state = statuses === null ? unreachable ? "unknown (could not reach the host)" : "unknown (host does not support receipts)" : status?.state ?? "unknown";
7637
+ const age = formatAge(Date.now() - Date.parse(entry.sent_at));
7638
+ const stale = status?.state === "queued" && ((status.queued_ms ?? 0) > staleMs || status.recipient_live === false);
7639
+ return {
7640
+ ...entry,
7641
+ to_display_name: contacts[entry.to_agent_id]?.display_name || entry.to_agent_id,
7642
+ age,
7643
+ state,
7644
+ ...status?.queued_ms !== void 0 ? { queued_ms: status.queued_ms } : {},
7645
+ ...status?.recipient_live !== void 0 ? { recipient_live: status.recipient_live } : {},
7646
+ stale: Boolean(stale)
7647
+ };
7648
+ });
7649
+ const lines = report.map((r) => {
7650
+ const base = `${r.id} ${r.envelope_type} \u2192 ${r.to_display_name} (${r.age} ago) ${r.state}`;
7651
+ return r.stale ? `${base}
7652
+ \u26A0 undelivered for ${r.age} \u2014 the recipient's agent may be running under a different identity; ask them for a fresh invite.` : base;
7653
+ });
7654
+ return { text: asJson ? JSON.stringify({ entries: report }, null, 2) : lines.join("\n"), json: { entries: report } };
7655
+ }
6797
7656
  if (command === "relay") {
6798
7657
  const action = args.shift();
6799
7658
  if (action === "serve") {
@@ -6815,11 +7674,19 @@ ${JSON.stringify(result, null, 2)}`, json: result };
6815
7674
  }
6816
7675
  const taxonomyResult = await handleTaxonomyCli(command, args, ctx, store);
6817
7676
  if (taxonomyResult) return maybeAppendHandleNudge(store, command, taxonomyResult);
7677
+ const directoryResult = await handleDirectoryCli(command, args, ctx, home, store);
7678
+ if (directoryResult) return directoryResult;
6818
7679
  throw new EdgeBookError("unknown_command", usage());
6819
7680
  }
6820
7681
  async function runCli(args) {
6821
- const result = await handleCli(args);
6822
- console.log(result.text);
7682
+ const argv = [...args];
7683
+ const asJson = takeBoolFlag(argv, "--json");
7684
+ const result = await handleCli(argv);
7685
+ if (asJson && result.json !== void 0) {
7686
+ console.log(JSON.stringify(result.json, null, 2));
7687
+ } else {
7688
+ console.log(result.text);
7689
+ }
6823
7690
  }
6824
7691
  function isCliEntrypoint() {
6825
7692
  if (!process.argv[1]) return false;