edge-book 0.14.0 → 0.15.1

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 +12 -2
  2. package/dist/edge-book.js +1079 -299
  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 path9 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
@@ -405,6 +406,8 @@ var REPORTS_FILE = "reports.json";
405
406
  var INVITE_CODES_FILE = "invite-codes.json";
406
407
  var INBOUND_RATE_FILE = "inbound-rate.json";
407
408
  var OUTBOX_FILE = "outbox.json";
409
+ var EVENTS_FILE = "events.ndjson";
410
+ var SUPPORT_BUNDLES_FILE = "support-bundles.json";
408
411
  var ATTESTATIONS_FILE = "attestations.json";
409
412
  var ENDORSEMENTS_FILE = "endorsements.json";
410
413
  var SIGNALS_FILE = "signals.json";
@@ -932,6 +935,7 @@ async function canReadObject(store, objectId, subjectAgentId, at = Date.now()) {
932
935
  for (const grant of candidates) {
933
936
  if (await store.verifyGrantSignature(grant)) return true;
934
937
  }
938
+ await store.audit("object.read.denied", subjectAgentId, { object_id: objectId, scope: "object.read" });
935
939
  return false;
936
940
  }
937
941
  async function readObject(store, objectId, subjectAgentId) {
@@ -971,6 +975,7 @@ async function shareObjectEnvelope(store, peerAgentId, objectId, expiresAt = "")
971
975
  if (object.attachment) {
972
976
  attachment_b64 = (await fs5.readFile(store.file(object.attachment.ref))).toString("base64");
973
977
  }
978
+ await store.audit("object.share", peerAgentId, { object_id: objectId, grant_id: grant.grant_id, scope: "object.read" });
974
979
  return store.signEnvelope({
975
980
  type: "object_share",
976
981
  to_agent_id: peerAgentId,
@@ -1016,7 +1021,7 @@ async function revokeObjectGrant(store, objectId, subjectAgentId) {
1016
1021
  }
1017
1022
  if (revoked.length) {
1018
1023
  await store.saveGrants(grants);
1019
- await store.audit("grant.revoke", subjectAgentId, { object_id: objectId, grant_ids: revoked });
1024
+ await store.audit("grant.revoke", subjectAgentId, { object_id: objectId, grant_ids: revoked, scope: "object.read" });
1020
1025
  }
1021
1026
  return revoked;
1022
1027
  }
@@ -1504,6 +1509,74 @@ async function expireEscalations(store) {
1504
1509
  if (changed) await store.saveEscalations(all);
1505
1510
  }
1506
1511
 
1512
+ // src/event-log.ts
1513
+ import crypto4 from "crypto";
1514
+ import fs6 from "fs/promises";
1515
+ import path6 from "path";
1516
+ var MAX_EVENT_LINES = 2e3;
1517
+ var COMPACT_KEEP_LINES = 1e3;
1518
+ function eventErrorCode(e) {
1519
+ if (e instanceof Error) {
1520
+ const code = e.code;
1521
+ if (typeof code === "string" && code.length > 0) return code;
1522
+ return e.constructor.name || "Error";
1523
+ }
1524
+ return "unknown_error";
1525
+ }
1526
+ async function logEvent(store, kind, fields = {}, caps = {}) {
1527
+ try {
1528
+ const event = { ts: (/* @__PURE__ */ new Date()).toISOString(), kind, ...fields };
1529
+ const file = store.file(EVENTS_FILE);
1530
+ await fs6.mkdir(path6.dirname(file), { recursive: true });
1531
+ await fs6.appendFile(file, `${JSON.stringify(event)}
1532
+ `, "utf8");
1533
+ await compactIfNeeded(file, caps.maxLines ?? MAX_EVENT_LINES, caps.keepLines ?? COMPACT_KEEP_LINES);
1534
+ } catch {
1535
+ }
1536
+ }
1537
+ async function compactIfNeeded(file, maxLines, keepLines) {
1538
+ const text = await fs6.readFile(file, "utf8");
1539
+ const lines = text.split("\n").filter((l) => l.length > 0);
1540
+ if (lines.length <= maxLines) return;
1541
+ const kept = lines.slice(-keepLines);
1542
+ const tmp = `${file}.tmp-${crypto4.randomBytes(6).toString("hex")}`;
1543
+ try {
1544
+ await fs6.writeFile(tmp, `${kept.join("\n")}
1545
+ `, "utf8");
1546
+ await fs6.rename(tmp, file);
1547
+ } catch (error) {
1548
+ await fs6.rm(tmp, { force: true }).catch(() => void 0);
1549
+ throw error;
1550
+ }
1551
+ }
1552
+ async function readEvents(store, limit) {
1553
+ let text;
1554
+ try {
1555
+ text = await fs6.readFile(store.file(EVENTS_FILE), "utf8");
1556
+ } catch (error) {
1557
+ if (error.code === "ENOENT") return [];
1558
+ return [];
1559
+ }
1560
+ const out = [];
1561
+ for (const line of text.split("\n")) {
1562
+ if (!line) continue;
1563
+ try {
1564
+ const parsed = JSON.parse(line);
1565
+ if (parsed && typeof parsed === "object" && typeof parsed.kind === "string") out.push(parsed);
1566
+ } catch {
1567
+ }
1568
+ }
1569
+ return limit !== void 0 && out.length > limit ? out.slice(-limit) : out;
1570
+ }
1571
+ async function lastEvent(store, kind) {
1572
+ const events = await readEvents(store);
1573
+ for (let i = events.length - 1; i >= 0; i--) {
1574
+ const e = events[i];
1575
+ if (kind.endsWith(".") ? e.kind.startsWith(kind) : e.kind === kind) return e;
1576
+ }
1577
+ return void 0;
1578
+ }
1579
+
1507
1580
  // src/store-friends.ts
1508
1581
  async function upsertContactFromCard(store, card, state) {
1509
1582
  validateCard(card);
@@ -1570,6 +1643,7 @@ async function setRelationship(store, peerAgentId, nextState, type, reason = "")
1570
1643
  const event = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
1571
1644
  await appendJsonl(store.file(RELATIONSHIP_EVENTS_FILE), event);
1572
1645
  await store.audit(`relationship.${type}`, peerAgentId, { previous, next: nextState, reason });
1646
+ await logEvent(store, "friend.state_changed", { peer: peerAgentId, previous, next: nextState, event_type: type });
1573
1647
  return event;
1574
1648
  }
1575
1649
  async function createFriendRequest(store, targetCard, note = "", inviteCode = "") {
@@ -1609,6 +1683,7 @@ async function receiveFriendRequest(store, envelope) {
1609
1683
  }
1610
1684
  const contact = await store.upsertContactFromCard(body.card, "request_received");
1611
1685
  await store.setRelationship(envelope.from_agent_id, "request_received", "FriendRequest", body.note);
1686
+ await logEvent(store, "friend.request_received", { from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
1612
1687
  await appendJsonl(store.file(INBOX_FILE), envelope);
1613
1688
  const existingApprovals = await store.approvals();
1614
1689
  const alreadyPending = Object.values(existingApprovals).some(
@@ -1652,6 +1727,7 @@ async function acceptFriend(store, peerAgentId, reason = "accepted") {
1652
1727
  if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
1653
1728
  if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked_peer", "Cannot accept a blocked peer");
1654
1729
  await store.setRelationship(peerAgentId, "friend", "Accept", reason);
1730
+ await logEvent(store, "friend.accepted", { peer: peerAgentId });
1655
1731
  const grant = await store.issueGrant(peerAgentId, ["message.friend", "feed.read.friends", "profile.read.friend", "escalation.raise"]);
1656
1732
  const card = await store.writeCard();
1657
1733
  const profile = await store.buildFriendProfile();
@@ -1749,13 +1825,18 @@ async function broadcastProfileEnvelopes(store) {
1749
1825
  async function revoke(store, peerAgentId) {
1750
1826
  await store.setRelationship(peerAgentId, "revoked", "Revoke", "revoked");
1751
1827
  const grants = await store.grants();
1828
+ const revoked = [];
1829
+ const scopes = /* @__PURE__ */ new Set();
1752
1830
  for (const grant of Object.values(grants)) {
1753
1831
  if (grant.subject_agent_id === peerAgentId || grant.issuer_agent_id === peerAgentId) {
1754
1832
  grant.status = "revoked";
1755
1833
  grant.revoked_at = now2();
1834
+ revoked.push(grant.grant_id);
1835
+ for (const scope of grant.scopes) scopes.add(scope);
1756
1836
  }
1757
1837
  }
1758
1838
  await store.saveGrants(grants);
1839
+ if (revoked.length) await store.audit("grant.revoke", peerAgentId, { grant_ids: revoked, scopes: [...scopes].sort() });
1759
1840
  }
1760
1841
  async function block(store, peerAgentId) {
1761
1842
  await store.setRelationship(peerAgentId, "blocked", "Block", "blocked");
@@ -1829,8 +1910,8 @@ async function consumeInviteCode(store, code) {
1829
1910
  }
1830
1911
 
1831
1912
  // src/store-identity.ts
1832
- import crypto4 from "crypto";
1833
- import fs6 from "fs/promises";
1913
+ import crypto5 from "crypto";
1914
+ import fs7 from "fs/promises";
1834
1915
  async function init(store, input = {}) {
1835
1916
  await ensureHome(store.home);
1836
1917
  const existing = await readJson(store.file(IDENTITY_FILE), null);
@@ -1838,7 +1919,7 @@ async function init(store, input = {}) {
1838
1919
  await store.updateConfig({ direct_url: input.directUrl, relay_url: input.relayUrl });
1839
1920
  return existing;
1840
1921
  }
1841
- const { publicKey, privateKey } = crypto4.generateKeyPairSync("ed25519");
1922
+ const { publicKey, privateKey } = crypto5.generateKeyPairSync("ed25519");
1842
1923
  const public_key_pem = publicKey.export({ type: "spki", format: "pem" }).toString();
1843
1924
  const private_key_pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
1844
1925
  const identity = {
@@ -1898,16 +1979,21 @@ async function setProfile(store, input) {
1898
1979
  await store.audit("identity.update", identity.agent_id, { display_name: identity.display_name, profile_version: profile.profile_version });
1899
1980
  return identity;
1900
1981
  }
1901
- async function setHandle(store, handle) {
1982
+ async function setHandle(store, handle, opts = {}) {
1902
1983
  if (!isValidHandle(handle)) {
1903
1984
  throw new EdgeBookError("invalid_handle", `invalid_handle: must be 3-30 chars [a-z0-9-], not reserved: ${handle}`);
1904
1985
  }
1905
1986
  const identity = await store.identity();
1906
1987
  identity.handle = handle;
1988
+ if (opts.discoverable === false) {
1989
+ identity.handle_discoverable = false;
1990
+ } else {
1991
+ delete identity.handle_discoverable;
1992
+ }
1907
1993
  identity.updated_at = now2();
1908
1994
  await writeJson(store.file(IDENTITY_FILE), identity, 384);
1909
1995
  await store.writeCard();
1910
- await store.audit("identity.set_handle", identity.agent_id, { handle });
1996
+ await store.audit("identity.set_handle", identity.agent_id, { handle, discoverable: opts.discoverable !== false });
1911
1997
  return identity;
1912
1998
  }
1913
1999
  async function exportIdentity(store) {
@@ -1943,6 +2029,7 @@ async function updateConfig(store, input) {
1943
2029
  if (input.inbound_window_ms !== void 0) next.inbound_window_ms = input.inbound_window_ms;
1944
2030
  if (input.handle_nudge_at !== void 0) next.handle_nudge_at = input.handle_nudge_at;
1945
2031
  if (input.greeter_mode !== void 0) next.greeter_mode = input.greeter_mode;
2032
+ if (input.support_inbox !== void 0) next.support_inbox = input.support_inbox;
1946
2033
  if (input.greeter_welcome_object_id !== void 0) next.greeter_welcome_object_id = input.greeter_welcome_object_id;
1947
2034
  await writeJson(store.file(CONFIG_FILE), next);
1948
2035
  return next;
@@ -1974,7 +2061,7 @@ async function buildCard(store, cardUrl) {
1974
2061
  refresh_after: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString(),
1975
2062
  expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString()
1976
2063
  };
1977
- const card_hash = crypto4.createHash("sha256").update(canonicalize(unsigned)).digest("base64url");
2064
+ const card_hash = crypto5.createHash("sha256").update(canonicalize(unsigned)).digest("base64url");
1978
2065
  const withHash = { ...unsigned, card_hash };
1979
2066
  return { ...withHash, signature: signPayload(withHash, identity.private_key_pem) };
1980
2067
  }
@@ -1991,7 +2078,7 @@ async function buildHandleClaim(store) {
1991
2078
  const card = await loadCard(store.file(CARD_FILE));
1992
2079
  const claimed_at = Date.now();
1993
2080
  const claim_sig = signPayload({ handle: identity.handle, agent_did: identity.agent_id, claimed_at }, identity.private_key_pem);
1994
- return { handle: identity.handle, agent_did: identity.agent_id, card, claimed_at, claim_sig };
2081
+ return { handle: identity.handle, agent_did: identity.agent_id, card, claimed_at, claim_sig, discoverable: identity.handle_discoverable !== false };
1995
2082
  }
1996
2083
  async function buildFriendProfile(store) {
1997
2084
  const identity = await store.identity();
@@ -2019,7 +2106,7 @@ async function doctor(store) {
2019
2106
  const files = {};
2020
2107
  for (const name of requiredFiles) {
2021
2108
  try {
2022
- const stat = await fs6.stat(store.file(name));
2109
+ const stat = await fs7.stat(store.file(name));
2023
2110
  files[name] = {
2024
2111
  exists: true,
2025
2112
  mode: `0${(stat.mode & 511).toString(8)}`
@@ -2119,6 +2206,79 @@ async function exportLocalData(store) {
2119
2206
  };
2120
2207
  }
2121
2208
 
2209
+ // src/store-support.ts
2210
+ var SUPPORT_BUNDLE_MAX_BYTES = 256 * 1024;
2211
+ var SUPPORT_BUNDLE_KEEP = 200;
2212
+ function pruneSupportBundles(all, keepId) {
2213
+ const records = Object.values(all);
2214
+ if (records.length <= SUPPORT_BUNDLE_KEEP) return all;
2215
+ const byAge = (a, b) => a.received_at.localeCompare(b.received_at);
2216
+ const evictable = [
2217
+ ...records.filter((r) => r.status === "dismissed" && r.bundle_id !== keepId).sort(byAge),
2218
+ ...records.filter((r) => r.status !== "dismissed" && r.bundle_id !== keepId).sort(byAge)
2219
+ ];
2220
+ for (const r of evictable) {
2221
+ if (Object.keys(all).length <= SUPPORT_BUNDLE_KEEP) break;
2222
+ delete all[r.bundle_id];
2223
+ }
2224
+ return all;
2225
+ }
2226
+ async function supportBundles(store) {
2227
+ return readJson(store.file(SUPPORT_BUNDLES_FILE), {});
2228
+ }
2229
+ async function saveSupportBundles(store, all) {
2230
+ await writeJson(store.file(SUPPORT_BUNDLES_FILE), all);
2231
+ }
2232
+ async function receiveSupportBundle(store, envelope) {
2233
+ if ((await store.config()).support_inbox !== true) {
2234
+ throw new EdgeBookError("support_inbox_disabled", "This agent does not accept support bundles (operator opt-in: edge-book support inbox --on)");
2235
+ }
2236
+ if (envelope.type !== "support_bundle") throw new EdgeBookError("wrong_message_type", "Expected support_bundle envelope");
2237
+ const size = Buffer.byteLength(JSON.stringify(envelope), "utf8");
2238
+ if (size > SUPPORT_BUNDLE_MAX_BYTES) {
2239
+ throw new EdgeBookError("support_bundle_too_large", `Support bundle is ${size} bytes; the receiver cap is ${SUPPORT_BUNDLE_MAX_BYTES} (256 KiB)`);
2240
+ }
2241
+ await store.enforceInboundRate(envelope.from_agent_id);
2242
+ await store.verifyEnvelope(envelope);
2243
+ const body = envelope.body;
2244
+ if (!body || typeof body !== "object" || !body.report || typeof body.report !== "object") {
2245
+ throw new EdgeBookError("bad_support_bundle", "support_bundle envelope carries no report");
2246
+ }
2247
+ validateCard(body.card);
2248
+ if (body.card.agent_id !== envelope.from_agent_id) {
2249
+ throw new EdgeBookError("agent_id_mismatch", "Embedded card does not match the envelope sender");
2250
+ }
2251
+ const record = {
2252
+ bundle_id: envelope.message_id,
2253
+ from_agent_id: envelope.from_agent_id,
2254
+ ...body.card.display_name ? { from_display_name: body.card.display_name } : {},
2255
+ ...envelope.trace_id ? { trace_id: envelope.trace_id } : {},
2256
+ received_at: now2(),
2257
+ status: "pending",
2258
+ report: body.report,
2259
+ ...typeof body.note === "string" && body.note ? { note: body.note } : {}
2260
+ };
2261
+ const all = await supportBundles(store);
2262
+ all[record.bundle_id] = record;
2263
+ await saveSupportBundles(store, pruneSupportBundles(all, record.bundle_id));
2264
+ await store.audit("support.receive", envelope.from_agent_id, { bundle_id: record.bundle_id, trace_id: envelope.trace_id ?? "" });
2265
+ await logEvent(store, "support.received", { from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
2266
+ return record;
2267
+ }
2268
+ async function pendingSupportBundles(store) {
2269
+ const all = await supportBundles(store);
2270
+ return Object.values(all).filter((b) => b.status === "pending").sort((a, b) => a.received_at.localeCompare(b.received_at));
2271
+ }
2272
+ async function setSupportBundleStatus(store, bundleId, status) {
2273
+ const all = await supportBundles(store);
2274
+ const record = all[bundleId];
2275
+ if (!record) throw new EdgeBookError("unknown_bundle", `Unknown support bundle: ${bundleId}`);
2276
+ record.status = status;
2277
+ all[bundleId] = record;
2278
+ await saveSupportBundles(store, all);
2279
+ return record;
2280
+ }
2281
+
2122
2282
  // src/store-trust.ts
2123
2283
  async function enforceInboundRate(store, peerAgentId) {
2124
2284
  const config = await store.config();
@@ -2245,33 +2405,42 @@ async function assertGrantSignature(store, grant) {
2245
2405
  }
2246
2406
  async function signEnvelope(store, input) {
2247
2407
  const identity = await store.identity();
2408
+ const { expires_at, ...rest } = input;
2248
2409
  const unsigned = {
2249
2410
  message_id: randomId("msg"),
2250
2411
  from_agent_id: identity.agent_id,
2251
2412
  created_at: now2(),
2252
- expires_at: new Date(Date.now() + 10 * 60 * 1e3).toISOString(),
2253
- ...input
2413
+ expires_at: expires_at ?? new Date(Date.now() + 10 * 60 * 1e3).toISOString(),
2414
+ ...rest,
2415
+ // Trace correlation (ea-claude-138): every new outbound envelope carries a
2416
+ // trace_id INSIDE the signed payload (tamper-evident; back-compat — old
2417
+ // receivers canonicalize whatever fields they parsed, so verification
2418
+ // still passes). Callers may pass an existing trace_id to chain a flow;
2419
+ // it is clamped to 128 chars (mirrors the host frame rule) and an empty
2420
+ // string falls back to a generated id.
2421
+ trace_id: input.trace_id?.slice(0, 128) || randomId("trace")
2254
2422
  };
2255
2423
  return { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
2256
2424
  }
2425
+ function embeddedCardKey(envelope) {
2426
+ if (envelope.type !== "friend_request" && envelope.type !== "friend_response" && envelope.type !== "support_bundle") return void 0;
2427
+ const card = envelope.body.card;
2428
+ return card?.public_keys?.[0]?.public_key_pem;
2429
+ }
2257
2430
  async function verifyEnvelope(store, envelope) {
2258
2431
  const identity = await store.identity();
2259
2432
  if (envelope.to_agent_id !== identity.agent_id) throw new EdgeBookError("wrong_recipient", "Envelope recipient does not match local identity");
2260
2433
  if (Date.parse(envelope.expires_at) <= Date.now()) throw new EdgeBookError("expired_message", "Message is expired");
2261
2434
  const seen = await readJson(store.file(SEEN_MESSAGES_FILE), []);
2262
- if (seen.includes(envelope.message_id)) throw new EdgeBookError("replay", `Replay detected for ${envelope.message_id}`);
2263
- const contacts = await store.contacts();
2264
- let publicKey = contacts[envelope.from_agent_id]?.public_keys?.[0]?.public_key_pem;
2265
- if (!publicKey && envelope.type === "friend_request") {
2266
- const card = envelope.body.card;
2267
- publicKey = card?.public_keys?.[0]?.public_key_pem;
2268
- }
2269
- if (!publicKey && envelope.type === "friend_response") {
2270
- const card = envelope.body.card;
2271
- publicKey = card?.public_keys?.[0]?.public_key_pem;
2435
+ if (seen.includes(envelope.message_id)) {
2436
+ 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 });
2437
+ throw new EdgeBookError("replay", `Replay detected for ${envelope.message_id}`);
2272
2438
  }
2439
+ const contacts = await store.contacts();
2440
+ const publicKey = contacts[envelope.from_agent_id]?.public_keys?.[0]?.public_key_pem ?? embeddedCardKey(envelope);
2273
2441
  if (!publicKey) throw new EdgeBookError("unknown_key", `Unknown sender key for ${envelope.from_agent_id}`);
2274
2442
  if (!verifyPayload(withoutSignature(envelope), envelope.signature, publicKey)) {
2443
+ 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 });
2275
2444
  throw new EdgeBookError("invalid_signature", "Message signature is invalid");
2276
2445
  }
2277
2446
  seen.push(envelope.message_id);
@@ -2318,6 +2487,7 @@ async function revokeSession(store, sessionId) {
2318
2487
  }
2319
2488
  async function receiveEnvelope(store, envelope) {
2320
2489
  if (envelope.type === "friend_request") return store.receiveFriendRequest(envelope);
2490
+ if (envelope.type === "support_bundle") return receiveSupportBundle(store, envelope);
2321
2491
  if (envelope.type === "friend_response") return store.applyFriendResponse(envelope);
2322
2492
  if (envelope.type === "privileged_message") return store.receivePrivilegedMessage(envelope);
2323
2493
  if (envelope.type === "object_share") {
@@ -2342,15 +2512,36 @@ async function receiveEnvelope(store, envelope) {
2342
2512
  }
2343
2513
  async function audit(store, action, peerAgentId, details) {
2344
2514
  const audit_id = randomId("audit");
2345
- await appendJsonl(store.file(AUDIT_FILE), {
2346
- audit_id,
2347
- created_at: now2(),
2348
- action,
2349
- peer_agent_id: peerAgentId,
2350
- details
2351
- });
2515
+ try {
2516
+ const identity = await store.identity();
2517
+ await appendJsonl(store.file(AUDIT_FILE), {
2518
+ audit_id,
2519
+ created_at: now2(),
2520
+ kind: action,
2521
+ action,
2522
+ actor_agent_id: identity.agent_id,
2523
+ peer_agent_id: peerAgentId,
2524
+ ...auditIndexFields(details),
2525
+ details
2526
+ });
2527
+ } catch {
2528
+ }
2352
2529
  return audit_id;
2353
2530
  }
2531
+ function auditIndexFields(details) {
2532
+ const fields = {};
2533
+ const objectId = details.object_id;
2534
+ const grantId = details.grant_id;
2535
+ const grantIds = details.grant_ids;
2536
+ const scope = details.scope;
2537
+ const scopes = details.scopes;
2538
+ if (typeof objectId === "string") fields.object_id = objectId;
2539
+ if (typeof grantId === "string") fields.grant_id = grantId;
2540
+ if (Array.isArray(grantIds) && grantIds.every((id) => typeof id === "string")) fields.grant_ids = grantIds.join(",");
2541
+ if (typeof scope === "string") fields.grant_scope = scope;
2542
+ if (Array.isArray(scopes) && scopes.every((s) => typeof s === "string")) fields.grant_scope = scopes.join(",");
2543
+ return fields;
2544
+ }
2354
2545
 
2355
2546
  // src/store-notify.ts
2356
2547
  async function peerName(store, agentId) {
@@ -2417,6 +2608,17 @@ var NOTIFY_POLICIES = {
2417
2608
  message: `${esc?.subject ?? "A decision is needed"} \u2014 ${esc?.body ?? ""}${opts}`,
2418
2609
  dedup_key: env.message_id
2419
2610
  };
2611
+ },
2612
+ support_bundle: async (env) => {
2613
+ const body = env.body;
2614
+ const name = body.card?.display_name || env.from_agent_id;
2615
+ return {
2616
+ kind: "support_bundle",
2617
+ from_id: env.from_agent_id,
2618
+ from_name: body.card?.display_name,
2619
+ message: `${name} sent a support bundle (ref ${env.trace_id ?? env.message_id}). Review: edge-book support pending`,
2620
+ dedup_key: env.message_id
2621
+ };
2420
2622
  }
2421
2623
  };
2422
2624
  async function notificationIntent(store, envelope) {
@@ -2438,6 +2640,14 @@ async function recordNotified(store, dedupKey) {
2438
2640
  ledger.push(dedupKey);
2439
2641
  await writeJson(store.file(NOTIFIED_FILE), ledger);
2440
2642
  }
2643
+ function buildPairCompleteNotifyIntent(deviceId, label) {
2644
+ return {
2645
+ kind: "pair_complete",
2646
+ message: `Pairing complete \u2014 your reader is connected (device: ${label}).`,
2647
+ from_id: deviceId,
2648
+ dedup_key: deviceId
2649
+ };
2650
+ }
2441
2651
 
2442
2652
  // src/edge-book.ts
2443
2653
  var EdgeBookStore = class {
@@ -2446,7 +2656,7 @@ var EdgeBookStore = class {
2446
2656
  this.home = resolveHome(options.home);
2447
2657
  }
2448
2658
  file(name) {
2449
- return path6.join(this.home, name);
2659
+ return path7.join(this.home, name);
2450
2660
  }
2451
2661
  async init(input = {}) {
2452
2662
  return init(this, input);
@@ -2465,8 +2675,8 @@ var EdgeBookStore = class {
2465
2675
  return setProfile(this, input);
2466
2676
  }
2467
2677
  // Set a user-chosen unique handle. Re-signs the card; does NOT rotate keys.
2468
- async setHandle(handle) {
2469
- return setHandle(this, handle);
2678
+ async setHandle(handle, opts) {
2679
+ return setHandle(this, handle, opts);
2470
2680
  }
2471
2681
  // Portable identity bundle (the DID keypair + chosen handle). Carry to a new
2472
2682
  // device → same DID → relay handle keeps resolving to you (spec-096).
@@ -2982,16 +3192,17 @@ function nextAction(result, target) {
2982
3192
  return first ? `candidates list # then: friend request ${first.candidate_id}` : "candidates list";
2983
3193
  }
2984
3194
  default:
2985
- return "(no match \u2014 check the target)";
3195
+ return "not found \u2014 share your invite link so they can add you: card invite (use the deeplink_url)";
2986
3196
  }
2987
3197
  }
2988
3198
  var localContactProvider = {
2989
3199
  name: "local",
2990
3200
  priority: 100,
2991
3201
  async resolve(store, target) {
3202
+ const normalized = target.trim().replace(/^@/, "").toLowerCase();
2992
3203
  const contacts = await store.contacts();
2993
3204
  const match = Object.values(contacts).find(
2994
- (c) => c.peer_agent_id === target || c.aliases.includes(target) || c.display_name === target
3205
+ (c) => c.peer_agent_id === target || c.aliases.some((a) => a.toLowerCase() === normalized) || c.display_name.toLowerCase() === normalized
2995
3206
  );
2996
3207
  if (!match) return null;
2997
3208
  return {
@@ -3057,11 +3268,12 @@ async function writeCandidate(store, input) {
3057
3268
  await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
3058
3269
  return candidate;
3059
3270
  }
3271
+ var DEFAULT_RELAY_BASE = "https://edge-book-host.fly.dev";
3060
3272
  function defaultProviders(relayBase) {
3273
+ const base = relayBase ?? process.env["EDGE_BOOK_RELAY_BASE"] ?? DEFAULT_RELAY_BASE;
3061
3274
  const lookup = async (target) => {
3062
- if (!relayBase) return null;
3063
3275
  const slug = target.startsWith("registry:") ? target.slice("registry:".length) : target;
3064
- return `${relayBase.replace(/\/$/, "")}/handle/${encodeURIComponent(slug)}`;
3276
+ return `${base.replace(/\/$/, "")}/handle/${encodeURIComponent(slug)}`;
3065
3277
  };
3066
3278
  return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(lookup)];
3067
3279
  }
@@ -3109,15 +3321,22 @@ async function promoteCandidate(store, candidateId, note = "") {
3109
3321
  return envelope;
3110
3322
  }
3111
3323
  var HANDLE_SLUG2 = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
3324
+ function normalizeRegistryTarget(target) {
3325
+ if (target.startsWith("registry:")) {
3326
+ return "registry:" + target.slice("registry:".length).trim().toLowerCase().replace(/^@/, "");
3327
+ }
3328
+ return target.trim().toLowerCase().replace(/^@/, "");
3329
+ }
3112
3330
  function makeRegistryProvider(lookup) {
3113
3331
  return {
3114
3332
  name: "registry",
3115
3333
  priority: 50,
3116
3334
  async resolve(_store, target) {
3117
- const isExplicit = target.startsWith("registry:");
3118
- const slug = isExplicit ? target.slice("registry:".length) : target;
3119
- if (!isExplicit && !HANDLE_SLUG2.test(slug)) return null;
3120
- const cardTarget = await lookup(target);
3335
+ const slug = normalizeRegistryTarget(target);
3336
+ const isExplicit = slug.startsWith("registry:");
3337
+ const bareSlug = isExplicit ? slug.slice("registry:".length) : slug;
3338
+ if (!isExplicit && !HANDLE_SLUG2.test(bareSlug)) return null;
3339
+ const cardTarget = await lookup(slug);
3121
3340
  if (!cardTarget) return null;
3122
3341
  let card;
3123
3342
  try {
@@ -4466,7 +4685,9 @@ async function handleOwnerApi(req, res, url, adapters) {
4466
4685
  const card = await store.buildCard();
4467
4686
  const identity = await store.identity();
4468
4687
  const invite_url = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
4469
- sendJson(res, 200, { agent_id: identity.agent_id, display_name: identity.display_name, card_url: card.card_url, card, invite_url });
4688
+ const origin = card.card_url ? new URL(card.card_url).origin : "https://edge-book-host.fly.dev";
4689
+ const deeplink_url = `${origin}/add#i=${encodeURIComponent(invite_url)}`;
4690
+ sendJson(res, 200, { agent_id: identity.agent_id, display_name: identity.display_name, card_url: card.card_url, card, invite_url, deeplink_url });
4470
4691
  return true;
4471
4692
  }
4472
4693
  const attachmentMatch = /^\/api\/shared-objects\/([^/]+)\/attachment$/.exec(url.pathname);
@@ -4748,6 +4969,95 @@ function requestBody(frame, method) {
4748
4969
  return Buffer.from(JSON.stringify(frame.body ?? {}), "utf8");
4749
4970
  }
4750
4971
 
4972
+ // src/dialout-pair.ts
4973
+ var PairCompleteWaiter = class {
4974
+ resolve = null;
4975
+ timer = null;
4976
+ // Called by EdgeBookDialoutClient.handleMessage when type === "pair_complete".
4977
+ onFrame(frame) {
4978
+ if (!this.resolve) return;
4979
+ const cb = this.resolve;
4980
+ this.resolve = null;
4981
+ if (this.timer !== null) {
4982
+ clearTimeout(this.timer);
4983
+ this.timer = null;
4984
+ }
4985
+ cb({ device_id: frame.device_id ?? "", label: frame.label ?? "" });
4986
+ }
4987
+ // Wait up to ttlMs for a pair_complete frame from the host.
4988
+ // Returns null on timeout — old-host degradation (spec-135 §C.2).
4989
+ wait(ttlMs) {
4990
+ return new Promise((resolve) => {
4991
+ this.resolve = resolve;
4992
+ this.timer = setTimeout(() => {
4993
+ if (this.resolve) {
4994
+ this.resolve = null;
4995
+ resolve(null);
4996
+ }
4997
+ this.timer = null;
4998
+ }, ttlMs);
4999
+ });
5000
+ }
5001
+ };
5002
+
5003
+ // src/dialout-oneshot.ts
5004
+ async function sendPairRegistration(options) {
5005
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5006
+ await client.start();
5007
+ await new Promise((resolve) => setTimeout(resolve, 0));
5008
+ const registration = await client.pair(options.ttlMs ?? DEFAULT_PAIR_TTL_MS);
5009
+ await client.stop();
5010
+ return registration;
5011
+ }
5012
+ async function deliverEnvelopeViaMailbox(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.sendEnvelope(options.envelope);
5018
+ } finally {
5019
+ await client.stop();
5020
+ }
5021
+ }
5022
+ async function sendSessionsRevoke(options) {
5023
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5024
+ await client.start();
5025
+ await new Promise((resolve) => setTimeout(resolve, 0));
5026
+ const { frame, ack } = await client.revokeSessionsAndWait();
5027
+ await client.stop();
5028
+ return { ...frame, channel_id: ack.channel_id };
5029
+ }
5030
+ async function listSessions(options) {
5031
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5032
+ await client.start();
5033
+ await new Promise((resolve) => setTimeout(resolve, 0));
5034
+ try {
5035
+ return await client.listSessionsAndWait();
5036
+ } finally {
5037
+ await client.stop();
5038
+ }
5039
+ }
5040
+ async function revokeOneSession(options) {
5041
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5042
+ await client.start();
5043
+ await new Promise((resolve) => setTimeout(resolve, 0));
5044
+ try {
5045
+ return await client.revokeOneSessionAndWait(options.deviceId);
5046
+ } finally {
5047
+ await client.stop();
5048
+ }
5049
+ }
5050
+ async function mailboxStatus(options) {
5051
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5052
+ await client.start();
5053
+ await new Promise((resolve) => setTimeout(resolve, 0));
5054
+ try {
5055
+ return await client.mailboxStatusAndWait(options.ids, options.timeoutMs ?? 5e3);
5056
+ } finally {
5057
+ await client.stop();
5058
+ }
5059
+ }
5060
+
4751
5061
  // src/dialout.ts
4752
5062
  var DEFAULT_DIALOUT_HOST = "wss://edge-book-host.fly.dev/agent/ws";
4753
5063
  var DEFAULT_HEARTBEAT_MS = 25e3;
@@ -4784,6 +5094,7 @@ var EdgeBookDialoutClient = class {
4784
5094
  // frame — which carries NO request_id — reject pending requests of that type.
4785
5095
  pendingRpc = /* @__PURE__ */ new Map();
4786
5096
  pendingMailboxSends = /* @__PURE__ */ new Map();
5097
+ pairCompleteWaiter = new PairCompleteWaiter();
4787
5098
  constructor(options) {
4788
5099
  this.options = {
4789
5100
  heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
@@ -4817,6 +5128,11 @@ var EdgeBookDialoutClient = class {
4817
5128
  this.send(registration.frame);
4818
5129
  return registration;
4819
5130
  }
5131
+ // Wait up to ttlMs for a pair_complete frame from the host.
5132
+ // Returns null on timeout — old-host degradation (spec-135).
5133
+ waitForPairComplete(ttlMs) {
5134
+ return this.pairCompleteWaiter.wait(ttlMs);
5135
+ }
4820
5136
  async revokeSessions() {
4821
5137
  const frame = await createSessionsRevokeFrame(this.store);
4822
5138
  this.send(frame);
@@ -4848,7 +5164,7 @@ var EdgeBookDialoutClient = class {
4848
5164
  // Small request/response helper over the dial-out socket, correlated by
4849
5165
  // request_id. `expect` documents the ack type; resolution is by request_id.
4850
5166
  async rpc(type, extra, expect, timeoutMs) {
4851
- const request_id = crypto5.randomUUID();
5167
+ const request_id = crypto6.randomUUID();
4852
5168
  const promise = new Promise((resolve, reject) => {
4853
5169
  const timer = setTimeout(() => {
4854
5170
  this.pendingRpc.delete(request_id);
@@ -4875,8 +5191,8 @@ var EdgeBookDialoutClient = class {
4875
5191
  // Low-level: hand an opaque blob to the host for delivery to `to` (a peer DID
4876
5192
  // or channel_id). Resolves with the host-assigned message id once enqueued,
4877
5193
  // plus recipient_live when the host supports receipts (spec-097).
4878
- async sendMailbox(to, blob, timeoutMs = 5e3) {
4879
- const request_id = crypto5.randomUUID();
5194
+ async sendMailbox(to, blob, timeoutMs = 5e3, trace_id) {
5195
+ const request_id = crypto6.randomUUID();
4880
5196
  const blob_b64 = Buffer.from(blob).toString("base64");
4881
5197
  const ack = new Promise((resolve, reject) => {
4882
5198
  const timer = setTimeout(() => {
@@ -4885,14 +5201,16 @@ var EdgeBookDialoutClient = class {
4885
5201
  }, timeoutMs);
4886
5202
  this.pendingMailboxSends.set(request_id, { resolve, reject, timer });
4887
5203
  });
4888
- this.send({ type: "mailbox_send", request_id, to, blob_b64 });
5204
+ this.send({ type: "mailbox_send", request_id, to, blob_b64, ...trace_id ? { trace_id } : {} });
4889
5205
  return ack;
4890
5206
  }
4891
5207
  // High-level: deliver a signed envelope to its recipient (envelope.to_agent_id;
4892
5208
  // the host resolves the DID to a channel). Used to route friend requests,
4893
5209
  // object shares, and revokes through the mailbox instead of a manual file hop.
4894
5210
  async sendEnvelope(envelope) {
4895
- return this.sendMailbox(envelope.to_agent_id, Buffer.from(JSON.stringify(envelope), "utf8"));
5211
+ const ack = await this.sendMailbox(envelope.to_agent_id, Buffer.from(JSON.stringify(envelope), "utf8"), 5e3, envelope.trace_id);
5212
+ 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 });
5213
+ return ack;
4896
5214
  }
4897
5215
  async handleMailboxDeliver(frame) {
4898
5216
  let envelope;
@@ -4910,8 +5228,16 @@ var EdgeBookDialoutClient = class {
4910
5228
  }
4911
5229
  }
4912
5230
  } catch (e) {
4913
- error = e instanceof Error ? e.message : String(e);
4914
- }
5231
+ error = eventErrorCode(e);
5232
+ }
5233
+ await logEvent(this.store, "envelope.received", {
5234
+ envelope_kind: envelope?.type ?? "unparseable",
5235
+ from: frame.from,
5236
+ dedup_key: envelope?.message_id,
5237
+ trace_id: envelope?.trace_id,
5238
+ applied,
5239
+ ...error ? { error } : {}
5240
+ });
4915
5241
  if (!this.mailboxQueue) this.send({ type: "mailbox_ack", id: frame.id });
4916
5242
  if (envelope) await this.options.onEnvelope?.(envelope, { applied, error });
4917
5243
  }
@@ -4959,7 +5285,7 @@ var EdgeBookDialoutClient = class {
4959
5285
  agent_key: key.agent_key,
4960
5286
  agent_did: identity.agent_id,
4961
5287
  version: "0.1.0",
4962
- nonce: crypto5.randomUUID()
5288
+ nonce: crypto6.randomUUID()
4963
5289
  });
4964
5290
  } catch (error) {
4965
5291
  this.opened = void 0;
@@ -4972,6 +5298,7 @@ var EdgeBookDialoutClient = class {
4972
5298
  });
4973
5299
  addSocketListener(socket, "close", () => {
4974
5300
  if (this.heartbeat) clearInterval(this.heartbeat);
5301
+ void logEvent(this.store, "dialout.disconnected", { host: this.options.host, stopped: this.stopped });
4975
5302
  if (!this.stopped && this.options.reconnect) this.scheduleReconnect();
4976
5303
  });
4977
5304
  await opened;
@@ -4980,6 +5307,7 @@ var EdgeBookDialoutClient = class {
4980
5307
  if (this.stopped) return;
4981
5308
  const delay = this.currentBackoff;
4982
5309
  this.currentBackoff = Math.min(MAX_BACKOFF_MS, Math.round(this.currentBackoff * 1.7));
5310
+ void logEvent(this.store, "dialout.reconnect_scheduled", { host: this.options.host, delay_ms: delay });
4983
5311
  this.reconnectTimer = setTimeout(() => {
4984
5312
  void this.connect();
4985
5313
  }, delay);
@@ -4993,6 +5321,7 @@ var EdgeBookDialoutClient = class {
4993
5321
  if (frame.type === "hello_ok") {
4994
5322
  this.opened?.resolve();
4995
5323
  this.opened = void 0;
5324
+ void logEvent(this.store, "dialout.connected", { host: this.options.host });
4996
5325
  try {
4997
5326
  const identity = await this.store.identity();
4998
5327
  if (shouldClaimHandle(identity.handle)) {
@@ -5003,7 +5332,8 @@ var EdgeBookDialoutClient = class {
5003
5332
  handle: claim.handle,
5004
5333
  card: claim.card,
5005
5334
  claimed_at: claim.claimed_at,
5006
- claim_sig: claim.claim_sig
5335
+ claim_sig: claim.claim_sig,
5336
+ discoverable: claim.discoverable
5007
5337
  });
5008
5338
  }
5009
5339
  } catch {
@@ -5035,6 +5365,10 @@ var EdgeBookDialoutClient = class {
5035
5365
  return;
5036
5366
  }
5037
5367
  if (frame.type === "pair_register_ok" || frame.type === "pair_register_err") return;
5368
+ if (frame.type === "pair_complete") {
5369
+ this.pairCompleteWaiter.onFrame(frame);
5370
+ return;
5371
+ }
5038
5372
  if (frame.type === "sessions_revoke_ok") {
5039
5373
  const ack = frame;
5040
5374
  const pending = this.pendingSessionRevokes.get(ack.request_id || "");
@@ -5088,6 +5422,7 @@ var EdgeBookDialoutClient = class {
5088
5422
  }
5089
5423
  async standDown(frame) {
5090
5424
  this.stopped = true;
5425
+ await logEvent(this.store, "dialout.stand_down", { host: this.options.host, reason: frame.reason, idle_ms: frame.idle_ms });
5091
5426
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
5092
5427
  if (this.heartbeat) clearInterval(this.heartbeat);
5093
5428
  this.socket?.close();
@@ -5163,80 +5498,24 @@ var EdgeBookDialoutClient = class {
5163
5498
  }
5164
5499
  }
5165
5500
  };
5166
- async function sendPairRegistration(options) {
5167
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5168
- await client.start();
5169
- await new Promise((resolve) => setTimeout(resolve, 0));
5170
- const registration = await client.pair(options.ttlMs ?? DEFAULT_PAIR_TTL_MS);
5171
- await client.stop();
5172
- return registration;
5173
- }
5174
- async function deliverEnvelopeViaMailbox(options) {
5175
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5176
- await client.start();
5177
- await new Promise((resolve) => setTimeout(resolve, 0));
5178
- try {
5179
- return await client.sendEnvelope(options.envelope);
5180
- } finally {
5181
- await client.stop();
5182
- }
5183
- }
5184
- async function sendSessionsRevoke(options) {
5185
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5186
- await client.start();
5187
- await new Promise((resolve) => setTimeout(resolve, 0));
5188
- const { frame, ack } = await client.revokeSessionsAndWait();
5189
- await client.stop();
5190
- return { ...frame, channel_id: ack.channel_id };
5191
- }
5192
- async function listSessions(options) {
5193
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5194
- await client.start();
5195
- await new Promise((resolve) => setTimeout(resolve, 0));
5196
- try {
5197
- return await client.listSessionsAndWait();
5198
- } finally {
5199
- await client.stop();
5200
- }
5201
- }
5202
- async function revokeOneSession(options) {
5203
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5204
- await client.start();
5205
- await new Promise((resolve) => setTimeout(resolve, 0));
5206
- try {
5207
- return await client.revokeOneSessionAndWait(options.deviceId);
5208
- } finally {
5209
- await client.stop();
5210
- }
5211
- }
5212
- async function mailboxStatus(options) {
5213
- const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5214
- await client.start();
5215
- await new Promise((resolve) => setTimeout(resolve, 0));
5216
- try {
5217
- return await client.mailboxStatusAndWait(options.ids, options.timeoutMs ?? 5e3);
5218
- } finally {
5219
- await client.stop();
5220
- }
5221
- }
5222
5501
 
5223
5502
  // src/http-relay.ts
5224
- import fs7 from "fs/promises";
5503
+ import fs8 from "fs/promises";
5225
5504
  import http2 from "http";
5226
- import path7 from "path";
5505
+ import path8 from "path";
5227
5506
  function relayFile(store, agentId) {
5228
- return path7.join(store, `${encodeURIComponent(agentId)}.jsonl`);
5507
+ return path8.join(store, `${encodeURIComponent(agentId)}.jsonl`);
5229
5508
  }
5230
5509
  async function appendRelayEnvelope(store, agentId, envelope) {
5231
- await fs7.mkdir(store, { recursive: true });
5232
- await fs7.appendFile(relayFile(store, agentId), `${JSON.stringify(envelope)}
5510
+ await fs8.mkdir(store, { recursive: true });
5511
+ await fs8.appendFile(relayFile(store, agentId), `${JSON.stringify(envelope)}
5233
5512
  `, "utf8");
5234
5513
  }
5235
5514
  async function drainRelayEnvelopes(store, agentId) {
5236
5515
  const file = relayFile(store, agentId);
5237
5516
  try {
5238
- const text = await fs7.readFile(file, "utf8");
5239
- await fs7.writeFile(file, "", "utf8");
5517
+ const text = await fs8.readFile(file, "utf8");
5518
+ await fs8.writeFile(file, "", "utf8");
5240
5519
  return text.split(/\n/).filter(Boolean).map((line) => JSON.parse(line));
5241
5520
  } catch (error) {
5242
5521
  if (error.code === "ENOENT") return [];
@@ -5296,11 +5575,11 @@ async function pullRelayEnvelopes(relayBaseUrl, recipientAgentId) {
5296
5575
  }
5297
5576
 
5298
5577
  // src/store-outbox.ts
5299
- import path8 from "path";
5578
+ import path9 from "path";
5300
5579
  var OUTBOX_CAP = 200;
5301
5580
  var DEFAULT_STALE_QUEUE_MS = 10 * 60 * 1e3;
5302
5581
  function outboxPath(home) {
5303
- return path8.join(resolveHome(home), OUTBOX_FILE);
5582
+ return path9.join(resolveHome(home), OUTBOX_FILE);
5304
5583
  }
5305
5584
  async function readOutbox(home) {
5306
5585
  return readJson(outboxPath(home), []);
@@ -5370,7 +5649,7 @@ function takeRepeated(args, flag) {
5370
5649
  return out;
5371
5650
  }
5372
5651
  async function readEnvelope(filePath) {
5373
- return JSON.parse(await fs8.readFile(path9.resolve(filePath), "utf8"));
5652
+ return JSON.parse(await fs9.readFile(path10.resolve(filePath), "utf8"));
5374
5653
  }
5375
5654
  async function deliverToEndpoint(envelope, endpoint) {
5376
5655
  await postEnvelope(endpoint, envelope);
@@ -5451,46 +5730,467 @@ ${buildHandleNudge(suggestion)}` };
5451
5730
  }
5452
5731
 
5453
5732
  // src/cli-identity.ts
5454
- import fs9 from "fs/promises";
5455
- import path10 from "path";
5733
+ import fs11 from "fs/promises";
5734
+ import path11 from "path";
5456
5735
 
5457
- // src/onboarding.ts
5458
- 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.";
5459
- async function recordInviteCandidate(store, inviteUrl) {
5460
- const hashIndex = inviteUrl.indexOf("#");
5461
- const bareUrl = hashIndex === -1 ? inviteUrl : inviteUrl.slice(0, hashIndex);
5462
- const card = await loadCard(bareUrl);
5463
- const displayName = card.display_name || card.handle;
5464
- const candidate = await writeCandidate(store, {
5465
- source: "invite",
5466
- confidence: "high",
5467
- display_name: displayName,
5468
- reason: "Add-me invite passed to init --from-invite",
5469
- card_url: bareUrl
5470
- });
5471
- return { candidateId: candidate.candidate_id, displayName };
5472
- }
5473
- function buildOnboardingNote(opts = {}) {
5474
- const lines = [
5475
- `Agent: onboard your human (full script: skills/edge-book/prompts/onboard.md)`,
5476
- ` 1. Open with: "${ONBOARDING_MENTAL_MODEL}"`,
5477
- ` 2. Ask: public agent name? share your human's name with friends, or keep private?`,
5478
- ` Then propose a findable name (slug of the agent name) \u2192 edge-book handle set <slug>`,
5479
- ` 3. First friend: if they have an "Add me" link \u2192 edge-book friend request <link> --deliver`,
5480
- ` (no link? edge-book candidates list shows pending introductions)`,
5481
- ` 4. When accepted, draft a hello note: object create + object share \u2014 then tell them,`,
5482
- ` once: "they can read it until you take it back."`
5483
- ];
5484
- if (opts.invite) {
5485
- lines.push(
5486
- `First friend pre-loaded: "${opts.invite.displayName}" \u2014 send with: edge-book friend request ${opts.invite.candidateId} --deliver`
5487
- );
5488
- }
5489
- if (opts.inviteError) {
5490
- lines.push(`Invite link could not be read \u2014 ask your contact for a fresh "Add me" link. (${opts.inviteError})`);
5491
- }
5492
- return lines.join("\n");
5493
- }
5736
+ // src/doctor.ts
5737
+ import fs10 from "fs/promises";
5738
+
5739
+ // src/host-cron.ts
5740
+ import { existsSync } from "fs";
5741
+ import { execFileSync } from "child_process";
5742
+ var FRIEND_REQUESTS_CRON_NAME = "Edge Book \u2014 friend requests";
5743
+ var DEFAULT_FRIEND_REQUESTS_SCHEDULE = "*/20 * * * *";
5744
+ var HERMES_BIN_CANDIDATES = ["/opt/hermes/.venv/bin/hermes"];
5745
+ function buildFriendRequestsPrompt(home) {
5746
+ return [
5747
+ "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.",
5748
+ "",
5749
+ "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.",
5750
+ "",
5751
+ "1. List pending requests (run once):",
5752
+ ` edge-book friend pending --home ${home} --json`,
5753
+ " If edge-book is not on PATH, use: npm exec -y edge-book@0.11.0 -- friend pending --home " + home + " --json",
5754
+ " If the command errors, Edge Book is unavailable, or the list is empty ([]) \u2192 end your turn with exactly [SILENT].",
5755
+ "",
5756
+ '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.',
5757
+ "",
5758
+ "3. Mark each surfaced request notified so it is never re-sent (once per requester):",
5759
+ ` edge-book friend mark-notified <agent_id> --home ${home}`
5760
+ ].join("\n");
5761
+ }
5762
+ function ensureNotifierCron(opts) {
5763
+ if (opts.disabled) return { status: "disabled" };
5764
+ if (!opts.runner.hermesBin) return { status: "host_unsupported" };
5765
+ let listing;
5766
+ try {
5767
+ listing = opts.runner.list();
5768
+ } catch (e) {
5769
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5770
+ }
5771
+ if (listing.includes(FRIEND_REQUESTS_CRON_NAME)) return { status: "already_present" };
5772
+ const schedule = opts.schedule ?? DEFAULT_FRIEND_REQUESTS_SCHEDULE;
5773
+ const prompt = buildFriendRequestsPrompt(opts.home);
5774
+ const args = [
5775
+ "cron",
5776
+ "create",
5777
+ schedule,
5778
+ prompt,
5779
+ "--name",
5780
+ FRIEND_REQUESTS_CRON_NAME,
5781
+ "--deliver",
5782
+ "telegram",
5783
+ "--workdir",
5784
+ opts.home
5785
+ ];
5786
+ try {
5787
+ opts.runner.create(args);
5788
+ return { status: "installed" };
5789
+ } catch (e) {
5790
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5791
+ }
5792
+ }
5793
+ function defaultHermesRunner() {
5794
+ const bin = HERMES_BIN_CANDIDATES.find((p) => existsSync(p)) ?? null;
5795
+ return {
5796
+ hermesBin: bin,
5797
+ list: () => bin ? execFileSync(bin, ["cron", "list"], { encoding: "utf8" }) : "",
5798
+ create: (args) => {
5799
+ if (bin) execFileSync(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
5800
+ }
5801
+ };
5802
+ }
5803
+ var GREETER_CRON_NAME = "Edge Book \u2014 greeter";
5804
+ var DEFAULT_GREETER_SCHEDULE = "*/5 * * * *";
5805
+ function buildGreeterPrompt(home) {
5806
+ return [
5807
+ "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.",
5808
+ "",
5809
+ ` edge-book friend auto-accept --deliver --home ${home}`,
5810
+ ` If edge-book is not on PATH, use: npm exec -y edge-book -- friend auto-accept --deliver --home ${home}`,
5811
+ "",
5812
+ "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.",
5813
+ "",
5814
+ "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."
5815
+ ].join("\n");
5816
+ }
5817
+ function ensureGreeterCron(opts) {
5818
+ if (opts.disabled) return { status: "disabled" };
5819
+ if (!opts.runner.hermesBin) return { status: "host_unsupported" };
5820
+ let listing;
5821
+ try {
5822
+ listing = opts.runner.list();
5823
+ } catch (e) {
5824
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5825
+ }
5826
+ if (listing.includes(GREETER_CRON_NAME)) return { status: "already_present" };
5827
+ const args = [
5828
+ "cron",
5829
+ "create",
5830
+ opts.schedule ?? DEFAULT_GREETER_SCHEDULE,
5831
+ buildGreeterPrompt(opts.home),
5832
+ "--name",
5833
+ GREETER_CRON_NAME,
5834
+ "--deliver",
5835
+ "telegram",
5836
+ "--workdir",
5837
+ opts.home
5838
+ ];
5839
+ try {
5840
+ opts.runner.create(args);
5841
+ return { status: "installed" };
5842
+ } catch (e) {
5843
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5844
+ }
5845
+ }
5846
+
5847
+ // src/doctor.ts
5848
+ var DOCTOR_EVENT_TAIL = 50;
5849
+ var DOCTOR_TRACE_TAIL = 10;
5850
+ var DOCTOR_AUDIT_TAIL = 20;
5851
+ var DEFAULT_RELAY_TIMEOUT_MS = 3e3;
5852
+ function recentTraces(events, limit = DOCTOR_TRACE_TAIL) {
5853
+ const out = [];
5854
+ const seen = /* @__PURE__ */ new Set();
5855
+ for (let i = events.length - 1; i >= 0 && out.length < limit; i--) {
5856
+ const e = events[i];
5857
+ const trace_id = typeof e.trace_id === "string" ? e.trace_id : void 0;
5858
+ if (!trace_id || seen.has(trace_id)) continue;
5859
+ seen.add(trace_id);
5860
+ out.push({ trace_id, kind: e.kind, direction: e.kind === "envelope.sent" ? "out" : "in", ts: e.ts });
5861
+ }
5862
+ return out.reverse();
5863
+ }
5864
+ async function packageVersion() {
5865
+ try {
5866
+ const pkg = JSON.parse(await fs10.readFile(new URL("../package.json", import.meta.url), "utf8"));
5867
+ return pkg.version ?? "unknown";
5868
+ } catch {
5869
+ return "unknown";
5870
+ }
5871
+ }
5872
+ async function checkRelay(base, fetchImpl, timeoutMs) {
5873
+ const started = Date.now();
5874
+ try {
5875
+ const response = await fetchImpl(base, { method: "GET", signal: AbortSignal.timeout(timeoutMs) });
5876
+ return { url: base, reachable: true, status: response.status, latency_ms: Date.now() - started };
5877
+ } catch (error) {
5878
+ return { url: base, reachable: false, latency_ms: Date.now() - started, error: error instanceof Error ? error.message : String(error) };
5879
+ }
5880
+ }
5881
+ function notifierCronState(runner) {
5882
+ if (!runner.hermesBin) return "host_unsupported";
5883
+ try {
5884
+ return runner.list().includes(FRIEND_REQUESTS_CRON_NAME) ? "installed" : "not_installed";
5885
+ } catch {
5886
+ return "error";
5887
+ }
5888
+ }
5889
+ function stringField(value) {
5890
+ return typeof value === "string" && value.length > 0 ? value : void 0;
5891
+ }
5892
+ function detailString(details, key) {
5893
+ if (!details || typeof details !== "object") return void 0;
5894
+ const value = details[key];
5895
+ if (typeof value === "string" && value.length > 0) return value;
5896
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) return value.join(",");
5897
+ return void 0;
5898
+ }
5899
+ function firstString(...values) {
5900
+ for (const value of values) {
5901
+ const found = stringField(value);
5902
+ if (found) return found;
5903
+ }
5904
+ return void 0;
5905
+ }
5906
+ function addAuditField(event, key, value) {
5907
+ if (value) event[key] = value;
5908
+ }
5909
+ function auditEvent(event) {
5910
+ const out = {
5911
+ ts: firstString(event.created_at, event.ts) ?? "",
5912
+ kind: firstString(event.kind, event.action) ?? "unknown"
5913
+ };
5914
+ addAuditField(out, "actor_agent_id", stringField(event.actor_agent_id));
5915
+ addAuditField(out, "peer_agent_id", stringField(event.peer_agent_id));
5916
+ addAuditField(out, "object_id", firstString(event.object_id, detailString(event.details, "object_id")));
5917
+ addAuditField(out, "grant_id", firstString(event.grant_id, detailString(event.details, "grant_id")));
5918
+ addAuditField(out, "grant_ids", firstString(event.grant_ids, detailString(event.details, "grant_ids")));
5919
+ addAuditField(out, "grant_scope", firstString(event.grant_scope, detailString(event.details, "scope"), detailString(event.details, "scopes")));
5920
+ return out;
5921
+ }
5922
+ function auditTail(events, limit = DOCTOR_AUDIT_TAIL) {
5923
+ return events.slice(-limit).map(auditEvent);
5924
+ }
5925
+ async function buildDoctorReport(store, opts) {
5926
+ const legacy = await store.doctor();
5927
+ const config = await store.config();
5928
+ let identity = null;
5929
+ try {
5930
+ const id = await store.identity();
5931
+ identity = { fingerprint: id.agent_id, handle: id.handle, display_name: id.display_name };
5932
+ } catch {
5933
+ identity = null;
5934
+ }
5935
+ const relay = await checkRelay(
5936
+ relayBaseFromHost(opts.host),
5937
+ opts.fetchImpl ?? fetch,
5938
+ opts.timeoutMs ?? DEFAULT_RELAY_TIMEOUT_MS
5939
+ );
5940
+ const keyPresent = await fs10.stat(store.file(DIALOUT_KEY_FILE)).then(() => true).catch(() => false);
5941
+ const lastConnected = await lastEvent(store, "dialout.connected");
5942
+ const lastDisconnected = await lastEvent(store, "dialout.disconnected");
5943
+ const contacts = await store.contacts();
5944
+ const contactList = Object.values(contacts);
5945
+ const friendCount = contactList.filter((c) => c.relationship_state === "friend").length;
5946
+ const pending = await store.pendingFriendRequests();
5947
+ const approvals2 = await store.approvals();
5948
+ const stores = {
5949
+ contacts: contactList.length,
5950
+ friends: friendCount,
5951
+ posts: Object.keys(await store.posts()).length,
5952
+ objects: Object.keys(await store.objects()).length,
5953
+ escalations: Object.keys(await store.escalations()).length,
5954
+ pending_approvals: Object.values(approvals2).filter((a) => a.status === "pending").length
5955
+ };
5956
+ return {
5957
+ version: await packageVersion(),
5958
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
5959
+ home: store.home,
5960
+ initialized: Boolean(legacy.initialized),
5961
+ pass: Boolean(legacy.pass),
5962
+ card_valid: Boolean(legacy.card_valid),
5963
+ private_key_mode_ok: Boolean(legacy.private_key_mode_ok),
5964
+ files: legacy.files ?? {},
5965
+ identity,
5966
+ relay,
5967
+ dialout: {
5968
+ key_present: keyPresent,
5969
+ ...typeof lastConnected?.ts === "string" ? { last_connected_at: lastConnected.ts } : {},
5970
+ ...typeof lastDisconnected?.ts === "string" ? { last_disconnected_at: lastDisconnected.ts } : {}
5971
+ },
5972
+ friends: {
5973
+ pending_requests: pending.length,
5974
+ pending: pending.map((c) => ({ from: c.peer_agent_id, ...c.display_name ? { display_name: c.display_name } : {} })),
5975
+ contacts: contactList.length,
5976
+ friends: friendCount
5977
+ },
5978
+ notify: {
5979
+ notify_cmd_configured: Boolean(typeof config.notify_cmd === "string" && config.notify_cmd.trim()),
5980
+ notify_on_friend_request: config.notify_on_friend_request !== false,
5981
+ notifier_cron: notifierCronState(opts.hermesRunner ?? defaultHermesRunner())
5982
+ },
5983
+ stores,
5984
+ events: await readEvents(store, DOCTOR_EVENT_TAIL),
5985
+ audit: auditTail(await store.auditEvents()),
5986
+ // Traces are derived from the FULL event log (not just the tail) so a
5987
+ // busy log does not hide an older still-relevant trace.
5988
+ traces: recentTraces(await readEvents(store))
5989
+ };
5990
+ }
5991
+ function renderEventLine(e) {
5992
+ const extras = Object.entries(e).filter(([k]) => k !== "ts" && k !== "kind").map(([k, v]) => `${k}=${String(v)}`).join(" ");
5993
+ return ` ${e.ts} ${e.kind}${extras ? ` ${extras}` : ""}`;
5994
+ }
5995
+ function renderAuditLine(e) {
5996
+ const extras = Object.entries(e).filter(([k]) => k !== "ts" && k !== "kind").map(([k, v]) => `${k}=${v}`).join(" ");
5997
+ return ` ${e.ts} ${e.kind}${extras ? ` ${extras}` : ""}`;
5998
+ }
5999
+ function appendTraceSection(lines, traces) {
6000
+ lines.push(`Recent traces (distinct trace_ids, newest last, up to ${DOCTOR_TRACE_TAIL})`);
6001
+ if (traces.length === 0) lines.push(" (no traced envelopes yet)");
6002
+ for (const t of traces) lines.push(` ${t.ts} ${t.direction === "out" ? "\u2192" : "\u2190"} ${t.kind} ${t.trace_id}`);
6003
+ }
6004
+ function appendEventSection(lines, events) {
6005
+ lines.push(`Event log (newest last, up to ${DOCTOR_EVENT_TAIL})`);
6006
+ if (events.length === 0) lines.push(" (no events recorded yet)");
6007
+ for (const e of events) lines.push(renderEventLine(e));
6008
+ }
6009
+ function appendAuditSection(lines, audit2) {
6010
+ lines.push(`Audit log (newest last, up to ${DOCTOR_AUDIT_TAIL})`);
6011
+ if (audit2.length === 0) lines.push(" (no audit records yet)");
6012
+ for (const e of audit2) lines.push(renderAuditLine(e));
6013
+ }
6014
+ function renderDoctorText(report) {
6015
+ const lines = [];
6016
+ lines.push(`Edge Book doctor \u2014 v${report.version} (${report.generated_at})`);
6017
+ lines.push(`Home: ${report.home}`);
6018
+ lines.push(`Overall: ${report.pass ? "PASS" : "FAIL"} (initialized=${report.initialized} card_valid=${report.card_valid} key_mode_ok=${report.private_key_mode_ok})`);
6019
+ lines.push("");
6020
+ lines.push("Identity");
6021
+ if (report.identity) {
6022
+ lines.push(` fingerprint: ${report.identity.fingerprint}`);
6023
+ lines.push(` handle: ${report.identity.handle}`);
6024
+ lines.push(` display name: ${report.identity.display_name}`);
6025
+ } else {
6026
+ lines.push(" not initialized \u2014 run: edge-book init");
6027
+ }
6028
+ lines.push("");
6029
+ lines.push("Relay");
6030
+ lines.push(` url: ${report.relay.url}`);
6031
+ lines.push(report.relay.reachable ? ` reachable: yes (HTTP ${report.relay.status}, ${report.relay.latency_ms}ms)` : ` reachable: NO (${report.relay.error ?? "unreachable"})`);
6032
+ lines.push("");
6033
+ lines.push("Dial-out");
6034
+ lines.push(` transport key: ${report.dialout.key_present ? "present" : "missing (never dialed out from this home)"}`);
6035
+ lines.push(` last connected: ${report.dialout.last_connected_at ?? "never (no connect event recorded)"}`);
6036
+ lines.push(` last disconnected: ${report.dialout.last_disconnected_at ?? "\u2014"}`);
6037
+ lines.push("");
6038
+ lines.push("Friend requests");
6039
+ lines.push(` pending: ${report.friends.pending_requests}`);
6040
+ for (const p of report.friends.pending) lines.push(` - ${p.display_name ?? "(no name)"} (${p.from})`);
6041
+ lines.push("");
6042
+ lines.push("Notifications");
6043
+ lines.push(` notify_cmd configured: ${report.notify.notify_cmd_configured ? "yes" : "no"}`);
6044
+ lines.push(` notify on friend request: ${report.notify.notify_on_friend_request ? "yes" : "no"}`);
6045
+ lines.push(` notifier cron: ${report.notify.notifier_cron}`);
6046
+ lines.push("");
6047
+ lines.push("Stores");
6048
+ lines.push(` contacts: ${report.stores.contacts} (friends: ${report.stores.friends})`);
6049
+ lines.push(` posts: ${report.stores.posts} objects: ${report.stores.objects} escalations: ${report.stores.escalations} pending approvals: ${report.stores.pending_approvals}`);
6050
+ lines.push("");
6051
+ appendTraceSection(lines, report.traces);
6052
+ lines.push("");
6053
+ appendEventSection(lines, report.events);
6054
+ lines.push("");
6055
+ appendAuditSection(lines, report.audit);
6056
+ return lines.join("\n");
6057
+ }
6058
+
6059
+ // src/doctor-send.ts
6060
+ import readline from "readline/promises";
6061
+ var SUPPORT_ENVELOPE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
6062
+ async function discoverSupportRecipient(relayBase, fetchImpl = fetch) {
6063
+ try {
6064
+ const response = await fetchImpl(`${relayBase}/support/recipient`, { signal: AbortSignal.timeout(5e3) });
6065
+ if (!response.ok) return null;
6066
+ const body = await response.json();
6067
+ return body.ok && typeof body.did === "string" && body.did ? body.did : null;
6068
+ } catch {
6069
+ return null;
6070
+ }
6071
+ }
6072
+ async function buildSupportBundleEnvelope(store, recipient, report, note) {
6073
+ const identity = await store.identity();
6074
+ const card = await store.writeCard();
6075
+ const body = {
6076
+ card,
6077
+ report,
6078
+ ...note ? { note } : {}
6079
+ };
6080
+ const envelope = await store.signEnvelope({
6081
+ type: "support_bundle",
6082
+ to_agent_id: recipient,
6083
+ relationship_id: relationshipId(identity.agent_id, recipient),
6084
+ capability_id: "",
6085
+ ref: "",
6086
+ transport: "local",
6087
+ expires_at: new Date(Date.now() + SUPPORT_ENVELOPE_TTL_MS).toISOString(),
6088
+ body
6089
+ });
6090
+ const size = Buffer.byteLength(JSON.stringify(envelope), "utf8");
6091
+ if (size > SUPPORT_BUNDLE_MAX_BYTES) {
6092
+ 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.`);
6093
+ }
6094
+ return envelope;
6095
+ }
6096
+ function renderConsentPrompt(report, recipient, sizeBytes, note) {
6097
+ const lines = [
6098
+ "edge-book doctor --send",
6099
+ "",
6100
+ "This will share your diagnostic bundle with the operator support mailbox:",
6101
+ "",
6102
+ ` recipient: ${recipient}`,
6103
+ ` size: ${(sizeBytes / 1024).toFixed(1)} KiB (cap 256 KiB)`,
6104
+ " sections: identity (fingerprint, handle, display name)",
6105
+ " relay reachability + dial-out state",
6106
+ ` friend-request counts + pending requester ids (${report.friends.pending_requests} pending)`,
6107
+ " notification settings (booleans only \u2014 never the notify command)",
6108
+ " store counts (contacts/posts/objects/escalations)",
6109
+ ` event-log tail (${report.events.length} protocol events: kinds, ids, dedup keys)`,
6110
+ ` recent envelope traces (${report.traces.length})`,
6111
+ ...note ? ["", ` your note: "${note}"`] : [],
6112
+ "",
6113
+ "The bundle is sanitized by construction: no private keys, no message or",
6114
+ "post bodies, no tokens. A support reference is printed after sending."
6115
+ ];
6116
+ return lines.join("\n");
6117
+ }
6118
+ async function promptYesNo(promptText) {
6119
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6120
+ try {
6121
+ const answer = (await rl.question(`${promptText}
6122
+
6123
+ Send? [y/N] `)).trim().toLowerCase();
6124
+ return answer === "y" || answer === "yes";
6125
+ } finally {
6126
+ rl.close();
6127
+ }
6128
+ }
6129
+ async function runDoctorSend(store, home, opts) {
6130
+ const report = await buildDoctorReport(store, { host: opts.host, fetchImpl: opts.fetchImpl });
6131
+ const recipient = opts.to ?? await discoverSupportRecipient(relayBaseFromHost(opts.host), opts.fetchImpl ?? fetch);
6132
+ if (!recipient) {
6133
+ 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>.`);
6134
+ }
6135
+ const envelope = await buildSupportBundleEnvelope(store, recipient, report, opts.note);
6136
+ if (!opts.yes) {
6137
+ const promptText = renderConsentPrompt(report, recipient, Buffer.byteLength(JSON.stringify(envelope), "utf8"), opts.note);
6138
+ const confirm = opts.confirmImpl ?? (process.stdin.isTTY ? promptYesNo : void 0);
6139
+ if (!confirm) throw new EdgeBookError("confirmation_required", "doctor --send needs interactive confirmation; pass --yes to send without a prompt.");
6140
+ if (!await confirm(promptText)) return { text: "Aborted \u2014 nothing was sent." };
6141
+ }
6142
+ const ack = await deliverEnvelopeViaMailbox({ home, host: opts.host, socketFactory: opts.socketFactory, envelope });
6143
+ await logEvent(store, "support.sent", { to: recipient, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
6144
+ const text = [
6145
+ `Support bundle sent to ${recipient} (host id ${ack.id}).`,
6146
+ `support reference: ${envelope.trace_id}`,
6147
+ "Quote this reference in any follow-up; the operator correlates it server-side."
6148
+ ].join("\n");
6149
+ return { text, json: { recipient, trace_id: envelope.trace_id, host_message_id: ack.id, message_id: envelope.message_id } };
6150
+ }
6151
+
6152
+ // src/onboarding.ts
6153
+ 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.";
6154
+ async function recordInviteCandidate(store, inviteUrl) {
6155
+ const hashIndex = inviteUrl.indexOf("#");
6156
+ const bareUrl = hashIndex === -1 ? inviteUrl : inviteUrl.slice(0, hashIndex);
6157
+ const card = await loadCard(bareUrl);
6158
+ const displayName = card.display_name || card.handle;
6159
+ const candidate = await writeCandidate(store, {
6160
+ source: "invite",
6161
+ confidence: "high",
6162
+ display_name: displayName,
6163
+ reason: "Add-me invite passed to init --from-invite",
6164
+ card_url: bareUrl
6165
+ });
6166
+ return { candidateId: candidate.candidate_id, displayName };
6167
+ }
6168
+ function buildOnboardingNote(opts = {}) {
6169
+ const lines = [
6170
+ `Agent: onboard your human (full script: skills/edge-book/prompts/onboard.md)`,
6171
+ ` 1. Open with: "${ONBOARDING_MENTAL_MODEL}"`,
6172
+ ` 2. Ask: public agent name? share your human's name with friends, or keep private?`,
6173
+ ` Then propose a findable name (slug of the agent name) \u2192 edge-book handle set <slug>`,
6174
+ ` 3. First friend: if they give you a name \u2192 edge-book resolve <name>; if resolved: edge-book friend request <name> --deliver`,
6175
+ ` If not found: share your link (edge-book card invite \u2192 use the deeplink_url)`,
6176
+ ` If they have an "Add me" link from someone \u2192 edge-book friend request <link> --deliver`,
6177
+ ` (no leads? edge-book candidates list shows pending introductions)`,
6178
+ ` 4. When accepted, draft a hello note: object create + object share \u2014 then tell them,`,
6179
+ ` once: "they can read it until you take it back."`,
6180
+ ` 5. Write to your persistent memory: "For 'friend <name>' or 'connect with <name>': run`,
6181
+ ` edge-book resolve <name> first. Index-network tools only for index:-prefixed targets."`,
6182
+ ` 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.`
6183
+ ];
6184
+ if (opts.invite) {
6185
+ lines.push(
6186
+ `First friend pre-loaded: "${opts.invite.displayName}" \u2014 send with: edge-book friend request ${opts.invite.candidateId} --deliver`
6187
+ );
6188
+ }
6189
+ if (opts.inviteError) {
6190
+ lines.push(`Invite link could not be read \u2014 ask your contact for a fresh "Add me" link. (${opts.inviteError})`);
6191
+ }
6192
+ return lines.join("\n");
6193
+ }
5494
6194
  var GREETER_DISPLAY_NAME = "Edge Book Greeter";
5495
6195
  var GREETER_CANDIDATE_REASON = "Says hi to every new agent \u2014 friend it to see how sharing works.";
5496
6196
  var DEFAULT_GREETER_HANDLE = "greeter";
@@ -5557,8 +6257,11 @@ Notifications (so inbound friend requests & messages reach you in real time):
5557
6257
  if (command === "handle") {
5558
6258
  const action = args.shift();
5559
6259
  if (action === "set") {
5560
- const id = await store.setHandle(slugifyHandle(requireArg(args.shift(), "handle set <slug>")));
5561
- return { text: `Handle set: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
6260
+ const slug = requireArg(args.shift(), "handle set <slug>");
6261
+ const hidden = takeBoolFlag(args, "--hidden");
6262
+ const id = await store.setHandle(slugifyHandle(slug), { discoverable: hidden ? false : void 0 });
6263
+ const hiddenNote = hidden ? " (hidden from /directory)" : "";
6264
+ return { text: `Handle set: ${id.handle} (${id.agent_id})${hiddenNote}`, json: { handle: id.handle, agent_id: id.agent_id, discoverable: !hidden } };
5562
6265
  }
5563
6266
  if (action === "show") {
5564
6267
  const id = await store.identity();
@@ -5573,9 +6276,9 @@ ${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
5573
6276
  const bundle = await store.exportIdentity();
5574
6277
  const p = takeFlag(args, "--path");
5575
6278
  if (p) {
5576
- const target = path10.resolve(p);
5577
- await fs9.mkdir(path10.dirname(target), { recursive: true });
5578
- await fs9.writeFile(target, `${JSON.stringify(bundle, null, 2)}
6279
+ const target = path11.resolve(p);
6280
+ await fs11.mkdir(path11.dirname(target), { recursive: true });
6281
+ await fs11.writeFile(target, `${JSON.stringify(bundle, null, 2)}
5579
6282
  `, { encoding: "utf8", mode: 384 });
5580
6283
  return { text: `Identity exported \u2192 ${target}`, json: { path: target } };
5581
6284
  }
@@ -5584,7 +6287,7 @@ ${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
5584
6287
  if (action === "import") {
5585
6288
  const source = requireArg(args.shift(), "identity import <path>");
5586
6289
  const force = takeBoolFlag(args, "--force");
5587
- const bundle = JSON.parse(await fs9.readFile(path10.resolve(source), "utf8"));
6290
+ const bundle = JSON.parse(await fs11.readFile(path11.resolve(source), "utf8"));
5588
6291
  const id = await store.importIdentity(bundle, { force });
5589
6292
  return { text: `Identity imported: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
5590
6293
  }
@@ -5684,8 +6387,20 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
5684
6387
  throw new EdgeBookError("unknown_action", `Unknown profile action: ${action} (use "show", "set", "visibility", or "broadcast")`);
5685
6388
  }
5686
6389
  if (command === "doctor") {
5687
- const result = await store.doctor();
5688
- return { text: JSON.stringify(result, null, 2), json: result };
6390
+ const asJson = takeBoolFlag(args, "--json");
6391
+ const send = takeBoolFlag(args, "--send");
6392
+ const host = parseHost(args, ctx);
6393
+ if (send) {
6394
+ return runDoctorSend(store, home, {
6395
+ host,
6396
+ yes: takeBoolFlag(args, "--yes"),
6397
+ to: takeFlag(args, "--to"),
6398
+ note: takeFlag(args, "--note"),
6399
+ socketFactory: ctx.socketFactory
6400
+ });
6401
+ }
6402
+ const report = await buildDoctorReport(store, { host });
6403
+ return { text: asJson ? JSON.stringify(report, null, 2) : renderDoctorText(report), json: report };
5689
6404
  }
5690
6405
  if (command === "card") {
5691
6406
  const action = args.shift() || "show";
@@ -5696,35 +6411,38 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
5696
6411
  if (action === "export") {
5697
6412
  const target = requireArg(takeFlag(args, "--path"), "--path");
5698
6413
  const card = await store.writeCard();
5699
- await fs9.mkdir(path10.dirname(path10.resolve(target)), { recursive: true });
5700
- await fs9.writeFile(path10.resolve(target), `${JSON.stringify(card, null, 2)}
6414
+ await fs11.mkdir(path11.dirname(path11.resolve(target)), { recursive: true });
6415
+ await fs11.writeFile(path11.resolve(target), `${JSON.stringify(card, null, 2)}
5701
6416
  `, "utf8");
5702
- return { text: `Exported Agent Card to ${path10.resolve(target)}`, json: card };
6417
+ return { text: `Exported Agent Card to ${path11.resolve(target)}`, json: card };
5703
6418
  }
5704
6419
  if (action === "invite") {
5705
6420
  const ttlMsStr = takeFlag(args, "--ttl-ms");
5706
6421
  const usesStr = takeFlag(args, "--uses");
6422
+ const raw = takeBoolFlag(args, "--raw");
5707
6423
  const ttlMs = ttlMsStr ? Number(ttlMsStr) : void 0;
5708
6424
  const maxUses = usesStr ? Number(usesStr) : void 0;
5709
6425
  const card = await store.writeCard();
5710
- const baseUrl = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
6426
+ const blob = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
6427
+ const origin = card.card_url ? new URL(card.card_url).origin : relayBaseFromHost(parseHost(args, ctx));
6428
+ const deeplink_url = `${origin}/add#i=${encodeURIComponent(blob)}`;
5711
6429
  if (ttlMs !== void 0 || maxUses !== void 0) {
5712
6430
  const invite = await store.mintInviteCode({ ttlMs, maxUses });
5713
- const inviteUrl = `${baseUrl}#code=${invite.code}`;
5714
- return { text: inviteUrl, json: { invite_url: inviteUrl, agent_id: card.agent_id, invite_code: invite.code } };
6431
+ const invite_url = `${blob}#code=${invite.code}`;
6432
+ return { text: raw ? invite_url : deeplink_url, json: { invite_url, deeplink_url, agent_id: card.agent_id, invite_code: invite.code } };
5715
6433
  }
5716
- return { text: baseUrl, json: { invite_url: baseUrl, agent_id: card.agent_id } };
6434
+ return { text: raw ? blob : deeplink_url, json: { invite_url: blob, deeplink_url, agent_id: card.agent_id } };
5717
6435
  }
5718
6436
  }
5719
6437
  return null;
5720
6438
  }
5721
6439
 
5722
6440
  // src/cli-social.ts
5723
- import path11 from "path";
6441
+ import path12 from "path";
5724
6442
 
5725
6443
  // src/store-greeter.ts
5726
6444
  var GREETER_WELCOME_TITLE = "Welcome to Edge Book";
5727
- 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.";
6445
+ 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.";
5728
6446
  function greeterWelcomeKey(agentId) {
5729
6447
  return `greeter_welcome:${agentId}`;
5730
6448
  }
@@ -5771,7 +6489,7 @@ async function runGreeterPass(store) {
5771
6489
  }
5772
6490
 
5773
6491
  // src/cli-social.ts
5774
- import fs10 from "fs/promises";
6492
+ import fs12 from "fs/promises";
5775
6493
  async function handleSocialCli(command, args, ctx, home, store) {
5776
6494
  if (command === "resolve") {
5777
6495
  const target = requireArg(args.shift(), "target");
@@ -5855,7 +6573,7 @@ next: ${result.next_action}`, json: result };
5855
6573
  const deliver = takeBoolFlag(args, "--deliver");
5856
6574
  const source = requireArg(args.shift(), "envelope-json-path");
5857
6575
  const followUp = await store.applyFriendResponse(await readEnvelope(source));
5858
- if (!followUp) return { text: `Applied friend response from ${path11.resolve(source)}` };
6576
+ if (!followUp) return { text: `Applied friend response from ${path12.resolve(source)}` };
5859
6577
  if (deliver) {
5860
6578
  try {
5861
6579
  return { text: await deliverToPeer(store, followUp, followUp.to_agent_id), json: followUp };
@@ -5958,8 +6676,8 @@ next: ${result.next_action}`, json: result };
5958
6676
  const file = takeFlag(args, "--file");
5959
6677
  let attachment;
5960
6678
  if (file) {
5961
- const bytes = await fs10.readFile(path11.resolve(file));
5962
- attachment = { filename: path11.basename(file), mime: takeFlag(args, "--mime") || "application/octet-stream", bytes };
6679
+ const bytes = await fs12.readFile(path12.resolve(file));
6680
+ attachment = { filename: path12.basename(file), mime: takeFlag(args, "--mime") || "application/octet-stream", bytes };
5963
6681
  }
5964
6682
  const object = await store.createObject({ title, body, attachment });
5965
6683
  return { text: `Created object ${object.object_id}`, json: object };
@@ -5999,7 +6717,7 @@ next: ${result.next_action}`, json: result };
5999
6717
  if (action === "receive") {
6000
6718
  const source = requireArg(args.shift(), "envelope-json-path");
6001
6719
  await store.receiveEnvelope(await readEnvelope(source));
6002
- return { text: `Applied object envelope from ${path11.resolve(source)}` };
6720
+ return { text: `Applied object envelope from ${path12.resolve(source)}` };
6003
6721
  }
6004
6722
  if (action === "list") {
6005
6723
  const objects2 = await store.sharedObjectsFor();
@@ -6038,7 +6756,7 @@ next: ${result.next_action}`, json: result };
6038
6756
  if (action === "receive") {
6039
6757
  const source = requireArg(args.shift(), "envelope-json-path");
6040
6758
  await store.receivePrivilegedMessage(await readEnvelope(source));
6041
- return { text: `Received privileged message from ${path11.resolve(source)}` };
6759
+ return { text: `Received privileged message from ${path12.resolve(source)}` };
6042
6760
  }
6043
6761
  }
6044
6762
  if (command === "escalation") {
@@ -6119,6 +6837,57 @@ next: ${result.next_action}`, json: result };
6119
6837
  return null;
6120
6838
  }
6121
6839
 
6840
+ // src/cli-support.ts
6841
+ function bundleLine(b) {
6842
+ return `${b.bundle_id} ${b.received_at} ${b.from_display_name ?? "(no name)"} (${b.from_agent_id}) ref=${b.trace_id ?? "-"}`;
6843
+ }
6844
+ async function handleSupportCli(command, args, _ctx, _home, store) {
6845
+ if (command !== "support") return null;
6846
+ const action = args.shift();
6847
+ if (action === "inbox") {
6848
+ const on = takeBoolFlag(args, "--on");
6849
+ const off = takeBoolFlag(args, "--off");
6850
+ if (on && off) throw new EdgeBookError("bad_flags", "support inbox takes either --on or --off, not both");
6851
+ if (!on && !off) throw new EdgeBookError("missing_arg", "support inbox needs --on or --off");
6852
+ const cfg = await store.updateConfig({ support_inbox: on ? true : false });
6853
+ return { text: `support_inbox = ${cfg.support_inbox}`, json: cfg };
6854
+ }
6855
+ if (action === "pending") {
6856
+ const bundles = await pendingSupportBundles(store);
6857
+ const text = bundles.length ? bundles.map(bundleLine).join("\n") : "No pending support bundles.";
6858
+ return { text, json: bundles };
6859
+ }
6860
+ if (action === "read") {
6861
+ const bundleId = requireArg(args.shift(), "bundle-id");
6862
+ const record = await setSupportBundleStatus(store, bundleId, "read");
6863
+ const header = [
6864
+ `Support bundle ${record.bundle_id} (marked read)`,
6865
+ `from: ${record.from_display_name ?? "(no name)"} (${record.from_agent_id})`,
6866
+ `ref: ${record.trace_id ?? "-"}`,
6867
+ `at: ${record.received_at}`,
6868
+ ...record.note ? [`note: ${record.note}`] : [],
6869
+ ""
6870
+ ].join("\n");
6871
+ return { text: `${header}${JSON.stringify(record.report, null, 2)}`, json: record };
6872
+ }
6873
+ if (action === "dismiss") {
6874
+ const bundleId = requireArg(args.shift(), "bundle-id");
6875
+ const record = await setSupportBundleStatus(store, bundleId, "dismissed");
6876
+ return { text: `Dismissed support bundle ${record.bundle_id}`, json: record };
6877
+ }
6878
+ if (action === "receive") {
6879
+ const source = requireArg(args.shift(), "envelope-json-path");
6880
+ const record = await receiveSupportBundle(store, await readEnvelope(source));
6881
+ return { text: `Received support bundle ${record.bundle_id} from ${record.from_agent_id} (ref ${record.trace_id ?? "-"})`, json: record };
6882
+ }
6883
+ if (action === "list") {
6884
+ const all = Object.values(await supportBundles(store)).sort((a, b) => a.received_at.localeCompare(b.received_at));
6885
+ const text = all.length ? all.map((b) => `${b.status.padEnd(9)} ${bundleLine(b)}`).join("\n") : "No support bundles.";
6886
+ return { text, json: all };
6887
+ }
6888
+ throw new EdgeBookError("unknown_action", `Unknown support action: ${action} (use "inbox", "pending", "read", "dismiss", "list", or "receive")`);
6889
+ }
6890
+
6122
6891
  // src/cli-taxonomy.ts
6123
6892
  async function handleTaxonomyCli(command, args, ctx, store) {
6124
6893
  if (command === "attest") {
@@ -6236,6 +7005,55 @@ async function handleTaxonomyCli(command, args, ctx, store) {
6236
7005
  return null;
6237
7006
  }
6238
7007
 
7008
+ // src/cli-directory.ts
7009
+ async function handleDirectoryCli(command, args, _ctx, _home, store) {
7010
+ if (command !== "directory") return null;
7011
+ const relayBase = takeFlag(args, "--relay-base") ?? process.env["EDGE_BOOK_RELAY_BASE"] ?? DEFAULT_RELAY_BASE;
7012
+ const limitStr = takeFlag(args, "--limit");
7013
+ const parsed = parseInt(limitStr ?? "", 10);
7014
+ const limit = limitStr ? Math.min(500, Math.max(1, Number.isNaN(parsed) ? 100 : parsed)) : 100;
7015
+ const url = `${relayBase.replace(/\/$/, "")}/directory?limit=${limit}&offset=0`;
7016
+ let data;
7017
+ try {
7018
+ const res = await fetch(url);
7019
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
7020
+ data = await res.json();
7021
+ } catch {
7022
+ return {
7023
+ text: "Could not reach the Edge Book relay. Check your connection or set EDGE_BOOK_RELAY_BASE.",
7024
+ json: null
7025
+ };
7026
+ }
7027
+ const { handles, total } = data;
7028
+ if (handles.length === 0) {
7029
+ return { text: "No agents are currently listed in the directory.", json: data };
7030
+ }
7031
+ const contactMap = await store.contacts().catch((err) => {
7032
+ process.stderr.write(`[directory] could not load contacts: ${String(err)}
7033
+ `);
7034
+ return {};
7035
+ });
7036
+ const relByHandle = /* @__PURE__ */ new Map();
7037
+ for (const c of Object.values(contactMap)) {
7038
+ const label = c.relationship_state === "friend" ? "friend" : c.relationship_state === "request_sent" ? "request sent" : c.relationship_state === "request_received" ? "request received" : "";
7039
+ if (label) {
7040
+ for (const alias of c.aliases) {
7041
+ relByHandle.set(alias.toLowerCase().replace(/^@/, ""), label);
7042
+ }
7043
+ }
7044
+ }
7045
+ const lines = handles.map((h) => {
7046
+ const owner = h.owner_label ? ` [${h.owner_label}]` : "";
7047
+ const rel = relByHandle.get(h.handle);
7048
+ const relPart = rel ? ` (${rel})` : "";
7049
+ return `@${h.handle} ${h.display_name}${owner}${relPart}`;
7050
+ });
7051
+ lines.push("");
7052
+ lines.push(`${handles.length} of ${total} agents listed.`);
7053
+ lines.push("To connect: edge-book friend request <handle> --deliver");
7054
+ return { text: lines.join("\n"), json: data };
7055
+ }
7056
+
6239
7057
  // src/commands-doc.ts
6240
7058
  var COMMAND_GROUPS = [
6241
7059
  {
@@ -6251,8 +7069,8 @@ var COMMAND_GROUPS = [
6251
7069
  title: "Handle / Identity",
6252
7070
  rows: [
6253
7071
  {
6254
- usage: "handle set <slug>",
6255
- desc: "Claim a unique human handle (replaces the default)"
7072
+ usage: "handle set <slug> [--hidden]",
7073
+ desc: "Claim a unique human handle (replaces the default); --hidden opts out of the /directory listing"
6256
7074
  },
6257
7075
  {
6258
7076
  usage: "handle show",
@@ -6512,8 +7330,41 @@ var COMMAND_GROUPS = [
6512
7330
  title: "Diagnostics",
6513
7331
  rows: [
6514
7332
  {
6515
- usage: "doctor",
6516
- desc: "Check your store, card, and key-file permissions"
7333
+ usage: "doctor [--json] [--host <wss-url>]",
7334
+ desc: "Diagnostic bundle: identity, relay reachability, dial-out state, stores, event-log tail (safe to paste publicly)"
7335
+ },
7336
+ {
7337
+ usage: "doctor --send [--yes] [--note <n>] [--to <did>] [--host <wss-url>]",
7338
+ desc: "Send the sanitized bundle to the operator support mailbox (consent prompt; prints a support reference)"
7339
+ }
7340
+ ]
7341
+ },
7342
+ {
7343
+ title: "Support inbox (operator)",
7344
+ rows: [
7345
+ {
7346
+ usage: "support inbox --on|--off",
7347
+ desc: "Opt this agent in/out as a support mailbox (off by default; inbound bundles are rejected)"
7348
+ },
7349
+ {
7350
+ usage: "support pending",
7351
+ desc: "List received support bundles awaiting review"
7352
+ },
7353
+ {
7354
+ usage: "support read <bundle-id>",
7355
+ desc: "Show a bundle's report and mark it read"
7356
+ },
7357
+ {
7358
+ usage: "support dismiss <bundle-id>",
7359
+ desc: "Dismiss a bundle without reading it"
7360
+ },
7361
+ {
7362
+ usage: "support list",
7363
+ desc: "List all support bundles including read/dismissed"
7364
+ },
7365
+ {
7366
+ usage: "support receive <envelope-json-path>",
7367
+ desc: "Apply an inbound support_bundle envelope from a file"
6517
7368
  }
6518
7369
  ]
6519
7370
  },
@@ -6578,6 +7429,15 @@ var COMMAND_GROUPS = [
6578
7429
  }
6579
7430
  ]
6580
7431
  },
7432
+ {
7433
+ title: "Network",
7434
+ rows: [
7435
+ {
7436
+ usage: "directory [--limit N] [--relay-base <url>]",
7437
+ desc: "List agents on the network with relationship annotations; EDGE_BOOK_RELAY_BASE env var overrides the default relay"
7438
+ }
7439
+ ]
7440
+ },
6581
7441
  {
6582
7442
  title: "Server / harness",
6583
7443
  rows: [
@@ -6659,10 +7519,15 @@ async function notifyInbound(store, envelope, opts) {
6659
7519
  if (!opts.cmd || !opts.cmd.trim()) return { notified: false, reason: "no_notify_cmd" };
6660
7520
  const intent = await store.notificationIntent(envelope);
6661
7521
  if (!intent) return { notified: false, reason: "silent" };
6662
- if (await store.wasNotified(intent.dedup_key)) return { notified: false, reason: "already_notified" };
7522
+ if (await store.wasNotified(intent.dedup_key)) {
7523
+ await logEvent(store, "notify.suppressed", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key, reason: "already_notified" });
7524
+ return { notified: false, reason: "already_notified" };
7525
+ }
7526
+ await logEvent(store, "notify.attempted", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
6663
7527
  const res = await deliverNotification(intent, opts);
6664
7528
  if (!res.delivered) {
6665
7529
  await store.audit("notify.failed", intent.from_id, { kind: intent.kind, dedup_key: intent.dedup_key, error: res.error ?? "" });
7530
+ await logEvent(store, "notify.failed", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
6666
7531
  return { notified: false, reason: res.error };
6667
7532
  }
6668
7533
  await store.recordNotified(intent.dedup_key);
@@ -6673,6 +7538,7 @@ async function notifyInbound(store, envelope, opts) {
6673
7538
  }
6674
7539
  }
6675
7540
  await store.audit("notify.delivered", intent.from_id, { kind: intent.kind, dedup_key: intent.dedup_key, channel: "hook" });
7541
+ await logEvent(store, "notify.delivered", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
6676
7542
  return { notified: true };
6677
7543
  }
6678
7544
  function makeNotifyOnEnvelope(store, cmd) {
@@ -6685,114 +7551,6 @@ function makeNotifyOnEnvelope(store, cmd) {
6685
7551
  };
6686
7552
  }
6687
7553
 
6688
- // src/host-cron.ts
6689
- import { existsSync } from "fs";
6690
- import { execFileSync } from "child_process";
6691
- var FRIEND_REQUESTS_CRON_NAME = "Edge Book \u2014 friend requests";
6692
- var DEFAULT_FRIEND_REQUESTS_SCHEDULE = "*/20 * * * *";
6693
- var HERMES_BIN_CANDIDATES = ["/opt/hermes/.venv/bin/hermes"];
6694
- function buildFriendRequestsPrompt(home) {
6695
- return [
6696
- "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.",
6697
- "",
6698
- "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.",
6699
- "",
6700
- "1. List pending requests (run once):",
6701
- ` edge-book friend pending --home ${home} --json`,
6702
- " If edge-book is not on PATH, use: npm exec -y edge-book@0.11.0 -- friend pending --home " + home + " --json",
6703
- " If the command errors, Edge Book is unavailable, or the list is empty ([]) \u2192 end your turn with exactly [SILENT].",
6704
- "",
6705
- '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.',
6706
- "",
6707
- "3. Mark each surfaced request notified so it is never re-sent (once per requester):",
6708
- ` edge-book friend mark-notified <agent_id> --home ${home}`
6709
- ].join("\n");
6710
- }
6711
- function ensureNotifierCron(opts) {
6712
- if (opts.disabled) return { status: "disabled" };
6713
- if (!opts.runner.hermesBin) return { status: "host_unsupported" };
6714
- let listing;
6715
- try {
6716
- listing = opts.runner.list();
6717
- } catch (e) {
6718
- return { status: "error", detail: e instanceof Error ? e.message : String(e) };
6719
- }
6720
- if (listing.includes(FRIEND_REQUESTS_CRON_NAME)) return { status: "already_present" };
6721
- const schedule = opts.schedule ?? DEFAULT_FRIEND_REQUESTS_SCHEDULE;
6722
- const prompt = buildFriendRequestsPrompt(opts.home);
6723
- const args = [
6724
- "cron",
6725
- "create",
6726
- schedule,
6727
- prompt,
6728
- "--name",
6729
- FRIEND_REQUESTS_CRON_NAME,
6730
- "--deliver",
6731
- "telegram",
6732
- "--workdir",
6733
- opts.home
6734
- ];
6735
- try {
6736
- opts.runner.create(args);
6737
- return { status: "installed" };
6738
- } catch (e) {
6739
- return { status: "error", detail: e instanceof Error ? e.message : String(e) };
6740
- }
6741
- }
6742
- function defaultHermesRunner() {
6743
- const bin = HERMES_BIN_CANDIDATES.find((p) => existsSync(p)) ?? null;
6744
- return {
6745
- hermesBin: bin,
6746
- list: () => bin ? execFileSync(bin, ["cron", "list"], { encoding: "utf8" }) : "",
6747
- create: (args) => {
6748
- if (bin) execFileSync(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
6749
- }
6750
- };
6751
- }
6752
- var GREETER_CRON_NAME = "Edge Book \u2014 greeter";
6753
- var DEFAULT_GREETER_SCHEDULE = "*/5 * * * *";
6754
- function buildGreeterPrompt(home) {
6755
- return [
6756
- "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.",
6757
- "",
6758
- ` edge-book friend auto-accept --deliver --home ${home}`,
6759
- ` If edge-book is not on PATH, use: npm exec -y edge-book -- friend auto-accept --deliver --home ${home}`,
6760
- "",
6761
- "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.",
6762
- "",
6763
- "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."
6764
- ].join("\n");
6765
- }
6766
- function ensureGreeterCron(opts) {
6767
- if (opts.disabled) return { status: "disabled" };
6768
- if (!opts.runner.hermesBin) return { status: "host_unsupported" };
6769
- let listing;
6770
- try {
6771
- listing = opts.runner.list();
6772
- } catch (e) {
6773
- return { status: "error", detail: e instanceof Error ? e.message : String(e) };
6774
- }
6775
- if (listing.includes(GREETER_CRON_NAME)) return { status: "already_present" };
6776
- const args = [
6777
- "cron",
6778
- "create",
6779
- opts.schedule ?? DEFAULT_GREETER_SCHEDULE,
6780
- buildGreeterPrompt(opts.home),
6781
- "--name",
6782
- GREETER_CRON_NAME,
6783
- "--deliver",
6784
- "telegram",
6785
- "--workdir",
6786
- opts.home
6787
- ];
6788
- try {
6789
- opts.runner.create(args);
6790
- return { status: "installed" };
6791
- } catch (e) {
6792
- return { status: "error", detail: e instanceof Error ? e.message : String(e) };
6793
- }
6794
- }
6795
-
6796
7554
  // src/cli.ts
6797
7555
  function usage() {
6798
7556
  return renderUsage();
@@ -6818,6 +7576,8 @@ async function handleCli(inputArgs, ctx = {}) {
6818
7576
  if (command === "friend" && socialAction === "auto-accept") return socialResult;
6819
7577
  return maybeAppendHandleNudge(store, command, socialResult);
6820
7578
  }
7579
+ const supportResult = await handleSupportCli(command, args, ctx, home, store);
7580
+ if (supportResult) return supportResult;
6821
7581
  if (command === "serve") {
6822
7582
  const host = takeFlag(args, "--host") || "127.0.0.1";
6823
7583
  const port = Number(takeFlag(args, "--port") || "0");
@@ -6845,6 +7605,8 @@ async function handleCli(inputArgs, ctx = {}) {
6845
7605
  const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
6846
7606
  try {
6847
7607
  const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
7608
+ if (res.status === "installed") await logEvent(store2, "cron.notifier_installed", {});
7609
+ else if (res.status === "already_present") await logEvent(store2, "cron.notifier_already_present", {});
6848
7610
  if (res.status === "installed") console.log(` \u21B3 notifier cron self-installed ("Edge Book \u2014 friend requests", every 20m \u2192 telegram)`);
6849
7611
  else if (res.status === "error") console.log(` \u21B3 notifier cron install skipped: ${res.detail}`);
6850
7612
  } catch (e) {
@@ -6864,6 +7626,8 @@ async function handleCli(inputArgs, ctx = {}) {
6864
7626
  if (command === "ensure-notifier") {
6865
7627
  const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
6866
7628
  const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
7629
+ if (res.status === "installed") await logEvent(store, "cron.notifier_installed", {});
7630
+ else if (res.status === "already_present") await logEvent(store, "cron.notifier_already_present", {});
6867
7631
  const msg = {
6868
7632
  installed: 'Installed notifier cron "Edge Book \u2014 friend requests" (every 20m \u2192 telegram).',
6869
7633
  already_present: "Notifier cron already present \u2014 nothing to do.",
@@ -6890,8 +7654,22 @@ async function handleCli(inputArgs, ctx = {}) {
6890
7654
  const registration2 = await client.pair(ttlMs);
6891
7655
  console.log(`Pairing code: ${registration2.code}`);
6892
7656
  console.log(`Expires in: ${ttlMs}ms`);
6893
- console.log("Edge Book dial-out remains connected; leave this process running during the hosted reader session.");
6894
- await new Promise(() => void 0);
7657
+ console.log("Waiting for your browser reader to connect...");
7658
+ const pairResult = await client.waitForPairComplete(ttlMs);
7659
+ await client.stop();
7660
+ if (!pairResult) {
7661
+ throw new EdgeBookError("pair_timeout", "Pairing code expired unredeemed \u2014 run edge-book pair again for a fresh code.");
7662
+ }
7663
+ console.log(`
7664
+ Pairing complete \u2014 your reader is connected (device: ${pairResult.label}).`);
7665
+ const notifyCmd = resolveNotifyCmd({ env: process.env.EDGE_BOOK_NOTIFY_CMD, config: (await store.config()).notify_cmd });
7666
+ const intent = buildPairCompleteNotifyIntent(pairResult.device_id, pairResult.label);
7667
+ if (notifyCmd && !await store.wasNotified(intent.dedup_key)) {
7668
+ const nr = await deliverNotification(intent, { cmd: notifyCmd });
7669
+ if (nr.delivered) await store.recordNotified(intent.dedup_key);
7670
+ }
7671
+ console.log("\n" + buildOnboardingNote());
7672
+ return { text: `Pairing complete \u2014 your reader is connected (device: ${pairResult.label}).`, json: { device_id: pairResult.device_id, label: pairResult.label } };
6895
7673
  }
6896
7674
  const registration = await sendPairRegistration({ home, host: hostUrl, ttlMs, socketFactory: ctx.socketFactory });
6897
7675
  return { text: `Pairing code: ${registration.code}
@@ -6979,6 +7757,8 @@ ${JSON.stringify(result, null, 2)}`, json: result };
6979
7757
  }
6980
7758
  const taxonomyResult = await handleTaxonomyCli(command, args, ctx, store);
6981
7759
  if (taxonomyResult) return maybeAppendHandleNudge(store, command, taxonomyResult);
7760
+ const directoryResult = await handleDirectoryCli(command, args, ctx, home, store);
7761
+ if (directoryResult) return directoryResult;
6982
7762
  throw new EdgeBookError("unknown_command", usage());
6983
7763
  }
6984
7764
  async function runCli(args) {