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.
- package/README.md +12 -2
- package/dist/edge-book.js +975 -278
- 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
|
|
9
|
-
import
|
|
8
|
+
import fs9 from "fs/promises";
|
|
9
|
+
import path10 from "path";
|
|
10
10
|
|
|
11
11
|
// src/dialout.ts
|
|
12
|
-
import
|
|
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
|
|
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
|
|
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
|
|
1833
|
-
import
|
|
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 } =
|
|
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 =
|
|
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
|
|
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
|
-
...
|
|
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))
|
|
2263
|
-
|
|
2264
|
-
|
|
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
|
|
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 "
|
|
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.
|
|
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 `${
|
|
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
|
|
3118
|
-
const
|
|
3119
|
-
|
|
3120
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
5475
|
+
import fs8 from "fs/promises";
|
|
5225
5476
|
import http2 from "http";
|
|
5226
|
-
import
|
|
5477
|
+
import path8 from "path";
|
|
5227
5478
|
function relayFile(store, agentId) {
|
|
5228
|
-
return
|
|
5479
|
+
return path8.join(store, `${encodeURIComponent(agentId)}.jsonl`);
|
|
5229
5480
|
}
|
|
5230
5481
|
async function appendRelayEnvelope(store, agentId, envelope) {
|
|
5231
|
-
await
|
|
5232
|
-
await
|
|
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
|
|
5239
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5455
|
-
import
|
|
5705
|
+
import fs11 from "fs/promises";
|
|
5706
|
+
import path11 from "path";
|
|
5456
5707
|
|
|
5457
|
-
// src/
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
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
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
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
|
|
5561
|
-
|
|
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 =
|
|
5577
|
-
await
|
|
5578
|
-
await
|
|
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
|
|
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
|
|
5688
|
-
|
|
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
|
|
5700
|
-
await
|
|
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 ${
|
|
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
|
|
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
|
|
5714
|
-
return { text:
|
|
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:
|
|
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
|
|
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
|
|
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 ${
|
|
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
|
|
5962
|
-
attachment = { filename:
|
|
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 ${
|
|
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 ${
|
|
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: "
|
|
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))
|
|
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("
|
|
6894
|
-
|
|
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) {
|