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