edge-book 0.17.0 → 0.18.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 +11 -5
  2. package/dist/edge-book.js +787 -112
  3. package/package.json +1 -1
package/dist/edge-book.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { realpathSync } from "fs";
5
- import { fileURLToPath } from "url";
5
+ import { fileURLToPath as fileURLToPath2 } from "url";
6
6
 
7
7
  // src/cli-shared.ts
8
- import fs9 from "fs/promises";
9
- import path10 from "path";
8
+ import fs11 from "fs/promises";
9
+ import path11 from "path";
10
10
 
11
11
  // src/dialout.ts
12
12
  import crypto6 from "crypto";
@@ -18,7 +18,7 @@ import fs from "fs/promises";
18
18
  import path from "path";
19
19
  var DIALOUT_KEY_FILE = "host-dialout-key.json";
20
20
  var KEY_FILE = DIALOUT_KEY_FILE;
21
- var DEFAULT_PAIR_TTL_MS = 5 * 60 * 1e3;
21
+ var DEFAULT_PAIR_TTL_MS = 10 * 60 * 1e3;
22
22
  var PAIRING_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
23
23
  function now() {
24
24
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -1702,12 +1702,13 @@ async function receiveFriendRequest(store, envelope) {
1702
1702
  return contact;
1703
1703
  }
1704
1704
  async function pendingFriendRequests(store) {
1705
+ const contacts = await store.contacts();
1706
+ return Object.values(contacts).filter((c) => c.relationship_state === "request_received");
1707
+ }
1708
+ async function unnotifiedFriendRequests(store) {
1705
1709
  const config = await store.config();
1706
1710
  if (config.notify_on_friend_request === false) return [];
1707
- const contacts = await store.contacts();
1708
- return Object.values(contacts).filter(
1709
- (c) => c.relationship_state === "request_received" && !c.notified_at
1710
- );
1711
+ return (await pendingFriendRequests(store)).filter((c) => !c.notified_at);
1711
1712
  }
1712
1713
  async function markFriendRequestNotified(store, peerAgentId) {
1713
1714
  const contacts = await store.contacts();
@@ -2031,8 +2032,16 @@ async function updateConfig(store, input) {
2031
2032
  if (input.onboarding_nudge_at !== void 0) next.onboarding_nudge_at = input.onboarding_nudge_at;
2032
2033
  if (input.onboarding_nudge_count !== void 0) next.onboarding_nudge_count = input.onboarding_nudge_count;
2033
2034
  if (input.greeter_mode !== void 0) next.greeter_mode = input.greeter_mode;
2035
+ if (input.notifier_prompt_ack !== void 0) next.notifier_prompt_ack = input.notifier_prompt_ack;
2036
+ if (input.notifier_nudge_at !== void 0) next.notifier_nudge_at = input.notifier_nudge_at;
2034
2037
  if (input.support_inbox !== void 0) next.support_inbox = input.support_inbox;
2035
2038
  if (input.greeter_welcome_object_id !== void 0) next.greeter_welcome_object_id = input.greeter_welcome_object_id;
2039
+ if (input.auto_update !== void 0) next.auto_update = input.auto_update;
2040
+ if (input.update_check_at !== void 0) next.update_check_at = input.update_check_at;
2041
+ if (input.update_latest_known !== void 0) next.update_latest_known = input.update_latest_known;
2042
+ if (input.update_nudge_at !== void 0) next.update_nudge_at = input.update_nudge_at;
2043
+ if (input.updated_at !== void 0) next.updated_at = input.updated_at;
2044
+ if (input.dialout_respawn_expected !== void 0) next.dialout_respawn_expected = input.dialout_respawn_expected;
2036
2045
  await writeJson(store.file(CONFIG_FILE), next);
2037
2046
  return next;
2038
2047
  }
@@ -2744,11 +2753,15 @@ var EdgeBookStore = class {
2744
2753
  async receiveFriendRequest(envelope) {
2745
2754
  return receiveFriendRequest(this, envelope);
2746
2755
  }
2747
- // Inbound friend requests the human hasn't been told about yet. Empty when the
2748
- // agent has notifications disabled. Read-only — the notifier cron consumes this.
2756
+ // All inbound friend requests awaiting accept/decline (spec-139). Read-only.
2749
2757
  async pendingFriendRequests() {
2750
2758
  return pendingFriendRequests(this);
2751
2759
  }
2760
+ // Inbound friend requests the human hasn't been told about yet. Empty when the
2761
+ // agent has notifications disabled. Read-only — the notifier cron consumes this.
2762
+ async unnotifiedFriendRequests() {
2763
+ return unnotifiedFriendRequests(this);
2764
+ }
2752
2765
  // Stamp a request as notified so it won't surface again (idempotent sweep,
2753
2766
  // mirrors expireEscalations).
2754
2767
  async markFriendRequestNotified(peerAgentId) {
@@ -5110,6 +5123,246 @@ function gateHostFrame(frame) {
5110
5123
  return { ok: false, frameType: type, errorPaths: paths };
5111
5124
  }
5112
5125
 
5126
+ // src/update-drift.ts
5127
+ import fs9 from "fs/promises";
5128
+
5129
+ // src/self-update.ts
5130
+ import { execFile } from "child_process";
5131
+ import fs8 from "fs/promises";
5132
+ import path8 from "path";
5133
+ import { fileURLToPath } from "url";
5134
+ import { promisify } from "util";
5135
+ var execFileAsync = promisify(execFile);
5136
+ var REGISTRY_LATEST_URL = "https://registry.npmjs.org/edge-book/latest";
5137
+ var REGISTRY_TIMEOUT_MS = 3e3;
5138
+ var UPDATE_CHECK_THROTTLE_MS = 24 * 60 * 60 * 1e3;
5139
+ var SELF_UPDATE_LOCK = ".edge-book-self-update.lock";
5140
+ var LOCK_STALE_MS = 15 * 60 * 1e3;
5141
+ var SYSTEM_ROOT_PREFIXES = ["/usr/lib", "/usr/local/lib", "/usr/share", "/opt/homebrew", "/snap/", "/nix/store"];
5142
+ var cachedRunning;
5143
+ async function runningVersion() {
5144
+ if (cachedRunning) return cachedRunning;
5145
+ try {
5146
+ const pkg = JSON.parse(await fs8.readFile(new URL("../package.json", import.meta.url), "utf8"));
5147
+ cachedRunning = typeof pkg.version === "string" ? pkg.version : "0.0.0";
5148
+ } catch {
5149
+ cachedRunning = "0.0.0";
5150
+ }
5151
+ return cachedRunning;
5152
+ }
5153
+ function compareVersions(a, b) {
5154
+ const pa = a.split("-")[0].split(".").map(Number);
5155
+ const pb = b.split("-")[0].split(".").map(Number);
5156
+ for (let i = 0; i < 3; i++) {
5157
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
5158
+ if (d !== 0) return d < 0 ? -1 : 1;
5159
+ }
5160
+ return 0;
5161
+ }
5162
+ function majorOf(version) {
5163
+ return Number(version.split(".")[0]) || 0;
5164
+ }
5165
+ function autoUpdateAllowed(running, latest) {
5166
+ const runningMajor = majorOf(running);
5167
+ if (runningMajor >= 1 && majorOf(latest) !== runningMajor) return false;
5168
+ return true;
5169
+ }
5170
+ async function checkLatest(store, opts = {}) {
5171
+ const now4 = opts.now ?? Date.now();
5172
+ let cached;
5173
+ try {
5174
+ const config = await store.config();
5175
+ cached = config.update_latest_known;
5176
+ if (!opts.force && config.update_check_at !== void 0 && now4 - config.update_check_at < UPDATE_CHECK_THROTTLE_MS) {
5177
+ return cached;
5178
+ }
5179
+ const fetchImpl = opts.fetchImpl ?? fetch;
5180
+ const response = await fetchImpl(REGISTRY_LATEST_URL, { signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) });
5181
+ if (!response.ok) throw new Error(`registry ${response.status}`);
5182
+ const body = await response.json();
5183
+ if (typeof body.version !== "string" || !/^\d+\.\d+\.\d+/.test(body.version)) throw new Error("malformed registry response");
5184
+ await store.updateConfig({ update_check_at: now4, update_latest_known: body.version });
5185
+ return body.version;
5186
+ } catch {
5187
+ await store.updateConfig({ update_check_at: now4 }).catch(() => void 0);
5188
+ return cached;
5189
+ }
5190
+ }
5191
+ function resolveInstallRoot(modulePath = fileURLToPath(import.meta.url)) {
5192
+ let dir = path8.dirname(modulePath);
5193
+ for (; ; ) {
5194
+ const parent = path8.dirname(dir);
5195
+ if (path8.basename(dir) === "edge-book" && path8.basename(parent) === "node_modules") {
5196
+ return path8.dirname(parent);
5197
+ }
5198
+ if (parent === dir) return null;
5199
+ dir = parent;
5200
+ }
5201
+ }
5202
+ function notSelfUpdatable(detail, manualCommand) {
5203
+ return new EdgeBookError(
5204
+ "install_not_self_updatable",
5205
+ `install_not_self_updatable: ${detail}. Update manually with the right permissions: ${manualCommand}`
5206
+ );
5207
+ }
5208
+ async function assertSelfUpdatableRoot(root) {
5209
+ if (root === null) {
5210
+ throw notSelfUpdatable("this edge-book does not run from an npm install root", "npm install -g edge-book@latest");
5211
+ }
5212
+ const manual = `npm install edge-book@latest --prefix ${root}`;
5213
+ if (SYSTEM_ROOT_PREFIXES.some((prefix) => root === prefix.replace(/\/$/, "") || root.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`))) {
5214
+ throw notSelfUpdatable(`install root ${root} is system-managed`, manual);
5215
+ }
5216
+ try {
5217
+ await fs8.access(root, fs8.constants.W_OK);
5218
+ await fs8.access(path8.join(root, "node_modules"), fs8.constants.W_OK);
5219
+ } catch {
5220
+ throw notSelfUpdatable(`install root ${root} is not writable by this process`, manual);
5221
+ }
5222
+ return root;
5223
+ }
5224
+ async function acquireLock(lockPath, now4) {
5225
+ for (let attempt = 0; attempt < 2; attempt++) {
5226
+ try {
5227
+ await fs8.writeFile(lockPath, `${process.pid} ${new Date(now4).toISOString()}
5228
+ `, { flag: "wx" });
5229
+ return;
5230
+ } catch (error) {
5231
+ if (error.code !== "EEXIST") throw error;
5232
+ const stat = await fs8.stat(lockPath).catch(() => null);
5233
+ if (stat && now4 - stat.mtimeMs > LOCK_STALE_MS) {
5234
+ await fs8.rm(lockPath, { force: true });
5235
+ continue;
5236
+ }
5237
+ throw new EdgeBookError("update_in_progress", `update_in_progress: another self-update holds ${lockPath}`);
5238
+ }
5239
+ }
5240
+ throw new EdgeBookError("update_in_progress", `update_in_progress: could not acquire ${lockPath}`);
5241
+ }
5242
+ async function defaultNpmInstall(spec, root) {
5243
+ await execFileAsync("npm", ["install", spec, "--prefix", root, "--no-audit", "--no-fund"], { timeout: 18e4 });
5244
+ }
5245
+ async function defaultVerify(root) {
5246
+ const entry = path8.join(root, "node_modules", "edge-book", "dist", "edge-book.js");
5247
+ const { stdout } = await execFileAsync(process.execPath, [entry, "--version"], { timeout: 3e4 });
5248
+ return stdout.trim();
5249
+ }
5250
+ async function selfAgentId(store) {
5251
+ try {
5252
+ return (await store.identity()).agent_id;
5253
+ } catch {
5254
+ return "self";
5255
+ }
5256
+ }
5257
+ async function recordFailure(store, from, to, reason) {
5258
+ await store.audit("update.self", await selfAgentId(store), { from, to, ok: false, reason });
5259
+ await logEvent(store, "update.failed", { from, to, reason });
5260
+ }
5261
+ function restartAdvice(autoRespawn) {
5262
+ return autoRespawn ? "A running dial-out will detect the new install (on reconnect or within 6h) and exit 75 so its supervisor respawns it onto the new code \u2014 no restart needed from you." : "Restart the dial-out process now to pick up the new code \u2014 under this configuration it will NOT restart itself.";
5263
+ }
5264
+ async function performUpdate(store, root, from, latest, opts, now4) {
5265
+ const install = opts.npmInstall ?? defaultNpmInstall;
5266
+ try {
5267
+ await install(`edge-book@${latest}`, root);
5268
+ } catch (error) {
5269
+ await recordFailure(store, from, latest, eventErrorCode(error));
5270
+ throw new EdgeBookError("update_failed", `update_failed: npm install edge-book@${latest} --prefix ${root} failed; the previous install remains in place.`);
5271
+ }
5272
+ let reported;
5273
+ try {
5274
+ reported = (await (opts.verify ?? defaultVerify)(root)).trim();
5275
+ } catch (error) {
5276
+ reported = `spawn_failed:${eventErrorCode(error)}`;
5277
+ }
5278
+ if (reported !== latest) {
5279
+ await recordFailure(store, from, latest, `verify_mismatch:${reported}`);
5280
+ throw new EdgeBookError("update_failed", `update_failed: the new build reports "${reported}" instead of ${latest}; not trusting it. The npm tree at ${root} may need a manual reinstall.`);
5281
+ }
5282
+ await store.audit("update.self", await selfAgentId(store), { from, to: latest });
5283
+ await logEvent(store, "update.self", { from, to: latest });
5284
+ const config = await store.updateConfig({ updated_at: now4, update_latest_known: latest });
5285
+ const autoRespawn = (config.auto_update ?? "auto") === "auto" && config.dialout_respawn_expected !== false;
5286
+ return { status: "updated", from, to: latest, text: `Updated edge-book ${from} \u2192 ${latest} in ${root}.
5287
+ ${restartAdvice(autoRespawn)}` };
5288
+ }
5289
+ async function selfUpdate(store, opts = {}) {
5290
+ const now4 = opts.now ?? Date.now();
5291
+ const from = opts.running ?? await runningVersion();
5292
+ const config = await store.config();
5293
+ const mode = config.auto_update ?? "auto";
5294
+ const latest = await checkLatest(store, { now: now4, fetchImpl: opts.fetchImpl, force: true });
5295
+ if (!latest) {
5296
+ if (opts.ifStale) return { status: "current", from, text: "" };
5297
+ throw new EdgeBookError("registry_unreachable", "registry_unreachable: could not determine the latest edge-book version from the npm registry.");
5298
+ }
5299
+ if (compareVersions(latest, from) <= 0) {
5300
+ return { status: "current", from, to: latest, text: opts.ifStale ? "" : `edge-book ${from} is current (latest published: ${latest}); never downgrading.` };
5301
+ }
5302
+ if (opts.ifStale && (mode !== "auto" || !autoUpdateAllowed(from, latest))) {
5303
+ return { status: "skipped", from, to: latest, text: "" };
5304
+ }
5305
+ const root = await assertSelfUpdatableRoot(opts.installRoot !== void 0 ? opts.installRoot : resolveInstallRoot());
5306
+ if (opts.dryRun) {
5307
+ return { status: "dry_run", from, to: latest, text: `Would update edge-book ${from} \u2192 ${latest} in ${root} (dry run; nothing installed).` };
5308
+ }
5309
+ const lockPath = path8.join(root, SELF_UPDATE_LOCK);
5310
+ await acquireLock(lockPath, now4);
5311
+ try {
5312
+ return await performUpdate(store, root, from, latest, opts, now4);
5313
+ } finally {
5314
+ await fs8.rm(lockPath, { force: true }).catch(() => void 0);
5315
+ }
5316
+ }
5317
+
5318
+ // src/update-drift.ts
5319
+ var DRIFT_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
5320
+ async function installedVersion() {
5321
+ try {
5322
+ const pkg = JSON.parse(await fs9.readFile(new URL("../package.json", import.meta.url), "utf8"));
5323
+ return typeof pkg.version === "string" ? pkg.version : void 0;
5324
+ } catch {
5325
+ return void 0;
5326
+ }
5327
+ }
5328
+ function createUpdateDriftMonitor(store, deps = {}) {
5329
+ const running = deps.running !== void 0 ? Promise.resolve(deps.running) : runningVersion();
5330
+ const installed = deps.installed ?? installedVersion;
5331
+ const exit = deps.exit ?? ((code) => process.exit(code));
5332
+ const check2 = deps.checkLatestImpl ?? ((s) => checkLatest(s));
5333
+ let timer;
5334
+ async function driftCheck() {
5335
+ try {
5336
+ const inMemory = await running;
5337
+ const onDisk = await installed();
5338
+ if (!onDisk || onDisk === inMemory) return;
5339
+ const config = await store.config();
5340
+ const mode = config.auto_update ?? "auto";
5341
+ const respawnExpected = config.dialout_respawn_expected !== false;
5342
+ await logEvent(store, "dialout.version_drift", { running: inMemory, installed: onDisk, mode, respawn_expected: respawnExpected });
5343
+ if (mode !== "auto" || !respawnExpected) return;
5344
+ console.log(`dialout.restart_for_update: installed edge-book ${onDisk} != running ${inMemory} \u2014 exiting 75 for supervisor respawn`);
5345
+ await logEvent(store, "dialout.restart_for_update", { from: inMemory, to: onDisk });
5346
+ exit(75);
5347
+ } catch {
5348
+ }
5349
+ }
5350
+ return {
5351
+ onConnect: () => driftCheck(),
5352
+ start() {
5353
+ if (timer) return;
5354
+ timer = setInterval(() => {
5355
+ void check2(store).catch(() => void 0).then(() => driftCheck());
5356
+ }, deps.intervalMs ?? DRIFT_CHECK_INTERVAL_MS);
5357
+ timer.unref?.();
5358
+ },
5359
+ stop() {
5360
+ if (timer) clearInterval(timer);
5361
+ timer = void 0;
5362
+ }
5363
+ };
5364
+ }
5365
+
5113
5366
  // src/dialout-oneshot.ts
5114
5367
  async function sendPairRegistration(options) {
5115
5368
  const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
@@ -5190,6 +5443,7 @@ function addSocketListener(socket, event, handler) {
5190
5443
  }
5191
5444
  var EdgeBookDialoutClient = class {
5192
5445
  options;
5446
+ driftMonitor;
5193
5447
  store;
5194
5448
  socket;
5195
5449
  localApi;
@@ -5219,6 +5473,7 @@ var EdgeBookDialoutClient = class {
5219
5473
  home: options.home
5220
5474
  };
5221
5475
  this.store = new EdgeBookStore({ home: options.home });
5476
+ this.driftMonitor = options.driftMonitor ?? createUpdateDriftMonitor(this.store);
5222
5477
  this.currentBackoff = this.options.backoffMs;
5223
5478
  }
5224
5479
  async start() {
@@ -5227,6 +5482,7 @@ var EdgeBookDialoutClient = class {
5227
5482
  }
5228
5483
  async stop() {
5229
5484
  this.stopped = true;
5485
+ this.driftMonitor.stop();
5230
5486
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
5231
5487
  if (this.heartbeat) clearInterval(this.heartbeat);
5232
5488
  this.socket?.close();
@@ -5235,7 +5491,15 @@ var EdgeBookDialoutClient = class {
5235
5491
  }
5236
5492
  async pair(ttlMs = DEFAULT_PAIR_TTL_MS) {
5237
5493
  const registration = await createPairRegistration(this.store, ttlMs);
5494
+ const ackPromise = this.awaitAck(registration.frame.request_id, "pair_register", "pair_register_ok", 5e3);
5238
5495
  this.send(registration.frame);
5496
+ try {
5497
+ const ack = await ackPromise;
5498
+ if (ack.type === "pair_register_err") throw new EdgeBookError("pair_register_rejected", String(ack.error || "Host rejected the pairing registration"));
5499
+ if (typeof ack.expires_at === "number") registration.expires_at = ack.expires_at;
5500
+ } catch (error) {
5501
+ if (!(error instanceof EdgeBookError && error.code === "host_rpc_timeout")) throw error;
5502
+ }
5239
5503
  return registration;
5240
5504
  }
5241
5505
  // Wait up to ttlMs for a pair_complete frame from the host.
@@ -5275,15 +5539,20 @@ var EdgeBookDialoutClient = class {
5275
5539
  // request_id. `expect` documents the ack type; resolution is by request_id.
5276
5540
  async rpc(type, extra, expect, timeoutMs) {
5277
5541
  const request_id = crypto6.randomUUID();
5278
- const promise = new Promise((resolve, reject) => {
5542
+ const promise = this.awaitAck(request_id, type, expect, timeoutMs);
5543
+ this.send({ type, request_id, ...extra });
5544
+ return promise;
5545
+ }
5546
+ // Register a pending ack for an already-built frame's request_id (used by
5547
+ // pair(), whose frame is minted in dialout-key.ts rather than by rpc()).
5548
+ awaitAck(request_id, rpcType, expect, timeoutMs) {
5549
+ return new Promise((resolve, reject) => {
5279
5550
  const timer = setTimeout(() => {
5280
5551
  this.pendingRpc.delete(request_id);
5281
5552
  reject(new EdgeBookError("host_rpc_timeout", `Timed out waiting for ${expect}`));
5282
5553
  }, timeoutMs);
5283
- this.pendingRpc.set(request_id, { resolve, reject, timer, rpcType: type });
5554
+ this.pendingRpc.set(request_id, { resolve, reject, timer, rpcType });
5284
5555
  });
5285
- this.send({ type, request_id, ...extra });
5286
- return promise;
5287
5556
  }
5288
5557
  async revokeSessionsAndWait(timeoutMs = 5e3) {
5289
5558
  const frame = await createSessionsRevokeFrame(this.store);
@@ -5437,6 +5706,8 @@ var EdgeBookDialoutClient = class {
5437
5706
  this.opened?.resolve();
5438
5707
  this.opened = void 0;
5439
5708
  void logEvent(this.store, "dialout.connected", { host: this.options.host });
5709
+ void this.driftMonitor.onConnect();
5710
+ this.driftMonitor.start();
5440
5711
  try {
5441
5712
  const identity = await this.store.identity();
5442
5713
  if (shouldClaimHandle(identity.handle)) {
@@ -5465,7 +5736,7 @@ var EdgeBookDialoutClient = class {
5465
5736
  this.send({ type: "pong" });
5466
5737
  return;
5467
5738
  }
5468
- if (frame.type === "sessions_list_ok" || frame.type === "session_revoke_one_ok" || frame.type === "mailbox_status_ok" || frame.type === "mailbox_status_err") {
5739
+ if (frame.type === "sessions_list_ok" || frame.type === "session_revoke_one_ok" || frame.type === "mailbox_status_ok" || frame.type === "mailbox_status_err" || frame.type === "pair_register_ok" || frame.type === "pair_register_err") {
5469
5740
  const ack = frame;
5470
5741
  const pending = this.pendingRpc.get(ack.request_id || "");
5471
5742
  if (pending) {
@@ -5479,7 +5750,6 @@ var EdgeBookDialoutClient = class {
5479
5750
  await this.standDown(frame);
5480
5751
  return;
5481
5752
  }
5482
- if (frame.type === "pair_register_ok" || frame.type === "pair_register_err") return;
5483
5753
  if (frame.type === "pair_complete") {
5484
5754
  this.pairCompleteWaiter.onFrame(frame);
5485
5755
  return;
@@ -5537,6 +5807,7 @@ var EdgeBookDialoutClient = class {
5537
5807
  }
5538
5808
  async standDown(frame) {
5539
5809
  this.stopped = true;
5810
+ this.driftMonitor.stop();
5540
5811
  await logEvent(this.store, "dialout.stand_down", { host: this.options.host, reason: frame.reason, idle_ms: frame.idle_ms });
5541
5812
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
5542
5813
  if (this.heartbeat) clearInterval(this.heartbeat);
@@ -5615,22 +5886,22 @@ var EdgeBookDialoutClient = class {
5615
5886
  };
5616
5887
 
5617
5888
  // src/http-relay.ts
5618
- import fs8 from "fs/promises";
5889
+ import fs10 from "fs/promises";
5619
5890
  import http2 from "http";
5620
- import path8 from "path";
5891
+ import path9 from "path";
5621
5892
  function relayFile(store, agentId) {
5622
- return path8.join(store, `${encodeURIComponent(agentId)}.jsonl`);
5893
+ return path9.join(store, `${encodeURIComponent(agentId)}.jsonl`);
5623
5894
  }
5624
5895
  async function appendRelayEnvelope(store, agentId, envelope) {
5625
- await fs8.mkdir(store, { recursive: true });
5626
- await fs8.appendFile(relayFile(store, agentId), `${JSON.stringify(envelope)}
5896
+ await fs10.mkdir(store, { recursive: true });
5897
+ await fs10.appendFile(relayFile(store, agentId), `${JSON.stringify(envelope)}
5627
5898
  `, "utf8");
5628
5899
  }
5629
5900
  async function drainRelayEnvelopes(store, agentId) {
5630
5901
  const file = relayFile(store, agentId);
5631
5902
  try {
5632
- const text = await fs8.readFile(file, "utf8");
5633
- await fs8.writeFile(file, "", "utf8");
5903
+ const text = await fs10.readFile(file, "utf8");
5904
+ await fs10.writeFile(file, "", "utf8");
5634
5905
  return text.split(/\n/).filter(Boolean).map((line) => JSON.parse(line));
5635
5906
  } catch (error) {
5636
5907
  if (error.code === "ENOENT") return [];
@@ -5690,11 +5961,11 @@ async function pullRelayEnvelopes(relayBaseUrl, recipientAgentId) {
5690
5961
  }
5691
5962
 
5692
5963
  // src/store-outbox.ts
5693
- import path9 from "path";
5964
+ import path10 from "path";
5694
5965
  var OUTBOX_CAP = 200;
5695
5966
  var DEFAULT_STALE_QUEUE_MS = 10 * 60 * 1e3;
5696
5967
  function outboxPath(home) {
5697
- return path9.join(resolveHome(home), OUTBOX_FILE);
5968
+ return path10.join(resolveHome(home), OUTBOX_FILE);
5698
5969
  }
5699
5970
  async function readOutbox(home) {
5700
5971
  return readJson(outboxPath(home), []);
@@ -5764,7 +6035,7 @@ function takeRepeated(args, flag) {
5764
6035
  return out;
5765
6036
  }
5766
6037
  async function readEnvelope(filePath) {
5767
- return JSON.parse(await fs9.readFile(path10.resolve(filePath), "utf8"));
6038
+ return JSON.parse(await fs11.readFile(path11.resolve(filePath), "utf8"));
5768
6039
  }
5769
6040
  async function deliverToEndpoint(envelope, endpoint) {
5770
6041
  await postEnvelope(endpoint, envelope);
@@ -5844,58 +6115,30 @@ ${buildHandleNudge(suggestion)}` };
5844
6115
  }
5845
6116
  }
5846
6117
 
5847
- // src/onboarding-nudge.ts
5848
- var ONBOARDING_NUDGE_THROTTLE_MS = 6 * 60 * 60 * 1e3;
5849
- var ONBOARDING_NUDGE_MAX_EMITS = 3;
5850
- function buildOnboardingNudgeText() {
5851
- return [
5852
- `---`,
5853
- `Onboarding incomplete: this agent has no friends and no shares yet \u2014 the human was likely never onboarded.`,
5854
- `Load skills/edge-book/prompts/onboard.md and run the first-session script with your human now.`
5855
- ].join("\n");
5856
- }
5857
- async function maybeAppendOnboardingNudge(store, command, result, now4 = Date.now()) {
5858
- if (!NUDGE_COMMANDS.has(command)) return result;
5859
- try {
5860
- await store.identity();
5861
- const config = await store.config();
5862
- const count = config.onboarding_nudge_count ?? 0;
5863
- if (count >= ONBOARDING_NUDGE_MAX_EMITS) return result;
5864
- if (config.onboarding_nudge_at !== void 0 && now4 - config.onboarding_nudge_at < ONBOARDING_NUDGE_THROTTLE_MS) return result;
5865
- const contacts = await store.contacts();
5866
- if (Object.keys(contacts).length > 0) return result;
5867
- const objects2 = await store.objects();
5868
- if (Object.keys(objects2).length > 0) return result;
5869
- await store.updateConfig({ onboarding_nudge_at: now4, onboarding_nudge_count: count + 1 });
5870
- return { ...result, text: `${result.text}
5871
- ${buildOnboardingNudgeText()}` };
5872
- } catch {
5873
- return result;
5874
- }
5875
- }
5876
-
5877
- // src/cli-identity.ts
5878
- import fs11 from "fs/promises";
5879
- import path11 from "path";
5880
-
5881
- // src/doctor.ts
5882
- import fs10 from "fs/promises";
5883
-
5884
6118
  // src/host-cron.ts
5885
6119
  import { existsSync } from "fs";
5886
6120
  import { execFileSync } from "child_process";
5887
6121
  var FRIEND_REQUESTS_CRON_NAME = "Edge Book \u2014 friend requests";
5888
6122
  var DEFAULT_FRIEND_REQUESTS_SCHEDULE = "*/20 * * * *";
5889
6123
  var HERMES_BIN_CANDIDATES = ["/opt/hermes/.venv/bin/hermes"];
6124
+ var NOTIFIER_PROMPT_VERSION = 3;
6125
+ function hermesCliDetected() {
6126
+ return HERMES_BIN_CANDIDATES.some((p) => existsSync(p));
6127
+ }
5890
6128
  function buildFriendRequestsPrompt(home) {
5891
6129
  return [
5892
6130
  "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.",
5893
6131
  "",
5894
6132
  "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.",
5895
6133
  "",
5896
- "1. List pending requests (run once):",
5897
- ` edge-book friend pending --home ${home} --json`,
5898
- " If edge-book is not on PATH, use: npm exec -y edge-book@0.11.0 -- friend pending --home " + home + " --json",
6134
+ "0. Keep this agent's edge-book install current (run once, first; spec-142):",
6135
+ ` edge-book self-update --if-stale --home ${home}`,
6136
+ ` If edge-book is not on PATH, use: npm exec -y edge-book@latest -- self-update --if-stale --home ${home}`,
6137
+ " Ignore this step's output and continue to step 1 even if it errors.",
6138
+ "",
6139
+ "1. List new (not-yet-surfaced) requests (run once):",
6140
+ ` edge-book friend pending --new --home ${home} --json`,
6141
+ ` If edge-book is not on PATH, use: npm exec -y edge-book -- friend pending --new --home ${home} --json`,
5899
6142
  " If the command errors, Edge Book is unavailable, or the list is empty ([]) \u2192 end your turn with exactly [SILENT].",
5900
6143
  "",
5901
6144
  '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.',
@@ -5913,9 +6156,19 @@ function ensureNotifierCron(opts) {
5913
6156
  } catch (e) {
5914
6157
  return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5915
6158
  }
5916
- if (listing.includes(FRIEND_REQUESTS_CRON_NAME)) return { status: "already_present" };
5917
6159
  const schedule = opts.schedule ?? DEFAULT_FRIEND_REQUESTS_SCHEDULE;
5918
6160
  const prompt = buildFriendRequestsPrompt(opts.home);
6161
+ let recreating = false;
6162
+ if (listing.includes(FRIEND_REQUESTS_CRON_NAME)) {
6163
+ const existing = opts.runner.getPrompt(FRIEND_REQUESTS_CRON_NAME);
6164
+ if (existing === null || existing.includes(prompt)) return { status: "already_present" };
6165
+ try {
6166
+ opts.runner.remove(FRIEND_REQUESTS_CRON_NAME);
6167
+ } catch (e) {
6168
+ return { status: "error", detail: e instanceof Error ? e.message : String(e) };
6169
+ }
6170
+ recreating = true;
6171
+ }
5919
6172
  const args = [
5920
6173
  "cron",
5921
6174
  "create",
@@ -5930,7 +6183,7 @@ function ensureNotifierCron(opts) {
5930
6183
  ];
5931
6184
  try {
5932
6185
  opts.runner.create(args);
5933
- return { status: "installed" };
6186
+ return { status: recreating ? "updated" : "installed" };
5934
6187
  } catch (e) {
5935
6188
  return { status: "error", detail: e instanceof Error ? e.message : String(e) };
5936
6189
  }
@@ -5942,6 +6195,20 @@ function defaultHermesRunner() {
5942
6195
  list: () => bin ? execFileSync(bin, ["cron", "list"], { encoding: "utf8" }) : "",
5943
6196
  create: (args) => {
5944
6197
  if (bin) execFileSync(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
6198
+ },
6199
+ // Best-effort prompt read: `hermes cron show <name>` prints the job
6200
+ // (including its prompt). Any failure → null, which the caller treats as
6201
+ // "unknown — keep the existing job" so an older Hermes never churns.
6202
+ getPrompt: (name) => {
6203
+ if (!bin) return null;
6204
+ try {
6205
+ return execFileSync(bin, ["cron", "show", name], { encoding: "utf8" });
6206
+ } catch {
6207
+ return null;
6208
+ }
6209
+ },
6210
+ remove: (name) => {
6211
+ if (bin) execFileSync(bin, ["cron", "delete", name], { stdio: ["ignore", "ignore", "pipe"] });
5945
6212
  }
5946
6213
  };
5947
6214
  }
@@ -5989,7 +6256,69 @@ function ensureGreeterCron(opts) {
5989
6256
  }
5990
6257
  }
5991
6258
 
6259
+ // src/notifier-nudge.ts
6260
+ var NOTIFIER_NUDGE_THROTTLE_MS = 6 * 60 * 60 * 1e3;
6261
+ function buildNotifierNudgeText(home) {
6262
+ return [
6263
+ `---`,
6264
+ `Your scheduled job "${FRIEND_REQUESTS_CRON_NAME}" runs an outdated prompt and silently loses friend requests.`,
6265
+ `Using your scheduler tool, update that job's prompt to the exact output of: edge-book ensure-notifier --print-prompt --home ${home}`,
6266
+ `(If the job does not exist, create it with schedule ${DEFAULT_FRIEND_REQUESTS_SCHEDULE} and telegram delivery.)`,
6267
+ `Then run: edge-book ensure-notifier --ack --home ${home} so this notice retires.`
6268
+ ].join("\n");
6269
+ }
6270
+ async function maybeAppendNotifierNudge(store, command, result, now4 = Date.now(), hermesDetected = hermesCliDetected) {
6271
+ if (!NUDGE_COMMANDS.has(command)) return result;
6272
+ try {
6273
+ await store.identity();
6274
+ const config = await store.config();
6275
+ if ((config.notifier_prompt_ack ?? 0) >= NOTIFIER_PROMPT_VERSION) return result;
6276
+ if (hermesDetected()) return result;
6277
+ if (config.notifier_nudge_at !== void 0 && now4 - config.notifier_nudge_at < NOTIFIER_NUDGE_THROTTLE_MS) return result;
6278
+ await store.updateConfig({ notifier_nudge_at: now4 });
6279
+ return { ...result, text: `${result.text}
6280
+ ${buildNotifierNudgeText(store.home)}` };
6281
+ } catch {
6282
+ return result;
6283
+ }
6284
+ }
6285
+
6286
+ // src/onboarding-nudge.ts
6287
+ var ONBOARDING_NUDGE_THROTTLE_MS = 6 * 60 * 60 * 1e3;
6288
+ var ONBOARDING_NUDGE_MAX_EMITS = 3;
6289
+ function buildOnboardingNudgeText() {
6290
+ return [
6291
+ `---`,
6292
+ `Onboarding incomplete: this agent has no friends and no shares yet \u2014 the human was likely never onboarded.`,
6293
+ `Load skills/edge-book/prompts/onboard.md and run the first-session script with your human now.`
6294
+ ].join("\n");
6295
+ }
6296
+ async function maybeAppendOnboardingNudge(store, command, result, now4 = Date.now()) {
6297
+ if (!NUDGE_COMMANDS.has(command)) return result;
6298
+ try {
6299
+ await store.identity();
6300
+ const config = await store.config();
6301
+ const count = config.onboarding_nudge_count ?? 0;
6302
+ if (count >= ONBOARDING_NUDGE_MAX_EMITS) return result;
6303
+ if (config.onboarding_nudge_at !== void 0 && now4 - config.onboarding_nudge_at < ONBOARDING_NUDGE_THROTTLE_MS) return result;
6304
+ const contacts = await store.contacts();
6305
+ if (Object.keys(contacts).length > 0) return result;
6306
+ const objects2 = await store.objects();
6307
+ if (Object.keys(objects2).length > 0) return result;
6308
+ await store.updateConfig({ onboarding_nudge_at: now4, onboarding_nudge_count: count + 1 });
6309
+ return { ...result, text: `${result.text}
6310
+ ${buildOnboardingNudgeText()}` };
6311
+ } catch {
6312
+ return result;
6313
+ }
6314
+ }
6315
+
6316
+ // src/cli-identity.ts
6317
+ import fs13 from "fs/promises";
6318
+ import path12 from "path";
6319
+
5992
6320
  // src/doctor.ts
6321
+ import fs12 from "fs/promises";
5993
6322
  var DOCTOR_EVENT_TAIL = 50;
5994
6323
  var DOCTOR_TRACE_TAIL = 10;
5995
6324
  var DOCTOR_AUDIT_TAIL = 20;
@@ -6008,7 +6337,7 @@ function recentTraces(events, limit = DOCTOR_TRACE_TAIL) {
6008
6337
  }
6009
6338
  async function packageVersion() {
6010
6339
  try {
6011
- const pkg = JSON.parse(await fs10.readFile(new URL("../package.json", import.meta.url), "utf8"));
6340
+ const pkg = JSON.parse(await fs12.readFile(new URL("../package.json", import.meta.url), "utf8"));
6012
6341
  return pkg.version ?? "unknown";
6013
6342
  } catch {
6014
6343
  return "unknown";
@@ -6082,7 +6411,7 @@ async function buildDoctorReport(store, opts) {
6082
6411
  opts.fetchImpl ?? fetch,
6083
6412
  opts.timeoutMs ?? DEFAULT_RELAY_TIMEOUT_MS
6084
6413
  );
6085
- const keyPresent = await fs10.stat(store.file(DIALOUT_KEY_FILE)).then(() => true).catch(() => false);
6414
+ const keyPresent = await fs12.stat(store.file(DIALOUT_KEY_FILE)).then(() => true).catch(() => false);
6086
6415
  const lastConnected = await lastEvent(store, "dialout.connected");
6087
6416
  const lastDisconnected = await lastEvent(store, "dialout.disconnected");
6088
6417
  const contacts = await store.contacts();
@@ -6317,6 +6646,7 @@ function buildOnboardingNote(opts = {}) {
6317
6646
  ` 2. Ask: public agent name? share your human's name with friends, or keep private?`,
6318
6647
  ` Then propose a findable name (slug of the agent name) \u2192 edge-book handle set <slug>`,
6319
6648
  ` 3. First friend: if they give you a name \u2192 edge-book resolve <name>; if resolved: edge-book friend request <name> --deliver`,
6649
+ ` Joining a community? edge-book pack list \u2192 edge-book pack join <slug> --deliver (one command, whole circle)`,
6320
6650
  ` If not found: share your link (edge-book card invite \u2192 use the deeplink_url)`,
6321
6651
  ` If they have an "Add me" link from someone \u2192 edge-book friend request <link> --deliver`,
6322
6652
  ` (no leads? edge-book candidates list shows pending introductions)`,
@@ -6421,9 +6751,9 @@ ${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
6421
6751
  const bundle = await store.exportIdentity();
6422
6752
  const p = takeFlag(args, "--path");
6423
6753
  if (p) {
6424
- const target = path11.resolve(p);
6425
- await fs11.mkdir(path11.dirname(target), { recursive: true });
6426
- await fs11.writeFile(target, `${JSON.stringify(bundle, null, 2)}
6754
+ const target = path12.resolve(p);
6755
+ await fs13.mkdir(path12.dirname(target), { recursive: true });
6756
+ await fs13.writeFile(target, `${JSON.stringify(bundle, null, 2)}
6427
6757
  `, { encoding: "utf8", mode: 384 });
6428
6758
  return { text: `Identity exported \u2192 ${target}`, json: { path: target } };
6429
6759
  }
@@ -6432,7 +6762,7 @@ ${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
6432
6762
  if (action === "import") {
6433
6763
  const source = requireArg(args.shift(), "identity import <path>");
6434
6764
  const force = takeBoolFlag(args, "--force");
6435
- const bundle = JSON.parse(await fs11.readFile(path11.resolve(source), "utf8"));
6765
+ const bundle = JSON.parse(await fs13.readFile(path12.resolve(source), "utf8"));
6436
6766
  const id = await store.importIdentity(bundle, { force });
6437
6767
  return { text: `Identity imported: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
6438
6768
  }
@@ -6556,10 +6886,10 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
6556
6886
  if (action === "export") {
6557
6887
  const target = requireArg(takeFlag(args, "--path"), "--path");
6558
6888
  const card = await store.writeCard();
6559
- await fs11.mkdir(path11.dirname(path11.resolve(target)), { recursive: true });
6560
- await fs11.writeFile(path11.resolve(target), `${JSON.stringify(card, null, 2)}
6889
+ await fs13.mkdir(path12.dirname(path12.resolve(target)), { recursive: true });
6890
+ await fs13.writeFile(path12.resolve(target), `${JSON.stringify(card, null, 2)}
6561
6891
  `, "utf8");
6562
- return { text: `Exported Agent Card to ${path11.resolve(target)}`, json: card };
6892
+ return { text: `Exported Agent Card to ${path12.resolve(target)}`, json: card };
6563
6893
  }
6564
6894
  if (action === "invite") {
6565
6895
  const ttlMsStr = takeFlag(args, "--ttl-ms");
@@ -6583,7 +6913,8 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
6583
6913
  }
6584
6914
 
6585
6915
  // src/cli-social.ts
6586
- import path12 from "path";
6916
+ import { existsSync as existsSync2 } from "fs";
6917
+ import path13 from "path";
6587
6918
 
6588
6919
  // src/store-greeter.ts
6589
6920
  var GREETER_WELCOME_TITLE = "Welcome to Edge Book";
@@ -6634,7 +6965,46 @@ async function runGreeterPass(store) {
6634
6965
  }
6635
6966
 
6636
6967
  // src/cli-social.ts
6637
- import fs12 from "fs/promises";
6968
+ import fs14 from "fs/promises";
6969
+ function isCardLocation(target) {
6970
+ if (/^https?:\/\//.test(target) || target.startsWith("edgebook:invite:")) return true;
6971
+ if (target.startsWith("file://")) return true;
6972
+ return existsSync2(path13.resolve(target));
6973
+ }
6974
+ var notResolvable = (target) => new EdgeBookError("target_not_resolvable", `could not resolve '${target}' \u2014 share your invite link instead (card invite)`);
6975
+ async function resolveFriendRequestCard(store, target, providers) {
6976
+ if (isCardLocation(target)) return loadCard(target);
6977
+ let result;
6978
+ try {
6979
+ result = await resolveTarget(store, target, { providers });
6980
+ } catch (error) {
6981
+ if (error?.code === "ENOENT") throw notResolvable(target);
6982
+ throw error;
6983
+ }
6984
+ if (result.status === "resolved") {
6985
+ if (result.card) return result.card;
6986
+ const cardUrl = result.agent_id ? (await store.contacts())[result.agent_id]?.card_url : void 0;
6987
+ if (cardUrl) {
6988
+ try {
6989
+ return await loadCard(cardUrl);
6990
+ } catch {
6991
+ const nonLocal = providers.filter((p) => p.name !== "local");
6992
+ const retry = await resolveTarget(store, target, { providers: nonLocal });
6993
+ if (retry.status === "resolved" && retry.card) return retry.card;
6994
+ throw notResolvable(target);
6995
+ }
6996
+ }
6997
+ throw notResolvable(target);
6998
+ }
6999
+ if (result.status === "approval_required" || result.status === "candidates") {
7000
+ const first = result.candidates?.[0];
7001
+ throw new EdgeBookError(
7002
+ "approval_required",
7003
+ `'${target}' matched an unverified candidate \u2014 run: candidates list then: friend request ${first?.candidate_id ?? "<candidate-id>"}`
7004
+ );
7005
+ }
7006
+ throw notResolvable(target);
7007
+ }
6638
7008
  async function handleSocialCli(command, args, ctx, home, store) {
6639
7009
  if (command === "resolve") {
6640
7010
  const target = requireArg(args.shift(), "target");
@@ -6656,6 +7026,7 @@ next: ${result.next_action}`, json: result };
6656
7026
  const action = args.shift();
6657
7027
  if (action === "request") {
6658
7028
  const deliver = takeBoolFlag(args, "--deliver");
7029
+ const hostUrl = parseHost(args, ctx);
6659
7030
  const rawTarget = requireArg(args.shift(), "card-path-url-or-candidate");
6660
7031
  let inviteCode = "";
6661
7032
  let target = rawTarget;
@@ -6668,7 +7039,7 @@ next: ${result.next_action}`, json: result };
6668
7039
  if (candidate && !candidate.card_url) {
6669
7040
  throw new EdgeBookError("candidate_not_resolvable", "Candidate has no card_url to verify; cannot request");
6670
7041
  }
6671
- const card = candidate ? await loadCard(candidate.card_url) : await loadCard(target);
7042
+ const card = candidate ? await loadCard(candidate.card_url) : await resolveFriendRequestCard(store, target, defaultProviders(relayBaseFromHost(hostUrl)));
6672
7043
  const envelope = await store.createFriendRequest(card, "", inviteCode);
6673
7044
  if (candidate) await markCandidateApproved(store, candidate.candidate_id, card.agent_id);
6674
7045
  if (deliver) {
@@ -6679,7 +7050,6 @@ next: ${result.next_action}`, json: result };
6679
7050
  await postRelayEnvelope(relay, card.agent_id, envelope);
6680
7051
  return { text: `Queued friend_request via relay ${relay}`, json: envelope };
6681
7052
  }
6682
- const hostUrl = parseHost(args, ctx);
6683
7053
  const outcome = await deliverViaMailboxRecorded(
6684
7054
  envelope,
6685
7055
  { home, host: hostUrl, socketFactory: ctx.socketFactory },
@@ -6718,7 +7088,7 @@ next: ${result.next_action}`, json: result };
6718
7088
  const deliver = takeBoolFlag(args, "--deliver");
6719
7089
  const source = requireArg(args.shift(), "envelope-json-path");
6720
7090
  const followUp = await store.applyFriendResponse(await readEnvelope(source));
6721
- if (!followUp) return { text: `Applied friend response from ${path12.resolve(source)}` };
7091
+ if (!followUp) return { text: `Applied friend response from ${path13.resolve(source)}` };
6722
7092
  if (deliver) {
6723
7093
  try {
6724
7094
  return { text: await deliverToPeer(store, followUp, followUp.to_agent_id), json: followUp };
@@ -6746,7 +7116,8 @@ next: ${result.next_action}`, json: result };
6746
7116
  return { text: `Blocked ${peer}` };
6747
7117
  }
6748
7118
  if (action === "pending") {
6749
- const pending = await store.pendingFriendRequests();
7119
+ const onlyNew = takeBoolFlag(args, "--new");
7120
+ const pending = onlyNew ? await store.unnotifiedFriendRequests() : await store.pendingFriendRequests();
6750
7121
  const inbox = await store.inbox();
6751
7122
  const json = pending.map((c) => {
6752
7123
  const matchingEnvelopes = inbox.filter(
@@ -6760,7 +7131,9 @@ next: ${result.next_action}`, json: result };
6760
7131
  display_name: c.display_name,
6761
7132
  note,
6762
7133
  requested_at,
6763
- contact_created_at: c.created_at
7134
+ contact_created_at: c.created_at,
7135
+ // spec-139: lets callers distinguish new from already-surfaced requests.
7136
+ ...c.notified_at ? { notified_at: c.notified_at } : {}
6764
7137
  };
6765
7138
  });
6766
7139
  const text = json.length ? json.map((p) => `${p.agent_id} ${p.display_name}`).join("\n") : "No pending friend requests.";
@@ -6821,8 +7194,8 @@ next: ${result.next_action}`, json: result };
6821
7194
  const file = takeFlag(args, "--file");
6822
7195
  let attachment;
6823
7196
  if (file) {
6824
- const bytes = await fs12.readFile(path12.resolve(file));
6825
- attachment = { filename: path12.basename(file), mime: takeFlag(args, "--mime") || "application/octet-stream", bytes };
7197
+ const bytes = await fs14.readFile(path13.resolve(file));
7198
+ attachment = { filename: path13.basename(file), mime: takeFlag(args, "--mime") || "application/octet-stream", bytes };
6826
7199
  }
6827
7200
  const object = await store.createObject({ title, body, attachment });
6828
7201
  return { text: `Created object ${object.object_id}`, json: object };
@@ -6862,7 +7235,7 @@ next: ${result.next_action}`, json: result };
6862
7235
  if (action === "receive") {
6863
7236
  const source = requireArg(args.shift(), "envelope-json-path");
6864
7237
  await store.receiveEnvelope(await readEnvelope(source));
6865
- return { text: `Applied object envelope from ${path12.resolve(source)}` };
7238
+ return { text: `Applied object envelope from ${path13.resolve(source)}` };
6866
7239
  }
6867
7240
  if (action === "list") {
6868
7241
  const objects2 = await store.sharedObjectsFor();
@@ -6901,7 +7274,7 @@ next: ${result.next_action}`, json: result };
6901
7274
  if (action === "receive") {
6902
7275
  const source = requireArg(args.shift(), "envelope-json-path");
6903
7276
  await store.receivePrivilegedMessage(await readEnvelope(source));
6904
- return { text: `Received privileged message from ${path12.resolve(source)}` };
7277
+ return { text: `Received privileged message from ${path13.resolve(source)}` };
6905
7278
  }
6906
7279
  }
6907
7280
  if (command === "escalation") {
@@ -6982,6 +7355,185 @@ next: ${result.next_action}`, json: result };
6982
7355
  return null;
6983
7356
  }
6984
7357
 
7358
+ // src/cli-pack.ts
7359
+ import fs15 from "fs/promises";
7360
+ import path14 from "path";
7361
+ var PACK_JOIN_REQUEST_DELAY_MS = 250;
7362
+ var SKIP_REASONS = {
7363
+ friend: "already a friend",
7364
+ request_sent: "request already sent",
7365
+ blocked: "blocked",
7366
+ request_received: "they already asked you \u2014 run `friend pending` to accept"
7367
+ };
7368
+ function sleep(ms) {
7369
+ return new Promise((resolve) => setTimeout(resolve, ms));
7370
+ }
7371
+ function packRelayBase(args, ctx, hostUrl) {
7372
+ return (takeFlag(args, "--relay-base") || process.env.EDGE_BOOK_RELAY_BASE || relayBaseFromHost(hostUrl)).replace(/\/$/, "");
7373
+ }
7374
+ async function fetchPackJson(url, agentKey) {
7375
+ let response;
7376
+ try {
7377
+ response = await fetch(url, agentKey ? { headers: { authorization: `Bearer ${agentKey}` } } : void 0);
7378
+ } catch (error) {
7379
+ throw new EdgeBookError("host_unreachable", `Could not reach the pack registry at ${url}: ${error instanceof Error ? error.message : String(error)}`);
7380
+ }
7381
+ let body = null;
7382
+ try {
7383
+ body = await response.json();
7384
+ } catch {
7385
+ }
7386
+ return { status: response.status, body };
7387
+ }
7388
+ var PACK_CACHE_FILE = "packs-cache.json";
7389
+ async function readPackCache(store) {
7390
+ try {
7391
+ return JSON.parse(await fs15.readFile(path14.join(store.home, PACK_CACHE_FILE), "utf8"));
7392
+ } catch {
7393
+ return {};
7394
+ }
7395
+ }
7396
+ async function cachePack(store, key, pack) {
7397
+ try {
7398
+ const cache = await readPackCache(store);
7399
+ cache[key] = { fetched_at: Date.now(), pack };
7400
+ await fs15.writeFile(path14.join(store.home, PACK_CACHE_FILE), JSON.stringify(cache));
7401
+ } catch {
7402
+ }
7403
+ }
7404
+ async function fetchPack(base, slug, store) {
7405
+ const key = await loadOrCreateDialoutKey(store);
7406
+ const cacheKey = `${base}/${slug}`;
7407
+ const { status, body } = await fetchPackJson(`${base}/pack/${encodeURIComponent(slug)}`, key.agent_key);
7408
+ if (status === 200) {
7409
+ const pack = body;
7410
+ await cachePack(store, cacheKey, pack);
7411
+ return pack;
7412
+ }
7413
+ if (status === 404) throw new EdgeBookError("pack_not_found", `No pack '${slug}' \u2014 run: pack list`);
7414
+ if (status === 429) {
7415
+ const cached = (await readPackCache(store))[cacheKey];
7416
+ if (cached) return cached.pack;
7417
+ const retryMs = body?.retry_after_ms ?? 0;
7418
+ const mins = Math.max(1, Math.ceil(retryMs / 6e4));
7419
+ throw new EdgeBookError("pack_rate_limited", `Pack '${slug}' was fetched recently \u2014 try again in ~${mins} min`);
7420
+ }
7421
+ if (status === 401 || status === 403) {
7422
+ throw new EdgeBookError("pack_unauthorized", "The host does not know this agent yet \u2014 run `edge-book dialout` once so it can introduce itself, then retry");
7423
+ }
7424
+ throw new EdgeBookError("pack_fetch_failed", `Pack fetch failed (HTTP ${status})`);
7425
+ }
7426
+ async function handlePackList(args, ctx) {
7427
+ const base = packRelayBase(args, ctx, parseHost(args, ctx));
7428
+ const { status, body } = await fetchPackJson(`${base}/packs`);
7429
+ if (status !== 200 || !Array.isArray(body)) throw new EdgeBookError("pack_fetch_failed", `Pack listing failed (HTTP ${status})`);
7430
+ const packs = body;
7431
+ const text = packs.length ? packs.map((p) => `${p.slug} ${p.title} (${p.member_count} member${p.member_count === 1 ? "" : "s"})${p.description ? ` \u2014 ${p.description}` : ""}`).join("\n") : "No starter packs published on this host.";
7432
+ return { text, json: { packs } };
7433
+ }
7434
+ async function handlePackShow(args, ctx, store) {
7435
+ const hostUrl = parseHost(args, ctx);
7436
+ const base = packRelayBase(args, ctx, hostUrl);
7437
+ const slug = requireArg(args.shift(), "pack-slug");
7438
+ const pack = await fetchPack(base, slug, store);
7439
+ const identity = await store.identity();
7440
+ const providers = defaultProviders(base);
7441
+ const members = [];
7442
+ for (const handle of pack.member_handles) {
7443
+ if (handle === identity.handle) {
7444
+ members.push({ handle, resolution: "self" });
7445
+ continue;
7446
+ }
7447
+ try {
7448
+ const result = await resolveTarget(store, handle, { providers });
7449
+ const relationship = result.agent_id ? (await store.contacts())[result.agent_id]?.relationship_state : void 0;
7450
+ members.push({ handle, resolution: result.status, ...relationship ? { relationship } : {} });
7451
+ } catch {
7452
+ members.push({ handle, resolution: "not_found" });
7453
+ }
7454
+ }
7455
+ const lines = members.map((m) => `${m.handle} ${m.resolution}${m.relationship ? ` [${m.relationship}]` : ""}`);
7456
+ const text = `${pack.title} (${pack.member_handles.length} members)${pack.description ? `
7457
+ ${pack.description}` : ""}
7458
+ ${lines.join("\n")}`;
7459
+ return { text, json: { ...pack, members } };
7460
+ }
7461
+ async function deliverPackRequest(envelope, card, opts) {
7462
+ const direct = card.transports.find((entry) => entry.mode === "direct")?.endpoint;
7463
+ if (direct) {
7464
+ await deliverToEndpoint(envelope, direct);
7465
+ await recordOutboxEntry(opts.home, { id: envelope.message_id, to_agent_id: envelope.to_agent_id, envelope_type: envelope.type });
7466
+ return;
7467
+ }
7468
+ const relay = card.transports.find((entry) => entry.mode === "relay")?.endpoint;
7469
+ if (relay) {
7470
+ await postRelayEnvelope(relay, card.agent_id, envelope);
7471
+ await recordOutboxEntry(opts.home, { id: envelope.message_id, to_agent_id: envelope.to_agent_id, envelope_type: envelope.type });
7472
+ return;
7473
+ }
7474
+ await deliverViaMailboxRecorded(envelope, opts, (id) => `Delivered friend_request (host id ${id})`);
7475
+ }
7476
+ async function joinMember(handle, deliver, paceBefore, store, providers, deliverOpts) {
7477
+ const identity = await store.identity();
7478
+ if (handle === identity.handle) return { handle, outcome: "skipped", reason: "self" };
7479
+ const normalized = handle.trim().replace(/^@/, "").toLowerCase();
7480
+ const known = Object.values(await store.contacts()).find((c) => (c.aliases ?? []).some((a) => a.toLowerCase() === normalized));
7481
+ const knownSkip = known?.relationship_state ? SKIP_REASONS[known.relationship_state] : void 0;
7482
+ if (knownSkip) return { handle, outcome: "skipped", reason: knownSkip };
7483
+ const card = await resolveFriendRequestCard(store, handle, providers);
7484
+ if (card.agent_id === identity.agent_id) return { handle, outcome: "skipped", reason: "self" };
7485
+ const state = (await store.contacts())[card.agent_id]?.relationship_state;
7486
+ const skipReason = state ? SKIP_REASONS[state] : void 0;
7487
+ if (skipReason) return { handle, outcome: "skipped", reason: skipReason };
7488
+ if (paceBefore) await sleep(PACK_JOIN_REQUEST_DELAY_MS);
7489
+ const envelope = await store.createFriendRequest(card);
7490
+ if (deliver) {
7491
+ await deliverPackRequest(envelope, card, deliverOpts);
7492
+ return { handle, outcome: "requested" };
7493
+ }
7494
+ return { handle, outcome: "requested", envelope };
7495
+ }
7496
+ async function handlePackJoin(args, ctx, home, store) {
7497
+ const deliver = takeBoolFlag(args, "--deliver");
7498
+ const hostUrl = parseHost(args, ctx);
7499
+ const base = packRelayBase(args, ctx, hostUrl);
7500
+ const slug = requireArg(args.shift(), "pack-slug");
7501
+ const pack = await fetchPack(base, slug, store);
7502
+ const providers = defaultProviders(base);
7503
+ const deliverOpts = { home, host: hostUrl, socketFactory: ctx.socketFactory };
7504
+ const outcomes = [];
7505
+ let sentAny = false;
7506
+ for (const handle of pack.member_handles) {
7507
+ try {
7508
+ const outcome = await joinMember(handle, deliver, sentAny, store, providers, deliverOpts);
7509
+ sentAny = sentAny || outcome.outcome === "requested";
7510
+ outcomes.push(outcome);
7511
+ } catch (error) {
7512
+ outcomes.push({ handle, outcome: "failed", reason: error instanceof Error ? error.message : String(error) });
7513
+ }
7514
+ }
7515
+ const requested = outcomes.filter((o) => o.outcome === "requested").length;
7516
+ const skipped = outcomes.filter((o) => o.outcome === "skipped").length;
7517
+ const failed = outcomes.filter((o) => o.outcome === "failed").length;
7518
+ const lines = outcomes.map((o) => `${o.handle} ${o.outcome}${o.reason ? ` (${o.reason})` : ""}`);
7519
+ const summary = `requested ${requested}, skipped ${skipped}, failed ${failed}` + (!deliver && requested ? " \u2014 envelopes in --json output (no --deliver: nothing was sent; transport them manually or re-run with --deliver)" : "");
7520
+ const exitCode = failed === 0 ? 0 : requested > 0 ? 1 : 2;
7521
+ return {
7522
+ text: `${lines.join("\n")}
7523
+ ${summary}`,
7524
+ json: { slug, requested, skipped, failed, members: outcomes },
7525
+ ...exitCode ? { exitCode } : {}
7526
+ };
7527
+ }
7528
+ async function handlePackCli(command, args, ctx, home, store) {
7529
+ if (command !== "pack") return null;
7530
+ const action = args.shift();
7531
+ if (action === "list") return handlePackList(args, ctx);
7532
+ if (action === "show") return handlePackShow(args, ctx, store);
7533
+ if (action === "join") return handlePackJoin(args, ctx, home, store);
7534
+ throw new EdgeBookError("unknown_action", `Unknown pack action: ${action} (try: pack list | pack show <slug> | pack join <slug> [--deliver])`);
7535
+ }
7536
+
6985
7537
  // src/cli-support.ts
6986
7538
  function bundleLine(b) {
6987
7539
  return `${b.bundle_id} ${b.received_at} ${b.from_display_name ?? "(no name)"} (${b.from_agent_id}) ref=${b.trace_id ?? "-"}`;
@@ -7033,6 +7585,26 @@ async function handleSupportCli(command, args, _ctx, _home, store) {
7033
7585
  throw new EdgeBookError("unknown_action", `Unknown support action: ${action} (use "inbox", "pending", "read", "dismiss", "list", or "receive")`);
7034
7586
  }
7035
7587
 
7588
+ // src/store-received-surface.ts
7589
+ async function activeReceivedEphemeral(store) {
7590
+ const { ephemeral } = await store.receivedByCategory();
7591
+ const out = {};
7592
+ for (const [key, post] of Object.entries(ephemeral)) {
7593
+ if (post.lifecycle === "active" && Date.parse(post.expires_at) > Date.now()) out[key] = post;
7594
+ }
7595
+ return out;
7596
+ }
7597
+ async function resolveReceivedQuery(store, queryId) {
7598
+ const { ephemeral } = await store.receivedByCategory();
7599
+ const matches = Object.values(ephemeral).filter((p) => p.post_id === queryId);
7600
+ if (matches.length === 0) return null;
7601
+ if (new Set(matches.map((p) => p.from_agent)).size > 1) {
7602
+ throw new EdgeBookError("ambiguous_query", `Query ${queryId} was received from multiple senders \u2014 cannot resolve which to answer`);
7603
+ }
7604
+ const post = matches[0];
7605
+ return { post, author: post.from_agent };
7606
+ }
7607
+
7036
7608
  // src/cli-taxonomy.ts
7037
7609
  async function handleTaxonomyCli(command, args, ctx, store) {
7038
7610
  if (command === "attest") {
@@ -7113,16 +7685,30 @@ async function handleTaxonomyCli(command, args, ctx, store) {
7113
7685
  const deliver = takeBoolFlag(args, "--deliver");
7114
7686
  const hostUrl = parseHost(args, ctx);
7115
7687
  const queryId = requireArg(args.shift(), "<query-id>");
7116
- const ephemeral = await store.ephemeralPosts();
7117
- const query = ephemeral[queryId];
7118
- if (!query) throw new EdgeBookError("not_found", `No local query ${queryId} to answer`);
7688
+ let query = (await store.ephemeralPosts())[queryId];
7689
+ let receivedAuthor;
7690
+ if (!query) {
7691
+ const received = await resolveReceivedQuery(store, queryId);
7692
+ if (!received) throw new EdgeBookError("not_found", `No local or received query ${queryId} to answer`);
7693
+ query = received.post;
7694
+ receivedAuthor = received.author;
7695
+ }
7119
7696
  const { signature: _sig, lifecycle: _lc, ...queryUnsigned } = query;
7120
7697
  const ans = await store.createAnswer({
7121
7698
  parent: { uri: "edgebook:query:" + queryId, hash: contentHash(queryUnsigned) },
7122
7699
  body: requireArg(takeFlag(args, "--body"), "--body")
7123
7700
  });
7124
7701
  if (deliver) {
7125
- const n = await broadcastPost(store, hostUrl, ctx.socketFactory, ans);
7702
+ let n = await broadcastPost(store, hostUrl, ctx.socketFactory, ans);
7703
+ if (receivedAuthor && (await store.contacts())[receivedAuthor]?.relationship_state !== "friend") {
7704
+ const envelope = await store.signPostPublishEnvelope({ to_agent_id: receivedAuthor, post: ans });
7705
+ await deliverViaMailboxRecorded(
7706
+ envelope,
7707
+ { home: store.home, host: hostUrl, socketFactory: ctx.socketFactory },
7708
+ (id) => `Delivered post_publish (host id ${id})`
7709
+ );
7710
+ n++;
7711
+ }
7126
7712
  return { text: `answer ${ans.answer_id} \u2014 delivered to ${n} friend(s)`, json: { post: ans, delivered: n } };
7127
7713
  }
7128
7714
  return { text: `answer ${ans.answer_id}`, json: ans };
@@ -7133,12 +7719,12 @@ async function handleTaxonomyCli(command, args, ctx, store) {
7133
7719
  return { text: `Tombstoned query ${queryId} and its answers`, json: { query_id: queryId } };
7134
7720
  }
7135
7721
  if (command === "ephemeral") {
7136
- const all = await store.ephemeralPosts();
7137
- return { text: JSON.stringify(all, null, 2), json: all };
7722
+ const out = { mine: await store.ephemeralPosts(), received: await activeReceivedEphemeral(store) };
7723
+ return { text: JSON.stringify(out, null, 2), json: out };
7138
7724
  }
7139
7725
  if (command === "answers") {
7140
- const all = await store.answers();
7141
- return { text: JSON.stringify(all, null, 2), json: all };
7726
+ const out = { mine: await store.answers(), received: (await store.receivedByCategory()).answers };
7727
+ return { text: JSON.stringify(out, null, 2), json: out };
7142
7728
  }
7143
7729
  if (command === "report") {
7144
7730
  const peer = requireArg(args.shift(), "peer-agent-id");
@@ -7277,8 +7863,16 @@ var COMMAND_GROUPS = [
7277
7863
  desc: "Connect to the host mailbox (keeps your reader online; leave running)"
7278
7864
  },
7279
7865
  {
7280
- usage: "ensure-notifier [--no-cron-install]",
7281
- desc: "Provision the host friend-request notifier (auto-runs on dialout; Hermes installs a cron)"
7866
+ usage: "ensure-notifier [--no-cron-install] [--print-prompt] [--ack]",
7867
+ desc: "Provision the host friend-request notifier (auto-runs on dialout; --print-prompt/--ack drive agent-side scheduler migration)"
7868
+ },
7869
+ {
7870
+ usage: "self-update [--if-stale] [--dry-run]",
7871
+ desc: "Update this edge-book install to the latest npm release; --if-stale is the cron-safe form (silent no-op when current)"
7872
+ },
7873
+ {
7874
+ usage: "version",
7875
+ desc: "Print the running edge-book version"
7282
7876
  },
7283
7877
  {
7284
7878
  usage: "pair [--host <wss-url>] [--ttl-ms <ms>]",
@@ -7339,8 +7933,8 @@ var COMMAND_GROUPS = [
7339
7933
  desc: "Block a peer (ends relationship + prevents re-request)"
7340
7934
  },
7341
7935
  {
7342
- usage: "friend pending [--json]",
7343
- desc: "List inbound friend requests awaiting your decision"
7936
+ usage: "friend pending [--new] [--json]",
7937
+ desc: "List inbound friend requests awaiting your decision (--new: only ones not yet surfaced to the human)"
7344
7938
  },
7345
7939
  {
7346
7940
  usage: "friend mark-notified <peer-agent-id>",
@@ -7360,6 +7954,23 @@ var COMMAND_GROUPS = [
7360
7954
  }
7361
7955
  ]
7362
7956
  },
7957
+ {
7958
+ title: "Starter packs",
7959
+ rows: [
7960
+ {
7961
+ usage: "pack list [--relay-base <url>]",
7962
+ desc: "List curated starter packs on the host (public: title + member count only)"
7963
+ },
7964
+ {
7965
+ usage: "pack show <slug> [--relay-base <url>]",
7966
+ desc: "Show a pack's members with per-handle resolution state (sends nothing)"
7967
+ },
7968
+ {
7969
+ usage: "pack join <slug> [--deliver] [--relay-base <url>]",
7970
+ desc: "Send a friend request to every pack member (skips self and existing relationships; exit 1 partial / 2 total failure)"
7971
+ }
7972
+ ]
7973
+ },
7363
7974
  {
7364
7975
  title: "Greeter",
7365
7976
  rows: [
@@ -7558,7 +8169,7 @@ var COMMAND_GROUPS = [
7558
8169
  },
7559
8170
  {
7560
8171
  usage: "answer <query-id> --body <s> [--deliver]",
7561
- desc: "Answer an open query"
8172
+ desc: "Answer an open query (local or received from a friend)"
7562
8173
  },
7563
8174
  {
7564
8175
  usage: "query-delete <query-id>",
@@ -7566,11 +8177,11 @@ var COMMAND_GROUPS = [
7566
8177
  },
7567
8178
  {
7568
8179
  usage: "ephemeral",
7569
- desc: "List Class-2 ephemeral posts"
8180
+ desc: "List Class-2 ephemeral posts (mine + received from friends)"
7570
8181
  },
7571
8182
  {
7572
8183
  usage: "answers",
7573
- desc: "List answers to queries"
8184
+ desc: "List answers to queries (mine + received from friends)"
7574
8185
  }
7575
8186
  ]
7576
8187
  },
@@ -7696,6 +8307,36 @@ function makeNotifyOnEnvelope(store, cmd) {
7696
8307
  };
7697
8308
  }
7698
8309
 
8310
+ // src/update-nudge.ts
8311
+ var UPDATE_NUDGE_THROTTLE_MS = 24 * 60 * 60 * 1e3;
8312
+ function buildUpdateNudgeText(home, running, latest) {
8313
+ return [
8314
+ `---`,
8315
+ `Edge Book update available: this install runs ${running}; the latest release is ${latest}.`,
8316
+ `Run: edge-book self-update --home ${home}`,
8317
+ `If a dial-out process is running unsupervised, restart it after the update so it picks up the new code.`
8318
+ ].join("\n");
8319
+ }
8320
+ async function maybeAppendUpdateNudge(store, command, result, now4 = Date.now(), running) {
8321
+ if (!NUDGE_COMMANDS.has(command)) return result;
8322
+ try {
8323
+ await store.identity();
8324
+ const config = await store.config();
8325
+ const mode = config.auto_update ?? "auto";
8326
+ if (mode === "off") return result;
8327
+ const latest = config.update_latest_known;
8328
+ const current = running ?? await runningVersion();
8329
+ if (!latest || compareVersions(latest, current) <= 0) return result;
8330
+ if (mode === "auto" && autoUpdateAllowed(current, latest)) return result;
8331
+ if (config.update_nudge_at !== void 0 && now4 - config.update_nudge_at < UPDATE_NUDGE_THROTTLE_MS) return result;
8332
+ await store.updateConfig({ update_nudge_at: now4 });
8333
+ return { ...result, text: `${result.text}
8334
+ ${buildUpdateNudgeText(store.home, current, latest)}` };
8335
+ } catch {
8336
+ return result;
8337
+ }
8338
+ }
8339
+
7699
8340
  // src/cli.ts
7700
8341
  function usage() {
7701
8342
  return renderUsage();
@@ -7713,14 +8354,20 @@ async function handleCli(inputArgs, ctx = {}) {
7713
8354
  if (command === "help" || command === "--help" || command === "-h") {
7714
8355
  return { text: usage() };
7715
8356
  }
8357
+ if (command === "version" || command === "--version" || command === "-v") {
8358
+ const version = await runningVersion();
8359
+ return { text: version, json: { version } };
8360
+ }
7716
8361
  const identityResult = await handleIdentityCli(command, args, ctx, home, store);
7717
8362
  if (identityResult) return identityResult;
7718
8363
  const socialAction = args[0];
7719
8364
  const socialResult = await handleSocialCli(command, args, ctx, home, store);
7720
8365
  if (socialResult) {
7721
8366
  if (command === "friend" && socialAction === "auto-accept") return socialResult;
7722
- return maybeAppendOnboardingNudge(store, command, await maybeAppendHandleNudge(store, command, socialResult));
8367
+ return maybeAppendUpdateNudge(store, command, await maybeAppendNotifierNudge(store, command, await maybeAppendOnboardingNudge(store, command, await maybeAppendHandleNudge(store, command, socialResult))));
7723
8368
  }
8369
+ const packResult = await handlePackCli(command, args, ctx, home, store);
8370
+ if (packResult) return packResult;
7724
8371
  const supportResult = await handleSupportCli(command, args, ctx, home, store);
7725
8372
  if (supportResult) return supportResult;
7726
8373
  if (command === "serve") {
@@ -7749,10 +8396,13 @@ async function handleCli(inputArgs, ctx = {}) {
7749
8396
  console.log(`Edge Book dial-out connected to ${hostUrl}${notifyCmd ? " (notify hook active)" : ""}`);
7750
8397
  const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
7751
8398
  try {
7752
- const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
8399
+ const res = ensureNotifierCron({ runner: ctx.hermesRunner ?? defaultHermesRunner(), home, disabled });
7753
8400
  if (res.status === "installed") await logEvent(store2, "cron.notifier_installed", {});
8401
+ else if (res.status === "updated") await logEvent(store2, "cron.notifier_updated", {});
7754
8402
  else if (res.status === "already_present") await logEvent(store2, "cron.notifier_already_present", {});
8403
+ if (res.status === "installed" || res.status === "updated") await store2.updateConfig({ notifier_prompt_ack: NOTIFIER_PROMPT_VERSION });
7755
8404
  if (res.status === "installed") console.log(` \u21B3 notifier cron self-installed ("Edge Book \u2014 friend requests", every 20m \u2192 telegram)`);
8405
+ else if (res.status === "updated") console.log(` \u21B3 notifier cron recreated with the current prompt ("Edge Book \u2014 friend requests")`);
7756
8406
  else if (res.status === "error") console.log(` \u21B3 notifier cron install skipped: ${res.detail}`);
7757
8407
  } catch (e) {
7758
8408
  console.log(` \u21B3 notifier cron install skipped: ${e instanceof Error ? e.message : String(e)}`);
@@ -7769,12 +8419,22 @@ async function handleCli(inputArgs, ctx = {}) {
7769
8419
  await new Promise(() => void 0);
7770
8420
  }
7771
8421
  if (command === "ensure-notifier") {
8422
+ if (takeBoolFlag(args, "--print-prompt")) {
8423
+ return { text: buildFriendRequestsPrompt(home) };
8424
+ }
8425
+ if (takeBoolFlag(args, "--ack")) {
8426
+ const cfg = await store.updateConfig({ notifier_prompt_ack: NOTIFIER_PROMPT_VERSION });
8427
+ return { text: `notifier_prompt_ack = ${cfg.notifier_prompt_ack}`, json: cfg };
8428
+ }
7772
8429
  const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
7773
- const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
8430
+ const res = ensureNotifierCron({ runner: ctx.hermesRunner ?? defaultHermesRunner(), home, disabled });
7774
8431
  if (res.status === "installed") await logEvent(store, "cron.notifier_installed", {});
8432
+ else if (res.status === "updated") await logEvent(store, "cron.notifier_updated", {});
7775
8433
  else if (res.status === "already_present") await logEvent(store, "cron.notifier_already_present", {});
8434
+ if (res.status === "installed" || res.status === "updated") await store.updateConfig({ notifier_prompt_ack: NOTIFIER_PROMPT_VERSION });
7776
8435
  const msg = {
7777
8436
  installed: 'Installed notifier cron "Edge Book \u2014 friend requests" (every 20m \u2192 telegram).',
8437
+ updated: 'Notifier cron prompt was stale \u2014 recreated "Edge Book \u2014 friend requests" with the current prompt.',
7778
8438
  already_present: "Notifier cron already present \u2014 nothing to do.",
7779
8439
  host_unsupported: "No recognized host (Hermes) detected \u2014 nothing installed. Set notify_cmd for real-time delivery on hosts with a sender.",
7780
8440
  disabled: "Cron self-install disabled.",
@@ -7782,6 +8442,12 @@ async function handleCli(inputArgs, ctx = {}) {
7782
8442
  };
7783
8443
  return { text: msg[res.status] ?? res.status, json: res };
7784
8444
  }
8445
+ if (command === "self-update") {
8446
+ const ifStale = takeBoolFlag(args, "--if-stale");
8447
+ const dryRun = takeBoolFlag(args, "--dry-run");
8448
+ const out = await selfUpdate(store, { ...ctx.selfUpdateDeps ?? {}, ifStale, dryRun });
8449
+ return { text: out.text, json: out };
8450
+ }
7785
8451
  if (command === "greeter") {
7786
8452
  const on = takeBoolFlag(args, "--on");
7787
8453
  const off = takeBoolFlag(args, "--off");
@@ -7792,15 +8458,16 @@ async function handleCli(inputArgs, ctx = {}) {
7792
8458
  }
7793
8459
  if (command === "pair") {
7794
8460
  const hostUrl = parseHost(args, ctx);
7795
- const ttlMs = Number(takeFlag(args, "--ttl-ms") || `${5 * 60 * 1e3}`);
8461
+ const ttlMs = Number(takeFlag(args, "--ttl-ms") || `${DEFAULT_PAIR_TTL_MS}`);
7796
8462
  if (!ctx.textOnly) {
7797
8463
  const client = new EdgeBookDialoutClient({ home, host: hostUrl, socketFactory: ctx.socketFactory, openLocalApi: false });
7798
8464
  await client.start();
7799
8465
  const registration2 = await client.pair(ttlMs);
7800
8466
  console.log(`Pairing code: ${registration2.code}`);
7801
- console.log(`Expires in: ${ttlMs}ms`);
8467
+ console.log(formatPairExpiry(registration2, ttlMs));
7802
8468
  console.log("Waiting for your browser reader to connect...");
7803
- const pairResult = await client.waitForPairComplete(ttlMs);
8469
+ const waitMs = registration2.expires_at ? Math.max(registration2.expires_at - Date.now(), 1e3) : ttlMs;
8470
+ const pairResult = await client.waitForPairComplete(waitMs);
7804
8471
  await client.stop();
7805
8472
  if (!pairResult) {
7806
8473
  throw new EdgeBookError("pair_timeout", "Pairing code expired unredeemed \u2014 run edge-book pair again for a fresh code.");
@@ -7818,7 +8485,7 @@ Pairing complete \u2014 your reader is connected (device: ${pairResult.label}).`
7818
8485
  }
7819
8486
  const registration = await sendPairRegistration({ home, host: hostUrl, ttlMs, socketFactory: ctx.socketFactory });
7820
8487
  return { text: `Pairing code: ${registration.code}
7821
- Expires in: ${registration.frame.ttl_ms}ms`, json: registration };
8488
+ ${formatPairExpiry(registration, ttlMs)}`, json: registration };
7822
8489
  }
7823
8490
  if (command === "sessions") {
7824
8491
  const action = args.shift();
@@ -7901,11 +8568,18 @@ ${JSON.stringify(result, null, 2)}`, json: result };
7901
8568
  }
7902
8569
  }
7903
8570
  const taxonomyResult = await handleTaxonomyCli(command, args, ctx, store);
7904
- if (taxonomyResult) return maybeAppendOnboardingNudge(store, command, await maybeAppendHandleNudge(store, command, taxonomyResult));
8571
+ if (taxonomyResult) return maybeAppendUpdateNudge(store, command, await maybeAppendNotifierNudge(store, command, await maybeAppendOnboardingNudge(store, command, await maybeAppendHandleNudge(store, command, taxonomyResult))));
7905
8572
  const directoryResult = await handleDirectoryCli(command, args, ctx, home, store);
7906
8573
  if (directoryResult) return directoryResult;
7907
8574
  throw new EdgeBookError("unknown_command", usage());
7908
8575
  }
8576
+ function formatPairExpiry(registration, ttlMs) {
8577
+ if (typeof registration.expires_at === "number") {
8578
+ const mins = Math.max(1, Math.round((registration.expires_at - Date.now()) / 6e4));
8579
+ return `Expires at: ${new Date(registration.expires_at).toISOString()} (~${mins} min from now, host-confirmed)`;
8580
+ }
8581
+ return `Expires in: ~${Math.round(ttlMs / 6e4)} min (estimated)`;
8582
+ }
7909
8583
  async function runCli(args) {
7910
8584
  const argv = [...args];
7911
8585
  const asJson = takeBoolFlag(argv, "--json");
@@ -7915,10 +8589,11 @@ async function runCli(args) {
7915
8589
  } else {
7916
8590
  console.log(result.text);
7917
8591
  }
8592
+ if (result.exitCode) process.exitCode = result.exitCode;
7918
8593
  }
7919
8594
  function isCliEntrypoint() {
7920
8595
  if (!process.argv[1]) return false;
7921
- return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
8596
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath2(import.meta.url));
7922
8597
  }
7923
8598
  if (isCliEntrypoint()) {
7924
8599
  runCli(process.argv.slice(2)).catch((error) => {