edge-book 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +12 -2
  2. package/dist/edge-book.js +975 -278
  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";
@@ -1504,6 +1507,74 @@ async function expireEscalations(store) {
1504
1507
  if (changed) await store.saveEscalations(all);
1505
1508
  }
1506
1509
 
1510
+ // src/event-log.ts
1511
+ import crypto4 from "crypto";
1512
+ import fs6 from "fs/promises";
1513
+ import path6 from "path";
1514
+ var MAX_EVENT_LINES = 2e3;
1515
+ var COMPACT_KEEP_LINES = 1e3;
1516
+ function eventErrorCode(e) {
1517
+ if (e instanceof Error) {
1518
+ const code = e.code;
1519
+ if (typeof code === "string" && code.length > 0) return code;
1520
+ return e.constructor.name || "Error";
1521
+ }
1522
+ return "unknown_error";
1523
+ }
1524
+ async function logEvent(store, kind, fields = {}, caps = {}) {
1525
+ try {
1526
+ const event = { ts: (/* @__PURE__ */ new Date()).toISOString(), kind, ...fields };
1527
+ const file = store.file(EVENTS_FILE);
1528
+ await fs6.mkdir(path6.dirname(file), { recursive: true });
1529
+ await fs6.appendFile(file, `${JSON.stringify(event)}
1530
+ `, "utf8");
1531
+ await compactIfNeeded(file, caps.maxLines ?? MAX_EVENT_LINES, caps.keepLines ?? COMPACT_KEEP_LINES);
1532
+ } catch {
1533
+ }
1534
+ }
1535
+ async function compactIfNeeded(file, maxLines, keepLines) {
1536
+ const text = await fs6.readFile(file, "utf8");
1537
+ const lines = text.split("\n").filter((l) => l.length > 0);
1538
+ if (lines.length <= maxLines) return;
1539
+ const kept = lines.slice(-keepLines);
1540
+ const tmp = `${file}.tmp-${crypto4.randomBytes(6).toString("hex")}`;
1541
+ try {
1542
+ await fs6.writeFile(tmp, `${kept.join("\n")}
1543
+ `, "utf8");
1544
+ await fs6.rename(tmp, file);
1545
+ } catch (error) {
1546
+ await fs6.rm(tmp, { force: true }).catch(() => void 0);
1547
+ throw error;
1548
+ }
1549
+ }
1550
+ async function readEvents(store, limit) {
1551
+ let text;
1552
+ try {
1553
+ text = await fs6.readFile(store.file(EVENTS_FILE), "utf8");
1554
+ } catch (error) {
1555
+ if (error.code === "ENOENT") return [];
1556
+ return [];
1557
+ }
1558
+ const out = [];
1559
+ for (const line of text.split("\n")) {
1560
+ if (!line) continue;
1561
+ try {
1562
+ const parsed = JSON.parse(line);
1563
+ if (parsed && typeof parsed === "object" && typeof parsed.kind === "string") out.push(parsed);
1564
+ } catch {
1565
+ }
1566
+ }
1567
+ return limit !== void 0 && out.length > limit ? out.slice(-limit) : out;
1568
+ }
1569
+ async function lastEvent(store, kind) {
1570
+ const events = await readEvents(store);
1571
+ for (let i = events.length - 1; i >= 0; i--) {
1572
+ const e = events[i];
1573
+ if (kind.endsWith(".") ? e.kind.startsWith(kind) : e.kind === kind) return e;
1574
+ }
1575
+ return void 0;
1576
+ }
1577
+
1507
1578
  // src/store-friends.ts
1508
1579
  async function upsertContactFromCard(store, card, state) {
1509
1580
  validateCard(card);
@@ -1570,6 +1641,7 @@ async function setRelationship(store, peerAgentId, nextState, type, reason = "")
1570
1641
  const event = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
1571
1642
  await appendJsonl(store.file(RELATIONSHIP_EVENTS_FILE), event);
1572
1643
  await store.audit(`relationship.${type}`, peerAgentId, { previous, next: nextState, reason });
1644
+ await logEvent(store, "friend.state_changed", { peer: peerAgentId, previous, next: nextState, event_type: type });
1573
1645
  return event;
1574
1646
  }
1575
1647
  async function createFriendRequest(store, targetCard, note = "", inviteCode = "") {
@@ -1609,6 +1681,7 @@ async function receiveFriendRequest(store, envelope) {
1609
1681
  }
1610
1682
  const contact = await store.upsertContactFromCard(body.card, "request_received");
1611
1683
  await store.setRelationship(envelope.from_agent_id, "request_received", "FriendRequest", body.note);
1684
+ await logEvent(store, "friend.request_received", { from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
1612
1685
  await appendJsonl(store.file(INBOX_FILE), envelope);
1613
1686
  const existingApprovals = await store.approvals();
1614
1687
  const alreadyPending = Object.values(existingApprovals).some(
@@ -1652,6 +1725,7 @@ async function acceptFriend(store, peerAgentId, reason = "accepted") {
1652
1725
  if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
1653
1726
  if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked_peer", "Cannot accept a blocked peer");
1654
1727
  await store.setRelationship(peerAgentId, "friend", "Accept", reason);
1728
+ await logEvent(store, "friend.accepted", { peer: peerAgentId });
1655
1729
  const grant = await store.issueGrant(peerAgentId, ["message.friend", "feed.read.friends", "profile.read.friend", "escalation.raise"]);
1656
1730
  const card = await store.writeCard();
1657
1731
  const profile = await store.buildFriendProfile();
@@ -1829,8 +1903,8 @@ async function consumeInviteCode(store, code) {
1829
1903
  }
1830
1904
 
1831
1905
  // src/store-identity.ts
1832
- import crypto4 from "crypto";
1833
- import fs6 from "fs/promises";
1906
+ import crypto5 from "crypto";
1907
+ import fs7 from "fs/promises";
1834
1908
  async function init(store, input = {}) {
1835
1909
  await ensureHome(store.home);
1836
1910
  const existing = await readJson(store.file(IDENTITY_FILE), null);
@@ -1838,7 +1912,7 @@ async function init(store, input = {}) {
1838
1912
  await store.updateConfig({ direct_url: input.directUrl, relay_url: input.relayUrl });
1839
1913
  return existing;
1840
1914
  }
1841
- const { publicKey, privateKey } = crypto4.generateKeyPairSync("ed25519");
1915
+ const { publicKey, privateKey } = crypto5.generateKeyPairSync("ed25519");
1842
1916
  const public_key_pem = publicKey.export({ type: "spki", format: "pem" }).toString();
1843
1917
  const private_key_pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
1844
1918
  const identity = {
@@ -1898,16 +1972,21 @@ async function setProfile(store, input) {
1898
1972
  await store.audit("identity.update", identity.agent_id, { display_name: identity.display_name, profile_version: profile.profile_version });
1899
1973
  return identity;
1900
1974
  }
1901
- async function setHandle(store, handle) {
1975
+ async function setHandle(store, handle, opts = {}) {
1902
1976
  if (!isValidHandle(handle)) {
1903
1977
  throw new EdgeBookError("invalid_handle", `invalid_handle: must be 3-30 chars [a-z0-9-], not reserved: ${handle}`);
1904
1978
  }
1905
1979
  const identity = await store.identity();
1906
1980
  identity.handle = handle;
1981
+ if (opts.discoverable === false) {
1982
+ identity.handle_discoverable = false;
1983
+ } else {
1984
+ delete identity.handle_discoverable;
1985
+ }
1907
1986
  identity.updated_at = now2();
1908
1987
  await writeJson(store.file(IDENTITY_FILE), identity, 384);
1909
1988
  await store.writeCard();
1910
- await store.audit("identity.set_handle", identity.agent_id, { handle });
1989
+ await store.audit("identity.set_handle", identity.agent_id, { handle, discoverable: opts.discoverable !== false });
1911
1990
  return identity;
1912
1991
  }
1913
1992
  async function exportIdentity(store) {
@@ -1943,6 +2022,7 @@ async function updateConfig(store, input) {
1943
2022
  if (input.inbound_window_ms !== void 0) next.inbound_window_ms = input.inbound_window_ms;
1944
2023
  if (input.handle_nudge_at !== void 0) next.handle_nudge_at = input.handle_nudge_at;
1945
2024
  if (input.greeter_mode !== void 0) next.greeter_mode = input.greeter_mode;
2025
+ if (input.support_inbox !== void 0) next.support_inbox = input.support_inbox;
1946
2026
  if (input.greeter_welcome_object_id !== void 0) next.greeter_welcome_object_id = input.greeter_welcome_object_id;
1947
2027
  await writeJson(store.file(CONFIG_FILE), next);
1948
2028
  return next;
@@ -1974,7 +2054,7 @@ async function buildCard(store, cardUrl) {
1974
2054
  refresh_after: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString(),
1975
2055
  expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString()
1976
2056
  };
1977
- const card_hash = crypto4.createHash("sha256").update(canonicalize(unsigned)).digest("base64url");
2057
+ const card_hash = crypto5.createHash("sha256").update(canonicalize(unsigned)).digest("base64url");
1978
2058
  const withHash = { ...unsigned, card_hash };
1979
2059
  return { ...withHash, signature: signPayload(withHash, identity.private_key_pem) };
1980
2060
  }
@@ -1991,7 +2071,7 @@ async function buildHandleClaim(store) {
1991
2071
  const card = await loadCard(store.file(CARD_FILE));
1992
2072
  const claimed_at = Date.now();
1993
2073
  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 };
2074
+ return { handle: identity.handle, agent_did: identity.agent_id, card, claimed_at, claim_sig, discoverable: identity.handle_discoverable !== false };
1995
2075
  }
1996
2076
  async function buildFriendProfile(store) {
1997
2077
  const identity = await store.identity();
@@ -2019,7 +2099,7 @@ async function doctor(store) {
2019
2099
  const files = {};
2020
2100
  for (const name of requiredFiles) {
2021
2101
  try {
2022
- const stat = await fs6.stat(store.file(name));
2102
+ const stat = await fs7.stat(store.file(name));
2023
2103
  files[name] = {
2024
2104
  exists: true,
2025
2105
  mode: `0${(stat.mode & 511).toString(8)}`
@@ -2119,6 +2199,79 @@ async function exportLocalData(store) {
2119
2199
  };
2120
2200
  }
2121
2201
 
2202
+ // src/store-support.ts
2203
+ var SUPPORT_BUNDLE_MAX_BYTES = 256 * 1024;
2204
+ var SUPPORT_BUNDLE_KEEP = 200;
2205
+ function pruneSupportBundles(all, keepId) {
2206
+ const records = Object.values(all);
2207
+ if (records.length <= SUPPORT_BUNDLE_KEEP) return all;
2208
+ const byAge = (a, b) => a.received_at.localeCompare(b.received_at);
2209
+ const evictable = [
2210
+ ...records.filter((r) => r.status === "dismissed" && r.bundle_id !== keepId).sort(byAge),
2211
+ ...records.filter((r) => r.status !== "dismissed" && r.bundle_id !== keepId).sort(byAge)
2212
+ ];
2213
+ for (const r of evictable) {
2214
+ if (Object.keys(all).length <= SUPPORT_BUNDLE_KEEP) break;
2215
+ delete all[r.bundle_id];
2216
+ }
2217
+ return all;
2218
+ }
2219
+ async function supportBundles(store) {
2220
+ return readJson(store.file(SUPPORT_BUNDLES_FILE), {});
2221
+ }
2222
+ async function saveSupportBundles(store, all) {
2223
+ await writeJson(store.file(SUPPORT_BUNDLES_FILE), all);
2224
+ }
2225
+ async function receiveSupportBundle(store, envelope) {
2226
+ if ((await store.config()).support_inbox !== true) {
2227
+ throw new EdgeBookError("support_inbox_disabled", "This agent does not accept support bundles (operator opt-in: edge-book support inbox --on)");
2228
+ }
2229
+ if (envelope.type !== "support_bundle") throw new EdgeBookError("wrong_message_type", "Expected support_bundle envelope");
2230
+ const size = Buffer.byteLength(JSON.stringify(envelope), "utf8");
2231
+ if (size > SUPPORT_BUNDLE_MAX_BYTES) {
2232
+ throw new EdgeBookError("support_bundle_too_large", `Support bundle is ${size} bytes; the receiver cap is ${SUPPORT_BUNDLE_MAX_BYTES} (256 KiB)`);
2233
+ }
2234
+ await store.enforceInboundRate(envelope.from_agent_id);
2235
+ await store.verifyEnvelope(envelope);
2236
+ const body = envelope.body;
2237
+ if (!body || typeof body !== "object" || !body.report || typeof body.report !== "object") {
2238
+ throw new EdgeBookError("bad_support_bundle", "support_bundle envelope carries no report");
2239
+ }
2240
+ validateCard(body.card);
2241
+ if (body.card.agent_id !== envelope.from_agent_id) {
2242
+ throw new EdgeBookError("agent_id_mismatch", "Embedded card does not match the envelope sender");
2243
+ }
2244
+ const record = {
2245
+ bundle_id: envelope.message_id,
2246
+ from_agent_id: envelope.from_agent_id,
2247
+ ...body.card.display_name ? { from_display_name: body.card.display_name } : {},
2248
+ ...envelope.trace_id ? { trace_id: envelope.trace_id } : {},
2249
+ received_at: now2(),
2250
+ status: "pending",
2251
+ report: body.report,
2252
+ ...typeof body.note === "string" && body.note ? { note: body.note } : {}
2253
+ };
2254
+ const all = await supportBundles(store);
2255
+ all[record.bundle_id] = record;
2256
+ await saveSupportBundles(store, pruneSupportBundles(all, record.bundle_id));
2257
+ await store.audit("support.receive", envelope.from_agent_id, { bundle_id: record.bundle_id, trace_id: envelope.trace_id ?? "" });
2258
+ await logEvent(store, "support.received", { from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
2259
+ return record;
2260
+ }
2261
+ async function pendingSupportBundles(store) {
2262
+ const all = await supportBundles(store);
2263
+ return Object.values(all).filter((b) => b.status === "pending").sort((a, b) => a.received_at.localeCompare(b.received_at));
2264
+ }
2265
+ async function setSupportBundleStatus(store, bundleId, status) {
2266
+ const all = await supportBundles(store);
2267
+ const record = all[bundleId];
2268
+ if (!record) throw new EdgeBookError("unknown_bundle", `Unknown support bundle: ${bundleId}`);
2269
+ record.status = status;
2270
+ all[bundleId] = record;
2271
+ await saveSupportBundles(store, all);
2272
+ return record;
2273
+ }
2274
+
2122
2275
  // src/store-trust.ts
2123
2276
  async function enforceInboundRate(store, peerAgentId) {
2124
2277
  const config = await store.config();
@@ -2245,33 +2398,42 @@ async function assertGrantSignature(store, grant) {
2245
2398
  }
2246
2399
  async function signEnvelope(store, input) {
2247
2400
  const identity = await store.identity();
2401
+ const { expires_at, ...rest } = input;
2248
2402
  const unsigned = {
2249
2403
  message_id: randomId("msg"),
2250
2404
  from_agent_id: identity.agent_id,
2251
2405
  created_at: now2(),
2252
- expires_at: new Date(Date.now() + 10 * 60 * 1e3).toISOString(),
2253
- ...input
2406
+ expires_at: expires_at ?? new Date(Date.now() + 10 * 60 * 1e3).toISOString(),
2407
+ ...rest,
2408
+ // Trace correlation (ea-claude-138): every new outbound envelope carries a
2409
+ // trace_id INSIDE the signed payload (tamper-evident; back-compat — old
2410
+ // receivers canonicalize whatever fields they parsed, so verification
2411
+ // still passes). Callers may pass an existing trace_id to chain a flow;
2412
+ // it is clamped to 128 chars (mirrors the host frame rule) and an empty
2413
+ // string falls back to a generated id.
2414
+ trace_id: input.trace_id?.slice(0, 128) || randomId("trace")
2254
2415
  };
2255
2416
  return { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
2256
2417
  }
2418
+ function embeddedCardKey(envelope) {
2419
+ if (envelope.type !== "friend_request" && envelope.type !== "friend_response" && envelope.type !== "support_bundle") return void 0;
2420
+ const card = envelope.body.card;
2421
+ return card?.public_keys?.[0]?.public_key_pem;
2422
+ }
2257
2423
  async function verifyEnvelope(store, envelope) {
2258
2424
  const identity = await store.identity();
2259
2425
  if (envelope.to_agent_id !== identity.agent_id) throw new EdgeBookError("wrong_recipient", "Envelope recipient does not match local identity");
2260
2426
  if (Date.parse(envelope.expires_at) <= Date.now()) throw new EdgeBookError("expired_message", "Message is expired");
2261
2427
  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;
2428
+ if (seen.includes(envelope.message_id)) {
2429
+ await logEvent(store, "envelope.dedup_hit", { envelope_kind: envelope.type, from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
2430
+ throw new EdgeBookError("replay", `Replay detected for ${envelope.message_id}`);
2272
2431
  }
2432
+ const contacts = await store.contacts();
2433
+ const publicKey = contacts[envelope.from_agent_id]?.public_keys?.[0]?.public_key_pem ?? embeddedCardKey(envelope);
2273
2434
  if (!publicKey) throw new EdgeBookError("unknown_key", `Unknown sender key for ${envelope.from_agent_id}`);
2274
2435
  if (!verifyPayload(withoutSignature(envelope), envelope.signature, publicKey)) {
2436
+ await logEvent(store, "envelope.signature_failed", { envelope_kind: envelope.type, from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
2275
2437
  throw new EdgeBookError("invalid_signature", "Message signature is invalid");
2276
2438
  }
2277
2439
  seen.push(envelope.message_id);
@@ -2318,6 +2480,7 @@ async function revokeSession(store, sessionId) {
2318
2480
  }
2319
2481
  async function receiveEnvelope(store, envelope) {
2320
2482
  if (envelope.type === "friend_request") return store.receiveFriendRequest(envelope);
2483
+ if (envelope.type === "support_bundle") return receiveSupportBundle(store, envelope);
2321
2484
  if (envelope.type === "friend_response") return store.applyFriendResponse(envelope);
2322
2485
  if (envelope.type === "privileged_message") return store.receivePrivilegedMessage(envelope);
2323
2486
  if (envelope.type === "object_share") {
@@ -2417,6 +2580,17 @@ var NOTIFY_POLICIES = {
2417
2580
  message: `${esc?.subject ?? "A decision is needed"} \u2014 ${esc?.body ?? ""}${opts}`,
2418
2581
  dedup_key: env.message_id
2419
2582
  };
2583
+ },
2584
+ support_bundle: async (env) => {
2585
+ const body = env.body;
2586
+ const name = body.card?.display_name || env.from_agent_id;
2587
+ return {
2588
+ kind: "support_bundle",
2589
+ from_id: env.from_agent_id,
2590
+ from_name: body.card?.display_name,
2591
+ message: `${name} sent a support bundle (ref ${env.trace_id ?? env.message_id}). Review: edge-book support pending`,
2592
+ dedup_key: env.message_id
2593
+ };
2420
2594
  }
2421
2595
  };
2422
2596
  async function notificationIntent(store, envelope) {
@@ -2438,6 +2612,14 @@ async function recordNotified(store, dedupKey) {
2438
2612
  ledger.push(dedupKey);
2439
2613
  await writeJson(store.file(NOTIFIED_FILE), ledger);
2440
2614
  }
2615
+ function buildPairCompleteNotifyIntent(deviceId, label) {
2616
+ return {
2617
+ kind: "pair_complete",
2618
+ message: `Pairing complete \u2014 your reader is connected (device: ${label}).`,
2619
+ from_id: deviceId,
2620
+ dedup_key: deviceId
2621
+ };
2622
+ }
2441
2623
 
2442
2624
  // src/edge-book.ts
2443
2625
  var EdgeBookStore = class {
@@ -2446,7 +2628,7 @@ var EdgeBookStore = class {
2446
2628
  this.home = resolveHome(options.home);
2447
2629
  }
2448
2630
  file(name) {
2449
- return path6.join(this.home, name);
2631
+ return path7.join(this.home, name);
2450
2632
  }
2451
2633
  async init(input = {}) {
2452
2634
  return init(this, input);
@@ -2465,8 +2647,8 @@ var EdgeBookStore = class {
2465
2647
  return setProfile(this, input);
2466
2648
  }
2467
2649
  // Set a user-chosen unique handle. Re-signs the card; does NOT rotate keys.
2468
- async setHandle(handle) {
2469
- return setHandle(this, handle);
2650
+ async setHandle(handle, opts) {
2651
+ return setHandle(this, handle, opts);
2470
2652
  }
2471
2653
  // Portable identity bundle (the DID keypair + chosen handle). Carry to a new
2472
2654
  // device → same DID → relay handle keeps resolving to you (spec-096).
@@ -2982,16 +3164,17 @@ function nextAction(result, target) {
2982
3164
  return first ? `candidates list # then: friend request ${first.candidate_id}` : "candidates list";
2983
3165
  }
2984
3166
  default:
2985
- return "(no match \u2014 check the target)";
3167
+ return "not found \u2014 share your invite link so they can add you: card invite (use the deeplink_url)";
2986
3168
  }
2987
3169
  }
2988
3170
  var localContactProvider = {
2989
3171
  name: "local",
2990
3172
  priority: 100,
2991
3173
  async resolve(store, target) {
3174
+ const normalized = target.trim().replace(/^@/, "").toLowerCase();
2992
3175
  const contacts = await store.contacts();
2993
3176
  const match = Object.values(contacts).find(
2994
- (c) => c.peer_agent_id === target || c.aliases.includes(target) || c.display_name === target
3177
+ (c) => c.peer_agent_id === target || c.aliases.some((a) => a.toLowerCase() === normalized) || c.display_name.toLowerCase() === normalized
2995
3178
  );
2996
3179
  if (!match) return null;
2997
3180
  return {
@@ -3057,11 +3240,12 @@ async function writeCandidate(store, input) {
3057
3240
  await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
3058
3241
  return candidate;
3059
3242
  }
3243
+ var DEFAULT_RELAY_BASE = "https://edge-book-host.fly.dev";
3060
3244
  function defaultProviders(relayBase) {
3245
+ const base = relayBase ?? process.env["EDGE_BOOK_RELAY_BASE"] ?? DEFAULT_RELAY_BASE;
3061
3246
  const lookup = async (target) => {
3062
- if (!relayBase) return null;
3063
3247
  const slug = target.startsWith("registry:") ? target.slice("registry:".length) : target;
3064
- return `${relayBase.replace(/\/$/, "")}/handle/${encodeURIComponent(slug)}`;
3248
+ return `${base.replace(/\/$/, "")}/handle/${encodeURIComponent(slug)}`;
3065
3249
  };
3066
3250
  return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(lookup)];
3067
3251
  }
@@ -3109,15 +3293,22 @@ async function promoteCandidate(store, candidateId, note = "") {
3109
3293
  return envelope;
3110
3294
  }
3111
3295
  var HANDLE_SLUG2 = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
3296
+ function normalizeRegistryTarget(target) {
3297
+ if (target.startsWith("registry:")) {
3298
+ return "registry:" + target.slice("registry:".length).trim().toLowerCase().replace(/^@/, "");
3299
+ }
3300
+ return target.trim().toLowerCase().replace(/^@/, "");
3301
+ }
3112
3302
  function makeRegistryProvider(lookup) {
3113
3303
  return {
3114
3304
  name: "registry",
3115
3305
  priority: 50,
3116
3306
  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);
3307
+ const slug = normalizeRegistryTarget(target);
3308
+ const isExplicit = slug.startsWith("registry:");
3309
+ const bareSlug = isExplicit ? slug.slice("registry:".length) : slug;
3310
+ if (!isExplicit && !HANDLE_SLUG2.test(bareSlug)) return null;
3311
+ const cardTarget = await lookup(slug);
3121
3312
  if (!cardTarget) return null;
3122
3313
  let card;
3123
3314
  try {
@@ -4466,7 +4657,9 @@ async function handleOwnerApi(req, res, url, adapters) {
4466
4657
  const card = await store.buildCard();
4467
4658
  const identity = await store.identity();
4468
4659
  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 });
4660
+ const origin = card.card_url ? new URL(card.card_url).origin : "https://edge-book-host.fly.dev";
4661
+ const deeplink_url = `${origin}/add#i=${encodeURIComponent(invite_url)}`;
4662
+ sendJson(res, 200, { agent_id: identity.agent_id, display_name: identity.display_name, card_url: card.card_url, card, invite_url, deeplink_url });
4470
4663
  return true;
4471
4664
  }
4472
4665
  const attachmentMatch = /^\/api\/shared-objects\/([^/]+)\/attachment$/.exec(url.pathname);
@@ -4748,6 +4941,95 @@ function requestBody(frame, method) {
4748
4941
  return Buffer.from(JSON.stringify(frame.body ?? {}), "utf8");
4749
4942
  }
4750
4943
 
4944
+ // src/dialout-pair.ts
4945
+ var PairCompleteWaiter = class {
4946
+ resolve = null;
4947
+ timer = null;
4948
+ // Called by EdgeBookDialoutClient.handleMessage when type === "pair_complete".
4949
+ onFrame(frame) {
4950
+ if (!this.resolve) return;
4951
+ const cb = this.resolve;
4952
+ this.resolve = null;
4953
+ if (this.timer !== null) {
4954
+ clearTimeout(this.timer);
4955
+ this.timer = null;
4956
+ }
4957
+ cb({ device_id: frame.device_id ?? "", label: frame.label ?? "" });
4958
+ }
4959
+ // Wait up to ttlMs for a pair_complete frame from the host.
4960
+ // Returns null on timeout — old-host degradation (spec-135 §C.2).
4961
+ wait(ttlMs) {
4962
+ return new Promise((resolve) => {
4963
+ this.resolve = resolve;
4964
+ this.timer = setTimeout(() => {
4965
+ if (this.resolve) {
4966
+ this.resolve = null;
4967
+ resolve(null);
4968
+ }
4969
+ this.timer = null;
4970
+ }, ttlMs);
4971
+ });
4972
+ }
4973
+ };
4974
+
4975
+ // src/dialout-oneshot.ts
4976
+ async function sendPairRegistration(options) {
4977
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
4978
+ await client.start();
4979
+ await new Promise((resolve) => setTimeout(resolve, 0));
4980
+ const registration = await client.pair(options.ttlMs ?? DEFAULT_PAIR_TTL_MS);
4981
+ await client.stop();
4982
+ return registration;
4983
+ }
4984
+ async function deliverEnvelopeViaMailbox(options) {
4985
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
4986
+ await client.start();
4987
+ await new Promise((resolve) => setTimeout(resolve, 0));
4988
+ try {
4989
+ return await client.sendEnvelope(options.envelope);
4990
+ } finally {
4991
+ await client.stop();
4992
+ }
4993
+ }
4994
+ async function sendSessionsRevoke(options) {
4995
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
4996
+ await client.start();
4997
+ await new Promise((resolve) => setTimeout(resolve, 0));
4998
+ const { frame, ack } = await client.revokeSessionsAndWait();
4999
+ await client.stop();
5000
+ return { ...frame, channel_id: ack.channel_id };
5001
+ }
5002
+ async function listSessions(options) {
5003
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5004
+ await client.start();
5005
+ await new Promise((resolve) => setTimeout(resolve, 0));
5006
+ try {
5007
+ return await client.listSessionsAndWait();
5008
+ } finally {
5009
+ await client.stop();
5010
+ }
5011
+ }
5012
+ async function revokeOneSession(options) {
5013
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5014
+ await client.start();
5015
+ await new Promise((resolve) => setTimeout(resolve, 0));
5016
+ try {
5017
+ return await client.revokeOneSessionAndWait(options.deviceId);
5018
+ } finally {
5019
+ await client.stop();
5020
+ }
5021
+ }
5022
+ async function mailboxStatus(options) {
5023
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
5024
+ await client.start();
5025
+ await new Promise((resolve) => setTimeout(resolve, 0));
5026
+ try {
5027
+ return await client.mailboxStatusAndWait(options.ids, options.timeoutMs ?? 5e3);
5028
+ } finally {
5029
+ await client.stop();
5030
+ }
5031
+ }
5032
+
4751
5033
  // src/dialout.ts
4752
5034
  var DEFAULT_DIALOUT_HOST = "wss://edge-book-host.fly.dev/agent/ws";
4753
5035
  var DEFAULT_HEARTBEAT_MS = 25e3;
@@ -4784,6 +5066,7 @@ var EdgeBookDialoutClient = class {
4784
5066
  // frame — which carries NO request_id — reject pending requests of that type.
4785
5067
  pendingRpc = /* @__PURE__ */ new Map();
4786
5068
  pendingMailboxSends = /* @__PURE__ */ new Map();
5069
+ pairCompleteWaiter = new PairCompleteWaiter();
4787
5070
  constructor(options) {
4788
5071
  this.options = {
4789
5072
  heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
@@ -4817,6 +5100,11 @@ var EdgeBookDialoutClient = class {
4817
5100
  this.send(registration.frame);
4818
5101
  return registration;
4819
5102
  }
5103
+ // Wait up to ttlMs for a pair_complete frame from the host.
5104
+ // Returns null on timeout — old-host degradation (spec-135).
5105
+ waitForPairComplete(ttlMs) {
5106
+ return this.pairCompleteWaiter.wait(ttlMs);
5107
+ }
4820
5108
  async revokeSessions() {
4821
5109
  const frame = await createSessionsRevokeFrame(this.store);
4822
5110
  this.send(frame);
@@ -4848,7 +5136,7 @@ var EdgeBookDialoutClient = class {
4848
5136
  // Small request/response helper over the dial-out socket, correlated by
4849
5137
  // request_id. `expect` documents the ack type; resolution is by request_id.
4850
5138
  async rpc(type, extra, expect, timeoutMs) {
4851
- const request_id = crypto5.randomUUID();
5139
+ const request_id = crypto6.randomUUID();
4852
5140
  const promise = new Promise((resolve, reject) => {
4853
5141
  const timer = setTimeout(() => {
4854
5142
  this.pendingRpc.delete(request_id);
@@ -4875,8 +5163,8 @@ var EdgeBookDialoutClient = class {
4875
5163
  // Low-level: hand an opaque blob to the host for delivery to `to` (a peer DID
4876
5164
  // or channel_id). Resolves with the host-assigned message id once enqueued,
4877
5165
  // plus recipient_live when the host supports receipts (spec-097).
4878
- async sendMailbox(to, blob, timeoutMs = 5e3) {
4879
- const request_id = crypto5.randomUUID();
5166
+ async sendMailbox(to, blob, timeoutMs = 5e3, trace_id) {
5167
+ const request_id = crypto6.randomUUID();
4880
5168
  const blob_b64 = Buffer.from(blob).toString("base64");
4881
5169
  const ack = new Promise((resolve, reject) => {
4882
5170
  const timer = setTimeout(() => {
@@ -4885,14 +5173,16 @@ var EdgeBookDialoutClient = class {
4885
5173
  }, timeoutMs);
4886
5174
  this.pendingMailboxSends.set(request_id, { resolve, reject, timer });
4887
5175
  });
4888
- this.send({ type: "mailbox_send", request_id, to, blob_b64 });
5176
+ this.send({ type: "mailbox_send", request_id, to, blob_b64, ...trace_id ? { trace_id } : {} });
4889
5177
  return ack;
4890
5178
  }
4891
5179
  // High-level: deliver a signed envelope to its recipient (envelope.to_agent_id;
4892
5180
  // the host resolves the DID to a channel). Used to route friend requests,
4893
5181
  // object shares, and revokes through the mailbox instead of a manual file hop.
4894
5182
  async sendEnvelope(envelope) {
4895
- return this.sendMailbox(envelope.to_agent_id, Buffer.from(JSON.stringify(envelope), "utf8"));
5183
+ const ack = await this.sendMailbox(envelope.to_agent_id, Buffer.from(JSON.stringify(envelope), "utf8"), 5e3, envelope.trace_id);
5184
+ await logEvent(this.store, "envelope.sent", { envelope_kind: envelope.type, to: envelope.to_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
5185
+ return ack;
4896
5186
  }
4897
5187
  async handleMailboxDeliver(frame) {
4898
5188
  let envelope;
@@ -4910,8 +5200,16 @@ var EdgeBookDialoutClient = class {
4910
5200
  }
4911
5201
  }
4912
5202
  } catch (e) {
4913
- error = e instanceof Error ? e.message : String(e);
4914
- }
5203
+ error = eventErrorCode(e);
5204
+ }
5205
+ await logEvent(this.store, "envelope.received", {
5206
+ envelope_kind: envelope?.type ?? "unparseable",
5207
+ from: frame.from,
5208
+ dedup_key: envelope?.message_id,
5209
+ trace_id: envelope?.trace_id,
5210
+ applied,
5211
+ ...error ? { error } : {}
5212
+ });
4915
5213
  if (!this.mailboxQueue) this.send({ type: "mailbox_ack", id: frame.id });
4916
5214
  if (envelope) await this.options.onEnvelope?.(envelope, { applied, error });
4917
5215
  }
@@ -4959,7 +5257,7 @@ var EdgeBookDialoutClient = class {
4959
5257
  agent_key: key.agent_key,
4960
5258
  agent_did: identity.agent_id,
4961
5259
  version: "0.1.0",
4962
- nonce: crypto5.randomUUID()
5260
+ nonce: crypto6.randomUUID()
4963
5261
  });
4964
5262
  } catch (error) {
4965
5263
  this.opened = void 0;
@@ -4972,6 +5270,7 @@ var EdgeBookDialoutClient = class {
4972
5270
  });
4973
5271
  addSocketListener(socket, "close", () => {
4974
5272
  if (this.heartbeat) clearInterval(this.heartbeat);
5273
+ void logEvent(this.store, "dialout.disconnected", { host: this.options.host, stopped: this.stopped });
4975
5274
  if (!this.stopped && this.options.reconnect) this.scheduleReconnect();
4976
5275
  });
4977
5276
  await opened;
@@ -4980,6 +5279,7 @@ var EdgeBookDialoutClient = class {
4980
5279
  if (this.stopped) return;
4981
5280
  const delay = this.currentBackoff;
4982
5281
  this.currentBackoff = Math.min(MAX_BACKOFF_MS, Math.round(this.currentBackoff * 1.7));
5282
+ void logEvent(this.store, "dialout.reconnect_scheduled", { host: this.options.host, delay_ms: delay });
4983
5283
  this.reconnectTimer = setTimeout(() => {
4984
5284
  void this.connect();
4985
5285
  }, delay);
@@ -4993,6 +5293,7 @@ var EdgeBookDialoutClient = class {
4993
5293
  if (frame.type === "hello_ok") {
4994
5294
  this.opened?.resolve();
4995
5295
  this.opened = void 0;
5296
+ void logEvent(this.store, "dialout.connected", { host: this.options.host });
4996
5297
  try {
4997
5298
  const identity = await this.store.identity();
4998
5299
  if (shouldClaimHandle(identity.handle)) {
@@ -5003,7 +5304,8 @@ var EdgeBookDialoutClient = class {
5003
5304
  handle: claim.handle,
5004
5305
  card: claim.card,
5005
5306
  claimed_at: claim.claimed_at,
5006
- claim_sig: claim.claim_sig
5307
+ claim_sig: claim.claim_sig,
5308
+ discoverable: claim.discoverable
5007
5309
  });
5008
5310
  }
5009
5311
  } catch {
@@ -5035,6 +5337,10 @@ var EdgeBookDialoutClient = class {
5035
5337
  return;
5036
5338
  }
5037
5339
  if (frame.type === "pair_register_ok" || frame.type === "pair_register_err") return;
5340
+ if (frame.type === "pair_complete") {
5341
+ this.pairCompleteWaiter.onFrame(frame);
5342
+ return;
5343
+ }
5038
5344
  if (frame.type === "sessions_revoke_ok") {
5039
5345
  const ack = frame;
5040
5346
  const pending = this.pendingSessionRevokes.get(ack.request_id || "");
@@ -5088,6 +5394,7 @@ var EdgeBookDialoutClient = class {
5088
5394
  }
5089
5395
  async standDown(frame) {
5090
5396
  this.stopped = true;
5397
+ await logEvent(this.store, "dialout.stand_down", { host: this.options.host, reason: frame.reason, idle_ms: frame.idle_ms });
5091
5398
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
5092
5399
  if (this.heartbeat) clearInterval(this.heartbeat);
5093
5400
  this.socket?.close();
@@ -5163,80 +5470,24 @@ var EdgeBookDialoutClient = class {
5163
5470
  }
5164
5471
  }
5165
5472
  };
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
5473
 
5223
5474
  // src/http-relay.ts
5224
- import fs7 from "fs/promises";
5475
+ import fs8 from "fs/promises";
5225
5476
  import http2 from "http";
5226
- import path7 from "path";
5477
+ import path8 from "path";
5227
5478
  function relayFile(store, agentId) {
5228
- return path7.join(store, `${encodeURIComponent(agentId)}.jsonl`);
5479
+ return path8.join(store, `${encodeURIComponent(agentId)}.jsonl`);
5229
5480
  }
5230
5481
  async function appendRelayEnvelope(store, agentId, envelope) {
5231
- await fs7.mkdir(store, { recursive: true });
5232
- await fs7.appendFile(relayFile(store, agentId), `${JSON.stringify(envelope)}
5482
+ await fs8.mkdir(store, { recursive: true });
5483
+ await fs8.appendFile(relayFile(store, agentId), `${JSON.stringify(envelope)}
5233
5484
  `, "utf8");
5234
5485
  }
5235
5486
  async function drainRelayEnvelopes(store, agentId) {
5236
5487
  const file = relayFile(store, agentId);
5237
5488
  try {
5238
- const text = await fs7.readFile(file, "utf8");
5239
- await fs7.writeFile(file, "", "utf8");
5489
+ const text = await fs8.readFile(file, "utf8");
5490
+ await fs8.writeFile(file, "", "utf8");
5240
5491
  return text.split(/\n/).filter(Boolean).map((line) => JSON.parse(line));
5241
5492
  } catch (error) {
5242
5493
  if (error.code === "ENOENT") return [];
@@ -5296,11 +5547,11 @@ async function pullRelayEnvelopes(relayBaseUrl, recipientAgentId) {
5296
5547
  }
5297
5548
 
5298
5549
  // src/store-outbox.ts
5299
- import path8 from "path";
5550
+ import path9 from "path";
5300
5551
  var OUTBOX_CAP = 200;
5301
5552
  var DEFAULT_STALE_QUEUE_MS = 10 * 60 * 1e3;
5302
5553
  function outboxPath(home) {
5303
- return path8.join(resolveHome(home), OUTBOX_FILE);
5554
+ return path9.join(resolveHome(home), OUTBOX_FILE);
5304
5555
  }
5305
5556
  async function readOutbox(home) {
5306
5557
  return readJson(outboxPath(home), []);
@@ -5370,7 +5621,7 @@ function takeRepeated(args, flag) {
5370
5621
  return out;
5371
5622
  }
5372
5623
  async function readEnvelope(filePath) {
5373
- return JSON.parse(await fs8.readFile(path9.resolve(filePath), "utf8"));
5624
+ return JSON.parse(await fs9.readFile(path10.resolve(filePath), "utf8"));
5374
5625
  }
5375
5626
  async function deliverToEndpoint(envelope, endpoint) {
5376
5627
  await postEnvelope(endpoint, envelope);
@@ -5451,35 +5702,401 @@ ${buildHandleNudge(suggestion)}` };
5451
5702
  }
5452
5703
 
5453
5704
  // src/cli-identity.ts
5454
- import fs9 from "fs/promises";
5455
- import path10 from "path";
5705
+ import fs11 from "fs/promises";
5706
+ import path11 from "path";
5456
5707
 
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 };
5708
+ // src/doctor.ts
5709
+ import fs10 from "fs/promises";
5710
+
5711
+ // src/host-cron.ts
5712
+ import { existsSync } from "fs";
5713
+ import { execFileSync } from "child_process";
5714
+ var FRIEND_REQUESTS_CRON_NAME = "Edge Book \u2014 friend requests";
5715
+ var DEFAULT_FRIEND_REQUESTS_SCHEDULE = "*/20 * * * *";
5716
+ var HERMES_BIN_CANDIDATES = ["/opt/hermes/.venv/bin/hermes"];
5717
+ function buildFriendRequestsPrompt(home) {
5718
+ return [
5719
+ "You are the Edge Book friend-request notifier. Tell the human on their Telegram when someone has asked to connect on Edge Book. Hermes delivers your final assistant reply to their chat.",
5720
+ "",
5721
+ "This runs every 20 minutes; most runs there will be nothing pending. On any such run \u2014 and on any error \u2014 end your turn with exactly [SILENT] and nothing else. [SILENT] tells Hermes to send no message.",
5722
+ "",
5723
+ "1. List pending requests (run once):",
5724
+ ` edge-book friend pending --home ${home} --json`,
5725
+ " If edge-book is not on PATH, use: npm exec -y edge-book@0.11.0 -- friend pending --home " + home + " --json",
5726
+ " If the command errors, Edge Book is unavailable, or the list is empty ([]) \u2192 end your turn with exactly [SILENT].",
5727
+ "",
5728
+ '2. Otherwise write ONE short, warm message. For each requester use their display_name; say they asked to connect on Edge Book and that the human can reply "yes" to connect or ignore to leave it pending. No internal IDs, no JSON.',
5729
+ "",
5730
+ "3. Mark each surfaced request notified so it is never re-sent (once per requester):",
5731
+ ` edge-book friend mark-notified <agent_id> --home ${home}`
5732
+ ].join("\n");
5472
5733
  }
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)`,
5734
+ function ensureNotifierCron(opts) {
5735
+ if (opts.disabled) return { status: "disabled" };
5736
+ if (!opts.runner.hermesBin) return { status: "host_unsupported" };
5737
+ let listing;
5738
+ try {
5739
+ listing = opts.runner.list();
5740
+ } catch (e) {
5741
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5742
+ }
5743
+ if (listing.includes(FRIEND_REQUESTS_CRON_NAME)) return { status: "already_present" };
5744
+ const schedule = opts.schedule ?? DEFAULT_FRIEND_REQUESTS_SCHEDULE;
5745
+ const prompt = buildFriendRequestsPrompt(opts.home);
5746
+ const args = [
5747
+ "cron",
5748
+ "create",
5749
+ schedule,
5750
+ prompt,
5751
+ "--name",
5752
+ FRIEND_REQUESTS_CRON_NAME,
5753
+ "--deliver",
5754
+ "telegram",
5755
+ "--workdir",
5756
+ opts.home
5757
+ ];
5758
+ try {
5759
+ opts.runner.create(args);
5760
+ return { status: "installed" };
5761
+ } catch (e) {
5762
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5763
+ }
5764
+ }
5765
+ function defaultHermesRunner() {
5766
+ const bin = HERMES_BIN_CANDIDATES.find((p) => existsSync(p)) ?? null;
5767
+ return {
5768
+ hermesBin: bin,
5769
+ list: () => bin ? execFileSync(bin, ["cron", "list"], { encoding: "utf8" }) : "",
5770
+ create: (args) => {
5771
+ if (bin) execFileSync(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
5772
+ }
5773
+ };
5774
+ }
5775
+ var GREETER_CRON_NAME = "Edge Book \u2014 greeter";
5776
+ var DEFAULT_GREETER_SCHEDULE = "*/5 * * * *";
5777
+ function buildGreeterPrompt(home) {
5778
+ return [
5779
+ "You are the Edge Book greeter runner. Run the command below once and report what it did. Hermes delivers your final assistant reply to the chat.",
5780
+ "",
5781
+ ` edge-book friend auto-accept --deliver --home ${home}`,
5782
+ ` If edge-book is not on PATH, use: npm exec -y edge-book -- friend auto-accept --deliver --home ${home}`,
5783
+ "",
5784
+ "If the command errors, or its JSON output is an empty list ([]), end your turn with exactly [SILENT] and nothing else. [SILENT] tells Hermes to send no message.",
5785
+ "",
5786
+ "Otherwise reply with one short line per entry: the agent_id, whether it was accepted, and whether it was welcomed. No extra commentary, no raw JSON."
5787
+ ].join("\n");
5788
+ }
5789
+ function ensureGreeterCron(opts) {
5790
+ if (opts.disabled) return { status: "disabled" };
5791
+ if (!opts.runner.hermesBin) return { status: "host_unsupported" };
5792
+ let listing;
5793
+ try {
5794
+ listing = opts.runner.list();
5795
+ } catch (e) {
5796
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5797
+ }
5798
+ if (listing.includes(GREETER_CRON_NAME)) return { status: "already_present" };
5799
+ const args = [
5800
+ "cron",
5801
+ "create",
5802
+ opts.schedule ?? DEFAULT_GREETER_SCHEDULE,
5803
+ buildGreeterPrompt(opts.home),
5804
+ "--name",
5805
+ GREETER_CRON_NAME,
5806
+ "--deliver",
5807
+ "telegram",
5808
+ "--workdir",
5809
+ opts.home
5810
+ ];
5811
+ try {
5812
+ opts.runner.create(args);
5813
+ return { status: "installed" };
5814
+ } catch (e) {
5815
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5816
+ }
5817
+ }
5818
+
5819
+ // src/doctor.ts
5820
+ var DOCTOR_EVENT_TAIL = 50;
5821
+ var DOCTOR_TRACE_TAIL = 10;
5822
+ var DEFAULT_RELAY_TIMEOUT_MS = 3e3;
5823
+ function recentTraces(events, limit = DOCTOR_TRACE_TAIL) {
5824
+ const out = [];
5825
+ const seen = /* @__PURE__ */ new Set();
5826
+ for (let i = events.length - 1; i >= 0 && out.length < limit; i--) {
5827
+ const e = events[i];
5828
+ const trace_id = typeof e.trace_id === "string" ? e.trace_id : void 0;
5829
+ if (!trace_id || seen.has(trace_id)) continue;
5830
+ seen.add(trace_id);
5831
+ out.push({ trace_id, kind: e.kind, direction: e.kind === "envelope.sent" ? "out" : "in", ts: e.ts });
5832
+ }
5833
+ return out.reverse();
5834
+ }
5835
+ async function packageVersion() {
5836
+ try {
5837
+ const pkg = JSON.parse(await fs10.readFile(new URL("../package.json", import.meta.url), "utf8"));
5838
+ return pkg.version ?? "unknown";
5839
+ } catch {
5840
+ return "unknown";
5841
+ }
5842
+ }
5843
+ async function checkRelay(base, fetchImpl, timeoutMs) {
5844
+ const started = Date.now();
5845
+ try {
5846
+ const response = await fetchImpl(base, { method: "GET", signal: AbortSignal.timeout(timeoutMs) });
5847
+ return { url: base, reachable: true, status: response.status, latency_ms: Date.now() - started };
5848
+ } catch (error) {
5849
+ return { url: base, reachable: false, latency_ms: Date.now() - started, error: error instanceof Error ? error.message : String(error) };
5850
+ }
5851
+ }
5852
+ function notifierCronState(runner) {
5853
+ if (!runner.hermesBin) return "host_unsupported";
5854
+ try {
5855
+ return runner.list().includes(FRIEND_REQUESTS_CRON_NAME) ? "installed" : "not_installed";
5856
+ } catch {
5857
+ return "error";
5858
+ }
5859
+ }
5860
+ async function buildDoctorReport(store, opts) {
5861
+ const legacy = await store.doctor();
5862
+ const config = await store.config();
5863
+ let identity = null;
5864
+ try {
5865
+ const id = await store.identity();
5866
+ identity = { fingerprint: id.agent_id, handle: id.handle, display_name: id.display_name };
5867
+ } catch {
5868
+ identity = null;
5869
+ }
5870
+ const relay = await checkRelay(
5871
+ relayBaseFromHost(opts.host),
5872
+ opts.fetchImpl ?? fetch,
5873
+ opts.timeoutMs ?? DEFAULT_RELAY_TIMEOUT_MS
5874
+ );
5875
+ const keyPresent = await fs10.stat(store.file(DIALOUT_KEY_FILE)).then(() => true).catch(() => false);
5876
+ const lastConnected = await lastEvent(store, "dialout.connected");
5877
+ const lastDisconnected = await lastEvent(store, "dialout.disconnected");
5878
+ const contacts = await store.contacts();
5879
+ const contactList = Object.values(contacts);
5880
+ const friendCount = contactList.filter((c) => c.relationship_state === "friend").length;
5881
+ const pending = await store.pendingFriendRequests();
5882
+ const approvals2 = await store.approvals();
5883
+ const stores = {
5884
+ contacts: contactList.length,
5885
+ friends: friendCount,
5886
+ posts: Object.keys(await store.posts()).length,
5887
+ objects: Object.keys(await store.objects()).length,
5888
+ escalations: Object.keys(await store.escalations()).length,
5889
+ pending_approvals: Object.values(approvals2).filter((a) => a.status === "pending").length
5890
+ };
5891
+ return {
5892
+ version: await packageVersion(),
5893
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
5894
+ home: store.home,
5895
+ initialized: Boolean(legacy.initialized),
5896
+ pass: Boolean(legacy.pass),
5897
+ card_valid: Boolean(legacy.card_valid),
5898
+ private_key_mode_ok: Boolean(legacy.private_key_mode_ok),
5899
+ files: legacy.files ?? {},
5900
+ identity,
5901
+ relay,
5902
+ dialout: {
5903
+ key_present: keyPresent,
5904
+ ...typeof lastConnected?.ts === "string" ? { last_connected_at: lastConnected.ts } : {},
5905
+ ...typeof lastDisconnected?.ts === "string" ? { last_disconnected_at: lastDisconnected.ts } : {}
5906
+ },
5907
+ friends: {
5908
+ pending_requests: pending.length,
5909
+ pending: pending.map((c) => ({ from: c.peer_agent_id, ...c.display_name ? { display_name: c.display_name } : {} })),
5910
+ contacts: contactList.length,
5911
+ friends: friendCount
5912
+ },
5913
+ notify: {
5914
+ notify_cmd_configured: Boolean(typeof config.notify_cmd === "string" && config.notify_cmd.trim()),
5915
+ notify_on_friend_request: config.notify_on_friend_request !== false,
5916
+ notifier_cron: notifierCronState(opts.hermesRunner ?? defaultHermesRunner())
5917
+ },
5918
+ stores,
5919
+ events: await readEvents(store, DOCTOR_EVENT_TAIL),
5920
+ // Traces are derived from the FULL event log (not just the tail) so a
5921
+ // busy log does not hide an older still-relevant trace.
5922
+ traces: recentTraces(await readEvents(store))
5923
+ };
5924
+ }
5925
+ function renderEventLine(e) {
5926
+ const extras = Object.entries(e).filter(([k]) => k !== "ts" && k !== "kind").map(([k, v]) => `${k}=${String(v)}`).join(" ");
5927
+ return ` ${e.ts} ${e.kind}${extras ? ` ${extras}` : ""}`;
5928
+ }
5929
+ function renderDoctorText(report) {
5930
+ const lines = [];
5931
+ lines.push(`Edge Book doctor \u2014 v${report.version} (${report.generated_at})`);
5932
+ lines.push(`Home: ${report.home}`);
5933
+ lines.push(`Overall: ${report.pass ? "PASS" : "FAIL"} (initialized=${report.initialized} card_valid=${report.card_valid} key_mode_ok=${report.private_key_mode_ok})`);
5934
+ lines.push("");
5935
+ lines.push("Identity");
5936
+ if (report.identity) {
5937
+ lines.push(` fingerprint: ${report.identity.fingerprint}`);
5938
+ lines.push(` handle: ${report.identity.handle}`);
5939
+ lines.push(` display name: ${report.identity.display_name}`);
5940
+ } else {
5941
+ lines.push(" not initialized \u2014 run: edge-book init");
5942
+ }
5943
+ lines.push("");
5944
+ lines.push("Relay");
5945
+ lines.push(` url: ${report.relay.url}`);
5946
+ lines.push(report.relay.reachable ? ` reachable: yes (HTTP ${report.relay.status}, ${report.relay.latency_ms}ms)` : ` reachable: NO (${report.relay.error ?? "unreachable"})`);
5947
+ lines.push("");
5948
+ lines.push("Dial-out");
5949
+ lines.push(` transport key: ${report.dialout.key_present ? "present" : "missing (never dialed out from this home)"}`);
5950
+ lines.push(` last connected: ${report.dialout.last_connected_at ?? "never (no connect event recorded)"}`);
5951
+ lines.push(` last disconnected: ${report.dialout.last_disconnected_at ?? "\u2014"}`);
5952
+ lines.push("");
5953
+ lines.push("Friend requests");
5954
+ lines.push(` pending: ${report.friends.pending_requests}`);
5955
+ for (const p of report.friends.pending) lines.push(` - ${p.display_name ?? "(no name)"} (${p.from})`);
5956
+ lines.push("");
5957
+ lines.push("Notifications");
5958
+ lines.push(` notify_cmd configured: ${report.notify.notify_cmd_configured ? "yes" : "no"}`);
5959
+ lines.push(` notify on friend request: ${report.notify.notify_on_friend_request ? "yes" : "no"}`);
5960
+ lines.push(` notifier cron: ${report.notify.notifier_cron}`);
5961
+ lines.push("");
5962
+ lines.push("Stores");
5963
+ lines.push(` contacts: ${report.stores.contacts} (friends: ${report.stores.friends})`);
5964
+ lines.push(` posts: ${report.stores.posts} objects: ${report.stores.objects} escalations: ${report.stores.escalations} pending approvals: ${report.stores.pending_approvals}`);
5965
+ lines.push("");
5966
+ lines.push(`Recent traces (distinct trace_ids, newest last, up to ${DOCTOR_TRACE_TAIL})`);
5967
+ if (report.traces.length === 0) lines.push(" (no traced envelopes yet)");
5968
+ for (const t of report.traces) lines.push(` ${t.ts} ${t.direction === "out" ? "\u2192" : "\u2190"} ${t.kind} ${t.trace_id}`);
5969
+ lines.push("");
5970
+ lines.push(`Event log (newest last, up to ${DOCTOR_EVENT_TAIL})`);
5971
+ if (report.events.length === 0) lines.push(" (no events recorded yet)");
5972
+ for (const e of report.events) lines.push(renderEventLine(e));
5973
+ return lines.join("\n");
5974
+ }
5975
+
5976
+ // src/doctor-send.ts
5977
+ import readline from "readline/promises";
5978
+ var SUPPORT_ENVELOPE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
5979
+ async function discoverSupportRecipient(relayBase, fetchImpl = fetch) {
5980
+ try {
5981
+ const response = await fetchImpl(`${relayBase}/support/recipient`, { signal: AbortSignal.timeout(5e3) });
5982
+ if (!response.ok) return null;
5983
+ const body = await response.json();
5984
+ return body.ok && typeof body.did === "string" && body.did ? body.did : null;
5985
+ } catch {
5986
+ return null;
5987
+ }
5988
+ }
5989
+ async function buildSupportBundleEnvelope(store, recipient, report, note) {
5990
+ const identity = await store.identity();
5991
+ const card = await store.writeCard();
5992
+ const body = {
5993
+ card,
5994
+ report,
5995
+ ...note ? { note } : {}
5996
+ };
5997
+ const envelope = await store.signEnvelope({
5998
+ type: "support_bundle",
5999
+ to_agent_id: recipient,
6000
+ relationship_id: relationshipId(identity.agent_id, recipient),
6001
+ capability_id: "",
6002
+ ref: "",
6003
+ transport: "local",
6004
+ expires_at: new Date(Date.now() + SUPPORT_ENVELOPE_TTL_MS).toISOString(),
6005
+ body
6006
+ });
6007
+ const size = Buffer.byteLength(JSON.stringify(envelope), "utf8");
6008
+ if (size > SUPPORT_BUNDLE_MAX_BYTES) {
6009
+ throw new EdgeBookError("support_bundle_too_large", `Support bundle is ${size} bytes; the cap is ${SUPPORT_BUNDLE_MAX_BYTES} (256 KiB). Trim the note or report the issue another way.`);
6010
+ }
6011
+ return envelope;
6012
+ }
6013
+ function renderConsentPrompt(report, recipient, sizeBytes, note) {
6014
+ const lines = [
6015
+ "edge-book doctor --send",
6016
+ "",
6017
+ "This will share your diagnostic bundle with the operator support mailbox:",
6018
+ "",
6019
+ ` recipient: ${recipient}`,
6020
+ ` size: ${(sizeBytes / 1024).toFixed(1)} KiB (cap 256 KiB)`,
6021
+ " sections: identity (fingerprint, handle, display name)",
6022
+ " relay reachability + dial-out state",
6023
+ ` friend-request counts + pending requester ids (${report.friends.pending_requests} pending)`,
6024
+ " notification settings (booleans only \u2014 never the notify command)",
6025
+ " store counts (contacts/posts/objects/escalations)",
6026
+ ` event-log tail (${report.events.length} protocol events: kinds, ids, dedup keys)`,
6027
+ ` recent envelope traces (${report.traces.length})`,
6028
+ ...note ? ["", ` your note: "${note}"`] : [],
6029
+ "",
6030
+ "The bundle is sanitized by construction: no private keys, no message or",
6031
+ "post bodies, no tokens. A support reference is printed after sending."
6032
+ ];
6033
+ return lines.join("\n");
6034
+ }
6035
+ async function promptYesNo(promptText) {
6036
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6037
+ try {
6038
+ const answer = (await rl.question(`${promptText}
6039
+
6040
+ Send? [y/N] `)).trim().toLowerCase();
6041
+ return answer === "y" || answer === "yes";
6042
+ } finally {
6043
+ rl.close();
6044
+ }
6045
+ }
6046
+ async function runDoctorSend(store, home, opts) {
6047
+ const report = await buildDoctorReport(store, { host: opts.host, fetchImpl: opts.fetchImpl });
6048
+ const recipient = opts.to ?? await discoverSupportRecipient(relayBaseFromHost(opts.host), opts.fetchImpl ?? fetch);
6049
+ if (!recipient) {
6050
+ throw new EdgeBookError("no_support_recipient", `No support recipient is configured on this relay (${relayBaseFromHost(opts.host)} \u2014 host SUPPORT_DID unset or unreachable). Ask the operator, or pass --to <did>.`);
6051
+ }
6052
+ const envelope = await buildSupportBundleEnvelope(store, recipient, report, opts.note);
6053
+ if (!opts.yes) {
6054
+ const promptText = renderConsentPrompt(report, recipient, Buffer.byteLength(JSON.stringify(envelope), "utf8"), opts.note);
6055
+ const confirm = opts.confirmImpl ?? (process.stdin.isTTY ? promptYesNo : void 0);
6056
+ if (!confirm) throw new EdgeBookError("confirmation_required", "doctor --send needs interactive confirmation; pass --yes to send without a prompt.");
6057
+ if (!await confirm(promptText)) return { text: "Aborted \u2014 nothing was sent." };
6058
+ }
6059
+ const ack = await deliverEnvelopeViaMailbox({ home, host: opts.host, socketFactory: opts.socketFactory, envelope });
6060
+ await logEvent(store, "support.sent", { to: recipient, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
6061
+ const text = [
6062
+ `Support bundle sent to ${recipient} (host id ${ack.id}).`,
6063
+ `support reference: ${envelope.trace_id}`,
6064
+ "Quote this reference in any follow-up; the operator correlates it server-side."
6065
+ ].join("\n");
6066
+ return { text, json: { recipient, trace_id: envelope.trace_id, host_message_id: ack.id, message_id: envelope.message_id } };
6067
+ }
6068
+
6069
+ // src/onboarding.ts
6070
+ var ONBOARDING_MENTAL_MODEL = "Edge Book is a permissioned room between agents \u2014 you decide who comes in, what they can see, and you can take it back anytime.";
6071
+ async function recordInviteCandidate(store, inviteUrl) {
6072
+ const hashIndex = inviteUrl.indexOf("#");
6073
+ const bareUrl = hashIndex === -1 ? inviteUrl : inviteUrl.slice(0, hashIndex);
6074
+ const card = await loadCard(bareUrl);
6075
+ const displayName = card.display_name || card.handle;
6076
+ const candidate = await writeCandidate(store, {
6077
+ source: "invite",
6078
+ confidence: "high",
6079
+ display_name: displayName,
6080
+ reason: "Add-me invite passed to init --from-invite",
6081
+ card_url: bareUrl
6082
+ });
6083
+ return { candidateId: candidate.candidate_id, displayName };
6084
+ }
6085
+ function buildOnboardingNote(opts = {}) {
6086
+ const lines = [
6087
+ `Agent: onboard your human (full script: skills/edge-book/prompts/onboard.md)`,
6088
+ ` 1. Open with: "${ONBOARDING_MENTAL_MODEL}"`,
6089
+ ` 2. Ask: public agent name? share your human's name with friends, or keep private?`,
6090
+ ` Then propose a findable name (slug of the agent name) \u2192 edge-book handle set <slug>`,
6091
+ ` 3. First friend: if they give you a name \u2192 edge-book resolve <name>; if resolved: edge-book friend request <name> --deliver`,
6092
+ ` If not found: share your link (edge-book card invite \u2192 use the deeplink_url)`,
6093
+ ` If they have an "Add me" link from someone \u2192 edge-book friend request <link> --deliver`,
6094
+ ` (no leads? edge-book candidates list shows pending introductions)`,
5481
6095
  ` 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."`
6096
+ ` once: "they can read it until you take it back."`,
6097
+ ` 5. Write to your persistent memory: "For 'friend <name>' or 'connect with <name>': run`,
6098
+ ` edge-book resolve <name> first. Index-network tools only for index:-prefixed targets."`,
6099
+ ` 6. Install the notification cron: run edge-book ensure-notifier \u2014 self-installs a periodic push (every 20 min) so you never miss a friend request or message while offline.`
5483
6100
  ];
5484
6101
  if (opts.invite) {
5485
6102
  lines.push(
@@ -5557,8 +6174,11 @@ Notifications (so inbound friend requests & messages reach you in real time):
5557
6174
  if (command === "handle") {
5558
6175
  const action = args.shift();
5559
6176
  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 } };
6177
+ const slug = requireArg(args.shift(), "handle set <slug>");
6178
+ const hidden = takeBoolFlag(args, "--hidden");
6179
+ const id = await store.setHandle(slugifyHandle(slug), { discoverable: hidden ? false : void 0 });
6180
+ const hiddenNote = hidden ? " (hidden from /directory)" : "";
6181
+ return { text: `Handle set: ${id.handle} (${id.agent_id})${hiddenNote}`, json: { handle: id.handle, agent_id: id.agent_id, discoverable: !hidden } };
5562
6182
  }
5563
6183
  if (action === "show") {
5564
6184
  const id = await store.identity();
@@ -5573,9 +6193,9 @@ ${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
5573
6193
  const bundle = await store.exportIdentity();
5574
6194
  const p = takeFlag(args, "--path");
5575
6195
  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)}
6196
+ const target = path11.resolve(p);
6197
+ await fs11.mkdir(path11.dirname(target), { recursive: true });
6198
+ await fs11.writeFile(target, `${JSON.stringify(bundle, null, 2)}
5579
6199
  `, { encoding: "utf8", mode: 384 });
5580
6200
  return { text: `Identity exported \u2192 ${target}`, json: { path: target } };
5581
6201
  }
@@ -5584,7 +6204,7 @@ ${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
5584
6204
  if (action === "import") {
5585
6205
  const source = requireArg(args.shift(), "identity import <path>");
5586
6206
  const force = takeBoolFlag(args, "--force");
5587
- const bundle = JSON.parse(await fs9.readFile(path10.resolve(source), "utf8"));
6207
+ const bundle = JSON.parse(await fs11.readFile(path11.resolve(source), "utf8"));
5588
6208
  const id = await store.importIdentity(bundle, { force });
5589
6209
  return { text: `Identity imported: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
5590
6210
  }
@@ -5684,8 +6304,20 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
5684
6304
  throw new EdgeBookError("unknown_action", `Unknown profile action: ${action} (use "show", "set", "visibility", or "broadcast")`);
5685
6305
  }
5686
6306
  if (command === "doctor") {
5687
- const result = await store.doctor();
5688
- return { text: JSON.stringify(result, null, 2), json: result };
6307
+ const asJson = takeBoolFlag(args, "--json");
6308
+ const send = takeBoolFlag(args, "--send");
6309
+ const host = parseHost(args, ctx);
6310
+ if (send) {
6311
+ return runDoctorSend(store, home, {
6312
+ host,
6313
+ yes: takeBoolFlag(args, "--yes"),
6314
+ to: takeFlag(args, "--to"),
6315
+ note: takeFlag(args, "--note"),
6316
+ socketFactory: ctx.socketFactory
6317
+ });
6318
+ }
6319
+ const report = await buildDoctorReport(store, { host });
6320
+ return { text: asJson ? JSON.stringify(report, null, 2) : renderDoctorText(report), json: report };
5689
6321
  }
5690
6322
  if (command === "card") {
5691
6323
  const action = args.shift() || "show";
@@ -5696,35 +6328,38 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
5696
6328
  if (action === "export") {
5697
6329
  const target = requireArg(takeFlag(args, "--path"), "--path");
5698
6330
  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)}
6331
+ await fs11.mkdir(path11.dirname(path11.resolve(target)), { recursive: true });
6332
+ await fs11.writeFile(path11.resolve(target), `${JSON.stringify(card, null, 2)}
5701
6333
  `, "utf8");
5702
- return { text: `Exported Agent Card to ${path10.resolve(target)}`, json: card };
6334
+ return { text: `Exported Agent Card to ${path11.resolve(target)}`, json: card };
5703
6335
  }
5704
6336
  if (action === "invite") {
5705
6337
  const ttlMsStr = takeFlag(args, "--ttl-ms");
5706
6338
  const usesStr = takeFlag(args, "--uses");
6339
+ const raw = takeBoolFlag(args, "--raw");
5707
6340
  const ttlMs = ttlMsStr ? Number(ttlMsStr) : void 0;
5708
6341
  const maxUses = usesStr ? Number(usesStr) : void 0;
5709
6342
  const card = await store.writeCard();
5710
- const baseUrl = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
6343
+ const blob = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
6344
+ const origin = card.card_url ? new URL(card.card_url).origin : relayBaseFromHost(parseHost(args, ctx));
6345
+ const deeplink_url = `${origin}/add#i=${encodeURIComponent(blob)}`;
5711
6346
  if (ttlMs !== void 0 || maxUses !== void 0) {
5712
6347
  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 } };
6348
+ const invite_url = `${blob}#code=${invite.code}`;
6349
+ return { text: raw ? invite_url : deeplink_url, json: { invite_url, deeplink_url, agent_id: card.agent_id, invite_code: invite.code } };
5715
6350
  }
5716
- return { text: baseUrl, json: { invite_url: baseUrl, agent_id: card.agent_id } };
6351
+ return { text: raw ? blob : deeplink_url, json: { invite_url: blob, deeplink_url, agent_id: card.agent_id } };
5717
6352
  }
5718
6353
  }
5719
6354
  return null;
5720
6355
  }
5721
6356
 
5722
6357
  // src/cli-social.ts
5723
- import path11 from "path";
6358
+ import path12 from "path";
5724
6359
 
5725
6360
  // src/store-greeter.ts
5726
6361
  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.";
6362
+ var GREETER_WELCOME_BODY = "Hi, and welcome! This is your first share on Edge Book. Your agent can read it to you whenever you ask. Sharing works both ways here: when someone shares with you, you can read it until they take it back \u2014 and anything you share, you can take back too. Try it: ask your agent to show you this note, then share something of your own with a friend. Curious who else is here? Ask your agent: edge-book directory. Glad you're here.";
5728
6363
  function greeterWelcomeKey(agentId) {
5729
6364
  return `greeter_welcome:${agentId}`;
5730
6365
  }
@@ -5771,7 +6406,7 @@ async function runGreeterPass(store) {
5771
6406
  }
5772
6407
 
5773
6408
  // src/cli-social.ts
5774
- import fs10 from "fs/promises";
6409
+ import fs12 from "fs/promises";
5775
6410
  async function handleSocialCli(command, args, ctx, home, store) {
5776
6411
  if (command === "resolve") {
5777
6412
  const target = requireArg(args.shift(), "target");
@@ -5855,7 +6490,7 @@ next: ${result.next_action}`, json: result };
5855
6490
  const deliver = takeBoolFlag(args, "--deliver");
5856
6491
  const source = requireArg(args.shift(), "envelope-json-path");
5857
6492
  const followUp = await store.applyFriendResponse(await readEnvelope(source));
5858
- if (!followUp) return { text: `Applied friend response from ${path11.resolve(source)}` };
6493
+ if (!followUp) return { text: `Applied friend response from ${path12.resolve(source)}` };
5859
6494
  if (deliver) {
5860
6495
  try {
5861
6496
  return { text: await deliverToPeer(store, followUp, followUp.to_agent_id), json: followUp };
@@ -5958,8 +6593,8 @@ next: ${result.next_action}`, json: result };
5958
6593
  const file = takeFlag(args, "--file");
5959
6594
  let attachment;
5960
6595
  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 };
6596
+ const bytes = await fs12.readFile(path12.resolve(file));
6597
+ attachment = { filename: path12.basename(file), mime: takeFlag(args, "--mime") || "application/octet-stream", bytes };
5963
6598
  }
5964
6599
  const object = await store.createObject({ title, body, attachment });
5965
6600
  return { text: `Created object ${object.object_id}`, json: object };
@@ -5999,7 +6634,7 @@ next: ${result.next_action}`, json: result };
5999
6634
  if (action === "receive") {
6000
6635
  const source = requireArg(args.shift(), "envelope-json-path");
6001
6636
  await store.receiveEnvelope(await readEnvelope(source));
6002
- return { text: `Applied object envelope from ${path11.resolve(source)}` };
6637
+ return { text: `Applied object envelope from ${path12.resolve(source)}` };
6003
6638
  }
6004
6639
  if (action === "list") {
6005
6640
  const objects2 = await store.sharedObjectsFor();
@@ -6038,7 +6673,7 @@ next: ${result.next_action}`, json: result };
6038
6673
  if (action === "receive") {
6039
6674
  const source = requireArg(args.shift(), "envelope-json-path");
6040
6675
  await store.receivePrivilegedMessage(await readEnvelope(source));
6041
- return { text: `Received privileged message from ${path11.resolve(source)}` };
6676
+ return { text: `Received privileged message from ${path12.resolve(source)}` };
6042
6677
  }
6043
6678
  }
6044
6679
  if (command === "escalation") {
@@ -6119,6 +6754,57 @@ next: ${result.next_action}`, json: result };
6119
6754
  return null;
6120
6755
  }
6121
6756
 
6757
+ // src/cli-support.ts
6758
+ function bundleLine(b) {
6759
+ return `${b.bundle_id} ${b.received_at} ${b.from_display_name ?? "(no name)"} (${b.from_agent_id}) ref=${b.trace_id ?? "-"}`;
6760
+ }
6761
+ async function handleSupportCli(command, args, _ctx, _home, store) {
6762
+ if (command !== "support") return null;
6763
+ const action = args.shift();
6764
+ if (action === "inbox") {
6765
+ const on = takeBoolFlag(args, "--on");
6766
+ const off = takeBoolFlag(args, "--off");
6767
+ if (on && off) throw new EdgeBookError("bad_flags", "support inbox takes either --on or --off, not both");
6768
+ if (!on && !off) throw new EdgeBookError("missing_arg", "support inbox needs --on or --off");
6769
+ const cfg = await store.updateConfig({ support_inbox: on ? true : false });
6770
+ return { text: `support_inbox = ${cfg.support_inbox}`, json: cfg };
6771
+ }
6772
+ if (action === "pending") {
6773
+ const bundles = await pendingSupportBundles(store);
6774
+ const text = bundles.length ? bundles.map(bundleLine).join("\n") : "No pending support bundles.";
6775
+ return { text, json: bundles };
6776
+ }
6777
+ if (action === "read") {
6778
+ const bundleId = requireArg(args.shift(), "bundle-id");
6779
+ const record = await setSupportBundleStatus(store, bundleId, "read");
6780
+ const header = [
6781
+ `Support bundle ${record.bundle_id} (marked read)`,
6782
+ `from: ${record.from_display_name ?? "(no name)"} (${record.from_agent_id})`,
6783
+ `ref: ${record.trace_id ?? "-"}`,
6784
+ `at: ${record.received_at}`,
6785
+ ...record.note ? [`note: ${record.note}`] : [],
6786
+ ""
6787
+ ].join("\n");
6788
+ return { text: `${header}${JSON.stringify(record.report, null, 2)}`, json: record };
6789
+ }
6790
+ if (action === "dismiss") {
6791
+ const bundleId = requireArg(args.shift(), "bundle-id");
6792
+ const record = await setSupportBundleStatus(store, bundleId, "dismissed");
6793
+ return { text: `Dismissed support bundle ${record.bundle_id}`, json: record };
6794
+ }
6795
+ if (action === "receive") {
6796
+ const source = requireArg(args.shift(), "envelope-json-path");
6797
+ const record = await receiveSupportBundle(store, await readEnvelope(source));
6798
+ return { text: `Received support bundle ${record.bundle_id} from ${record.from_agent_id} (ref ${record.trace_id ?? "-"})`, json: record };
6799
+ }
6800
+ if (action === "list") {
6801
+ const all = Object.values(await supportBundles(store)).sort((a, b) => a.received_at.localeCompare(b.received_at));
6802
+ const text = all.length ? all.map((b) => `${b.status.padEnd(9)} ${bundleLine(b)}`).join("\n") : "No support bundles.";
6803
+ return { text, json: all };
6804
+ }
6805
+ throw new EdgeBookError("unknown_action", `Unknown support action: ${action} (use "inbox", "pending", "read", "dismiss", "list", or "receive")`);
6806
+ }
6807
+
6122
6808
  // src/cli-taxonomy.ts
6123
6809
  async function handleTaxonomyCli(command, args, ctx, store) {
6124
6810
  if (command === "attest") {
@@ -6236,6 +6922,55 @@ async function handleTaxonomyCli(command, args, ctx, store) {
6236
6922
  return null;
6237
6923
  }
6238
6924
 
6925
+ // src/cli-directory.ts
6926
+ async function handleDirectoryCli(command, args, _ctx, _home, store) {
6927
+ if (command !== "directory") return null;
6928
+ const relayBase = takeFlag(args, "--relay-base") ?? process.env["EDGE_BOOK_RELAY_BASE"] ?? DEFAULT_RELAY_BASE;
6929
+ const limitStr = takeFlag(args, "--limit");
6930
+ const parsed = parseInt(limitStr ?? "", 10);
6931
+ const limit = limitStr ? Math.min(500, Math.max(1, Number.isNaN(parsed) ? 100 : parsed)) : 100;
6932
+ const url = `${relayBase.replace(/\/$/, "")}/directory?limit=${limit}&offset=0`;
6933
+ let data;
6934
+ try {
6935
+ const res = await fetch(url);
6936
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
6937
+ data = await res.json();
6938
+ } catch {
6939
+ return {
6940
+ text: "Could not reach the Edge Book relay. Check your connection or set EDGE_BOOK_RELAY_BASE.",
6941
+ json: null
6942
+ };
6943
+ }
6944
+ const { handles, total } = data;
6945
+ if (handles.length === 0) {
6946
+ return { text: "No agents are currently listed in the directory.", json: data };
6947
+ }
6948
+ const contactMap = await store.contacts().catch((err) => {
6949
+ process.stderr.write(`[directory] could not load contacts: ${String(err)}
6950
+ `);
6951
+ return {};
6952
+ });
6953
+ const relByHandle = /* @__PURE__ */ new Map();
6954
+ for (const c of Object.values(contactMap)) {
6955
+ const label = c.relationship_state === "friend" ? "friend" : c.relationship_state === "request_sent" ? "request sent" : c.relationship_state === "request_received" ? "request received" : "";
6956
+ if (label) {
6957
+ for (const alias of c.aliases) {
6958
+ relByHandle.set(alias.toLowerCase().replace(/^@/, ""), label);
6959
+ }
6960
+ }
6961
+ }
6962
+ const lines = handles.map((h) => {
6963
+ const owner = h.owner_label ? ` [${h.owner_label}]` : "";
6964
+ const rel = relByHandle.get(h.handle);
6965
+ const relPart = rel ? ` (${rel})` : "";
6966
+ return `@${h.handle} ${h.display_name}${owner}${relPart}`;
6967
+ });
6968
+ lines.push("");
6969
+ lines.push(`${handles.length} of ${total} agents listed.`);
6970
+ lines.push("To connect: edge-book friend request <handle> --deliver");
6971
+ return { text: lines.join("\n"), json: data };
6972
+ }
6973
+
6239
6974
  // src/commands-doc.ts
6240
6975
  var COMMAND_GROUPS = [
6241
6976
  {
@@ -6251,8 +6986,8 @@ var COMMAND_GROUPS = [
6251
6986
  title: "Handle / Identity",
6252
6987
  rows: [
6253
6988
  {
6254
- usage: "handle set <slug>",
6255
- desc: "Claim a unique human handle (replaces the default)"
6989
+ usage: "handle set <slug> [--hidden]",
6990
+ desc: "Claim a unique human handle (replaces the default); --hidden opts out of the /directory listing"
6256
6991
  },
6257
6992
  {
6258
6993
  usage: "handle show",
@@ -6512,8 +7247,41 @@ var COMMAND_GROUPS = [
6512
7247
  title: "Diagnostics",
6513
7248
  rows: [
6514
7249
  {
6515
- usage: "doctor",
6516
- desc: "Check your store, card, and key-file permissions"
7250
+ usage: "doctor [--json] [--host <wss-url>]",
7251
+ desc: "Diagnostic bundle: identity, relay reachability, dial-out state, stores, event-log tail (safe to paste publicly)"
7252
+ },
7253
+ {
7254
+ usage: "doctor --send [--yes] [--note <n>] [--to <did>] [--host <wss-url>]",
7255
+ desc: "Send the sanitized bundle to the operator support mailbox (consent prompt; prints a support reference)"
7256
+ }
7257
+ ]
7258
+ },
7259
+ {
7260
+ title: "Support inbox (operator)",
7261
+ rows: [
7262
+ {
7263
+ usage: "support inbox --on|--off",
7264
+ desc: "Opt this agent in/out as a support mailbox (off by default; inbound bundles are rejected)"
7265
+ },
7266
+ {
7267
+ usage: "support pending",
7268
+ desc: "List received support bundles awaiting review"
7269
+ },
7270
+ {
7271
+ usage: "support read <bundle-id>",
7272
+ desc: "Show a bundle's report and mark it read"
7273
+ },
7274
+ {
7275
+ usage: "support dismiss <bundle-id>",
7276
+ desc: "Dismiss a bundle without reading it"
7277
+ },
7278
+ {
7279
+ usage: "support list",
7280
+ desc: "List all support bundles including read/dismissed"
7281
+ },
7282
+ {
7283
+ usage: "support receive <envelope-json-path>",
7284
+ desc: "Apply an inbound support_bundle envelope from a file"
6517
7285
  }
6518
7286
  ]
6519
7287
  },
@@ -6578,6 +7346,15 @@ var COMMAND_GROUPS = [
6578
7346
  }
6579
7347
  ]
6580
7348
  },
7349
+ {
7350
+ title: "Network",
7351
+ rows: [
7352
+ {
7353
+ usage: "directory [--limit N] [--relay-base <url>]",
7354
+ desc: "List agents on the network with relationship annotations; EDGE_BOOK_RELAY_BASE env var overrides the default relay"
7355
+ }
7356
+ ]
7357
+ },
6581
7358
  {
6582
7359
  title: "Server / harness",
6583
7360
  rows: [
@@ -6659,10 +7436,15 @@ async function notifyInbound(store, envelope, opts) {
6659
7436
  if (!opts.cmd || !opts.cmd.trim()) return { notified: false, reason: "no_notify_cmd" };
6660
7437
  const intent = await store.notificationIntent(envelope);
6661
7438
  if (!intent) return { notified: false, reason: "silent" };
6662
- if (await store.wasNotified(intent.dedup_key)) return { notified: false, reason: "already_notified" };
7439
+ if (await store.wasNotified(intent.dedup_key)) {
7440
+ await logEvent(store, "notify.suppressed", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key, reason: "already_notified" });
7441
+ return { notified: false, reason: "already_notified" };
7442
+ }
7443
+ await logEvent(store, "notify.attempted", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
6663
7444
  const res = await deliverNotification(intent, opts);
6664
7445
  if (!res.delivered) {
6665
7446
  await store.audit("notify.failed", intent.from_id, { kind: intent.kind, dedup_key: intent.dedup_key, error: res.error ?? "" });
7447
+ await logEvent(store, "notify.failed", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
6666
7448
  return { notified: false, reason: res.error };
6667
7449
  }
6668
7450
  await store.recordNotified(intent.dedup_key);
@@ -6673,6 +7455,7 @@ async function notifyInbound(store, envelope, opts) {
6673
7455
  }
6674
7456
  }
6675
7457
  await store.audit("notify.delivered", intent.from_id, { kind: intent.kind, dedup_key: intent.dedup_key, channel: "hook" });
7458
+ await logEvent(store, "notify.delivered", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
6676
7459
  return { notified: true };
6677
7460
  }
6678
7461
  function makeNotifyOnEnvelope(store, cmd) {
@@ -6685,114 +7468,6 @@ function makeNotifyOnEnvelope(store, cmd) {
6685
7468
  };
6686
7469
  }
6687
7470
 
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
7471
  // src/cli.ts
6797
7472
  function usage() {
6798
7473
  return renderUsage();
@@ -6818,6 +7493,8 @@ async function handleCli(inputArgs, ctx = {}) {
6818
7493
  if (command === "friend" && socialAction === "auto-accept") return socialResult;
6819
7494
  return maybeAppendHandleNudge(store, command, socialResult);
6820
7495
  }
7496
+ const supportResult = await handleSupportCli(command, args, ctx, home, store);
7497
+ if (supportResult) return supportResult;
6821
7498
  if (command === "serve") {
6822
7499
  const host = takeFlag(args, "--host") || "127.0.0.1";
6823
7500
  const port = Number(takeFlag(args, "--port") || "0");
@@ -6845,6 +7522,8 @@ async function handleCli(inputArgs, ctx = {}) {
6845
7522
  const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
6846
7523
  try {
6847
7524
  const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
7525
+ if (res.status === "installed") await logEvent(store2, "cron.notifier_installed", {});
7526
+ else if (res.status === "already_present") await logEvent(store2, "cron.notifier_already_present", {});
6848
7527
  if (res.status === "installed") console.log(` \u21B3 notifier cron self-installed ("Edge Book \u2014 friend requests", every 20m \u2192 telegram)`);
6849
7528
  else if (res.status === "error") console.log(` \u21B3 notifier cron install skipped: ${res.detail}`);
6850
7529
  } catch (e) {
@@ -6864,6 +7543,8 @@ async function handleCli(inputArgs, ctx = {}) {
6864
7543
  if (command === "ensure-notifier") {
6865
7544
  const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
6866
7545
  const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
7546
+ if (res.status === "installed") await logEvent(store, "cron.notifier_installed", {});
7547
+ else if (res.status === "already_present") await logEvent(store, "cron.notifier_already_present", {});
6867
7548
  const msg = {
6868
7549
  installed: 'Installed notifier cron "Edge Book \u2014 friend requests" (every 20m \u2192 telegram).',
6869
7550
  already_present: "Notifier cron already present \u2014 nothing to do.",
@@ -6890,8 +7571,22 @@ async function handleCli(inputArgs, ctx = {}) {
6890
7571
  const registration2 = await client.pair(ttlMs);
6891
7572
  console.log(`Pairing code: ${registration2.code}`);
6892
7573
  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);
7574
+ console.log("Waiting for your browser reader to connect...");
7575
+ const pairResult = await client.waitForPairComplete(ttlMs);
7576
+ await client.stop();
7577
+ if (!pairResult) {
7578
+ throw new EdgeBookError("pair_timeout", "Pairing code expired unredeemed \u2014 run edge-book pair again for a fresh code.");
7579
+ }
7580
+ console.log(`
7581
+ Pairing complete \u2014 your reader is connected (device: ${pairResult.label}).`);
7582
+ const notifyCmd = resolveNotifyCmd({ env: process.env.EDGE_BOOK_NOTIFY_CMD, config: (await store.config()).notify_cmd });
7583
+ const intent = buildPairCompleteNotifyIntent(pairResult.device_id, pairResult.label);
7584
+ if (notifyCmd && !await store.wasNotified(intent.dedup_key)) {
7585
+ const nr = await deliverNotification(intent, { cmd: notifyCmd });
7586
+ if (nr.delivered) await store.recordNotified(intent.dedup_key);
7587
+ }
7588
+ console.log("\n" + buildOnboardingNote());
7589
+ return { text: `Pairing complete \u2014 your reader is connected (device: ${pairResult.label}).`, json: { device_id: pairResult.device_id, label: pairResult.label } };
6895
7590
  }
6896
7591
  const registration = await sendPairRegistration({ home, host: hostUrl, ttlMs, socketFactory: ctx.socketFactory });
6897
7592
  return { text: `Pairing code: ${registration.code}
@@ -6979,6 +7674,8 @@ ${JSON.stringify(result, null, 2)}`, json: result };
6979
7674
  }
6980
7675
  const taxonomyResult = await handleTaxonomyCli(command, args, ctx, store);
6981
7676
  if (taxonomyResult) return maybeAppendHandleNudge(store, command, taxonomyResult);
7677
+ const directoryResult = await handleDirectoryCli(command, args, ctx, home, store);
7678
+ if (directoryResult) return directoryResult;
6982
7679
  throw new EdgeBookError("unknown_command", usage());
6983
7680
  }
6984
7681
  async function runCli(args) {