edge-book 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/edge-book.js +1152 -285
- 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
|
|
@@ -404,6 +405,9 @@ var CONTACT_MUTES_FILE = "contact-mutes.json";
|
|
|
404
405
|
var REPORTS_FILE = "reports.json";
|
|
405
406
|
var INVITE_CODES_FILE = "invite-codes.json";
|
|
406
407
|
var INBOUND_RATE_FILE = "inbound-rate.json";
|
|
408
|
+
var OUTBOX_FILE = "outbox.json";
|
|
409
|
+
var EVENTS_FILE = "events.ndjson";
|
|
410
|
+
var SUPPORT_BUNDLES_FILE = "support-bundles.json";
|
|
407
411
|
var ATTESTATIONS_FILE = "attestations.json";
|
|
408
412
|
var ENDORSEMENTS_FILE = "endorsements.json";
|
|
409
413
|
var SIGNALS_FILE = "signals.json";
|
|
@@ -1503,6 +1507,74 @@ async function expireEscalations(store) {
|
|
|
1503
1507
|
if (changed) await store.saveEscalations(all);
|
|
1504
1508
|
}
|
|
1505
1509
|
|
|
1510
|
+
// src/event-log.ts
|
|
1511
|
+
import crypto4 from "crypto";
|
|
1512
|
+
import fs6 from "fs/promises";
|
|
1513
|
+
import path6 from "path";
|
|
1514
|
+
var MAX_EVENT_LINES = 2e3;
|
|
1515
|
+
var COMPACT_KEEP_LINES = 1e3;
|
|
1516
|
+
function eventErrorCode(e) {
|
|
1517
|
+
if (e instanceof Error) {
|
|
1518
|
+
const code = e.code;
|
|
1519
|
+
if (typeof code === "string" && code.length > 0) return code;
|
|
1520
|
+
return e.constructor.name || "Error";
|
|
1521
|
+
}
|
|
1522
|
+
return "unknown_error";
|
|
1523
|
+
}
|
|
1524
|
+
async function logEvent(store, kind, fields = {}, caps = {}) {
|
|
1525
|
+
try {
|
|
1526
|
+
const event = { ts: (/* @__PURE__ */ new Date()).toISOString(), kind, ...fields };
|
|
1527
|
+
const file = store.file(EVENTS_FILE);
|
|
1528
|
+
await fs6.mkdir(path6.dirname(file), { recursive: true });
|
|
1529
|
+
await fs6.appendFile(file, `${JSON.stringify(event)}
|
|
1530
|
+
`, "utf8");
|
|
1531
|
+
await compactIfNeeded(file, caps.maxLines ?? MAX_EVENT_LINES, caps.keepLines ?? COMPACT_KEEP_LINES);
|
|
1532
|
+
} catch {
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
async function compactIfNeeded(file, maxLines, keepLines) {
|
|
1536
|
+
const text = await fs6.readFile(file, "utf8");
|
|
1537
|
+
const lines = text.split("\n").filter((l) => l.length > 0);
|
|
1538
|
+
if (lines.length <= maxLines) return;
|
|
1539
|
+
const kept = lines.slice(-keepLines);
|
|
1540
|
+
const tmp = `${file}.tmp-${crypto4.randomBytes(6).toString("hex")}`;
|
|
1541
|
+
try {
|
|
1542
|
+
await fs6.writeFile(tmp, `${kept.join("\n")}
|
|
1543
|
+
`, "utf8");
|
|
1544
|
+
await fs6.rename(tmp, file);
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
await fs6.rm(tmp, { force: true }).catch(() => void 0);
|
|
1547
|
+
throw error;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
async function readEvents(store, limit) {
|
|
1551
|
+
let text;
|
|
1552
|
+
try {
|
|
1553
|
+
text = await fs6.readFile(store.file(EVENTS_FILE), "utf8");
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
if (error.code === "ENOENT") return [];
|
|
1556
|
+
return [];
|
|
1557
|
+
}
|
|
1558
|
+
const out = [];
|
|
1559
|
+
for (const line of text.split("\n")) {
|
|
1560
|
+
if (!line) continue;
|
|
1561
|
+
try {
|
|
1562
|
+
const parsed = JSON.parse(line);
|
|
1563
|
+
if (parsed && typeof parsed === "object" && typeof parsed.kind === "string") out.push(parsed);
|
|
1564
|
+
} catch {
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return limit !== void 0 && out.length > limit ? out.slice(-limit) : out;
|
|
1568
|
+
}
|
|
1569
|
+
async function lastEvent(store, kind) {
|
|
1570
|
+
const events = await readEvents(store);
|
|
1571
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
1572
|
+
const e = events[i];
|
|
1573
|
+
if (kind.endsWith(".") ? e.kind.startsWith(kind) : e.kind === kind) return e;
|
|
1574
|
+
}
|
|
1575
|
+
return void 0;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1506
1578
|
// src/store-friends.ts
|
|
1507
1579
|
async function upsertContactFromCard(store, card, state) {
|
|
1508
1580
|
validateCard(card);
|
|
@@ -1569,6 +1641,7 @@ async function setRelationship(store, peerAgentId, nextState, type, reason = "")
|
|
|
1569
1641
|
const event = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
|
|
1570
1642
|
await appendJsonl(store.file(RELATIONSHIP_EVENTS_FILE), event);
|
|
1571
1643
|
await store.audit(`relationship.${type}`, peerAgentId, { previous, next: nextState, reason });
|
|
1644
|
+
await logEvent(store, "friend.state_changed", { peer: peerAgentId, previous, next: nextState, event_type: type });
|
|
1572
1645
|
return event;
|
|
1573
1646
|
}
|
|
1574
1647
|
async function createFriendRequest(store, targetCard, note = "", inviteCode = "") {
|
|
@@ -1608,6 +1681,7 @@ async function receiveFriendRequest(store, envelope) {
|
|
|
1608
1681
|
}
|
|
1609
1682
|
const contact = await store.upsertContactFromCard(body.card, "request_received");
|
|
1610
1683
|
await store.setRelationship(envelope.from_agent_id, "request_received", "FriendRequest", body.note);
|
|
1684
|
+
await logEvent(store, "friend.request_received", { from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
|
|
1611
1685
|
await appendJsonl(store.file(INBOX_FILE), envelope);
|
|
1612
1686
|
const existingApprovals = await store.approvals();
|
|
1613
1687
|
const alreadyPending = Object.values(existingApprovals).some(
|
|
@@ -1651,6 +1725,7 @@ async function acceptFriend(store, peerAgentId, reason = "accepted") {
|
|
|
1651
1725
|
if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
|
|
1652
1726
|
if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked_peer", "Cannot accept a blocked peer");
|
|
1653
1727
|
await store.setRelationship(peerAgentId, "friend", "Accept", reason);
|
|
1728
|
+
await logEvent(store, "friend.accepted", { peer: peerAgentId });
|
|
1654
1729
|
const grant = await store.issueGrant(peerAgentId, ["message.friend", "feed.read.friends", "profile.read.friend", "escalation.raise"]);
|
|
1655
1730
|
const card = await store.writeCard();
|
|
1656
1731
|
const profile = await store.buildFriendProfile();
|
|
@@ -1828,8 +1903,8 @@ async function consumeInviteCode(store, code) {
|
|
|
1828
1903
|
}
|
|
1829
1904
|
|
|
1830
1905
|
// src/store-identity.ts
|
|
1831
|
-
import
|
|
1832
|
-
import
|
|
1906
|
+
import crypto5 from "crypto";
|
|
1907
|
+
import fs7 from "fs/promises";
|
|
1833
1908
|
async function init(store, input = {}) {
|
|
1834
1909
|
await ensureHome(store.home);
|
|
1835
1910
|
const existing = await readJson(store.file(IDENTITY_FILE), null);
|
|
@@ -1837,7 +1912,7 @@ async function init(store, input = {}) {
|
|
|
1837
1912
|
await store.updateConfig({ direct_url: input.directUrl, relay_url: input.relayUrl });
|
|
1838
1913
|
return existing;
|
|
1839
1914
|
}
|
|
1840
|
-
const { publicKey, privateKey } =
|
|
1915
|
+
const { publicKey, privateKey } = crypto5.generateKeyPairSync("ed25519");
|
|
1841
1916
|
const public_key_pem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
1842
1917
|
const private_key_pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
1843
1918
|
const identity = {
|
|
@@ -1897,16 +1972,21 @@ async function setProfile(store, input) {
|
|
|
1897
1972
|
await store.audit("identity.update", identity.agent_id, { display_name: identity.display_name, profile_version: profile.profile_version });
|
|
1898
1973
|
return identity;
|
|
1899
1974
|
}
|
|
1900
|
-
async function setHandle(store, handle) {
|
|
1975
|
+
async function setHandle(store, handle, opts = {}) {
|
|
1901
1976
|
if (!isValidHandle(handle)) {
|
|
1902
1977
|
throw new EdgeBookError("invalid_handle", `invalid_handle: must be 3-30 chars [a-z0-9-], not reserved: ${handle}`);
|
|
1903
1978
|
}
|
|
1904
1979
|
const identity = await store.identity();
|
|
1905
1980
|
identity.handle = handle;
|
|
1981
|
+
if (opts.discoverable === false) {
|
|
1982
|
+
identity.handle_discoverable = false;
|
|
1983
|
+
} else {
|
|
1984
|
+
delete identity.handle_discoverable;
|
|
1985
|
+
}
|
|
1906
1986
|
identity.updated_at = now2();
|
|
1907
1987
|
await writeJson(store.file(IDENTITY_FILE), identity, 384);
|
|
1908
1988
|
await store.writeCard();
|
|
1909
|
-
await store.audit("identity.set_handle", identity.agent_id, { handle });
|
|
1989
|
+
await store.audit("identity.set_handle", identity.agent_id, { handle, discoverable: opts.discoverable !== false });
|
|
1910
1990
|
return identity;
|
|
1911
1991
|
}
|
|
1912
1992
|
async function exportIdentity(store) {
|
|
@@ -1942,6 +2022,7 @@ async function updateConfig(store, input) {
|
|
|
1942
2022
|
if (input.inbound_window_ms !== void 0) next.inbound_window_ms = input.inbound_window_ms;
|
|
1943
2023
|
if (input.handle_nudge_at !== void 0) next.handle_nudge_at = input.handle_nudge_at;
|
|
1944
2024
|
if (input.greeter_mode !== void 0) next.greeter_mode = input.greeter_mode;
|
|
2025
|
+
if (input.support_inbox !== void 0) next.support_inbox = input.support_inbox;
|
|
1945
2026
|
if (input.greeter_welcome_object_id !== void 0) next.greeter_welcome_object_id = input.greeter_welcome_object_id;
|
|
1946
2027
|
await writeJson(store.file(CONFIG_FILE), next);
|
|
1947
2028
|
return next;
|
|
@@ -1973,7 +2054,7 @@ async function buildCard(store, cardUrl) {
|
|
|
1973
2054
|
refresh_after: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString(),
|
|
1974
2055
|
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString()
|
|
1975
2056
|
};
|
|
1976
|
-
const card_hash =
|
|
2057
|
+
const card_hash = crypto5.createHash("sha256").update(canonicalize(unsigned)).digest("base64url");
|
|
1977
2058
|
const withHash = { ...unsigned, card_hash };
|
|
1978
2059
|
return { ...withHash, signature: signPayload(withHash, identity.private_key_pem) };
|
|
1979
2060
|
}
|
|
@@ -1990,7 +2071,7 @@ async function buildHandleClaim(store) {
|
|
|
1990
2071
|
const card = await loadCard(store.file(CARD_FILE));
|
|
1991
2072
|
const claimed_at = Date.now();
|
|
1992
2073
|
const claim_sig = signPayload({ handle: identity.handle, agent_did: identity.agent_id, claimed_at }, identity.private_key_pem);
|
|
1993
|
-
return { handle: identity.handle, agent_did: identity.agent_id, card, claimed_at, claim_sig };
|
|
2074
|
+
return { handle: identity.handle, agent_did: identity.agent_id, card, claimed_at, claim_sig, discoverable: identity.handle_discoverable !== false };
|
|
1994
2075
|
}
|
|
1995
2076
|
async function buildFriendProfile(store) {
|
|
1996
2077
|
const identity = await store.identity();
|
|
@@ -2018,7 +2099,7 @@ async function doctor(store) {
|
|
|
2018
2099
|
const files = {};
|
|
2019
2100
|
for (const name of requiredFiles) {
|
|
2020
2101
|
try {
|
|
2021
|
-
const stat = await
|
|
2102
|
+
const stat = await fs7.stat(store.file(name));
|
|
2022
2103
|
files[name] = {
|
|
2023
2104
|
exists: true,
|
|
2024
2105
|
mode: `0${(stat.mode & 511).toString(8)}`
|
|
@@ -2118,6 +2199,79 @@ async function exportLocalData(store) {
|
|
|
2118
2199
|
};
|
|
2119
2200
|
}
|
|
2120
2201
|
|
|
2202
|
+
// src/store-support.ts
|
|
2203
|
+
var SUPPORT_BUNDLE_MAX_BYTES = 256 * 1024;
|
|
2204
|
+
var SUPPORT_BUNDLE_KEEP = 200;
|
|
2205
|
+
function pruneSupportBundles(all, keepId) {
|
|
2206
|
+
const records = Object.values(all);
|
|
2207
|
+
if (records.length <= SUPPORT_BUNDLE_KEEP) return all;
|
|
2208
|
+
const byAge = (a, b) => a.received_at.localeCompare(b.received_at);
|
|
2209
|
+
const evictable = [
|
|
2210
|
+
...records.filter((r) => r.status === "dismissed" && r.bundle_id !== keepId).sort(byAge),
|
|
2211
|
+
...records.filter((r) => r.status !== "dismissed" && r.bundle_id !== keepId).sort(byAge)
|
|
2212
|
+
];
|
|
2213
|
+
for (const r of evictable) {
|
|
2214
|
+
if (Object.keys(all).length <= SUPPORT_BUNDLE_KEEP) break;
|
|
2215
|
+
delete all[r.bundle_id];
|
|
2216
|
+
}
|
|
2217
|
+
return all;
|
|
2218
|
+
}
|
|
2219
|
+
async function supportBundles(store) {
|
|
2220
|
+
return readJson(store.file(SUPPORT_BUNDLES_FILE), {});
|
|
2221
|
+
}
|
|
2222
|
+
async function saveSupportBundles(store, all) {
|
|
2223
|
+
await writeJson(store.file(SUPPORT_BUNDLES_FILE), all);
|
|
2224
|
+
}
|
|
2225
|
+
async function receiveSupportBundle(store, envelope) {
|
|
2226
|
+
if ((await store.config()).support_inbox !== true) {
|
|
2227
|
+
throw new EdgeBookError("support_inbox_disabled", "This agent does not accept support bundles (operator opt-in: edge-book support inbox --on)");
|
|
2228
|
+
}
|
|
2229
|
+
if (envelope.type !== "support_bundle") throw new EdgeBookError("wrong_message_type", "Expected support_bundle envelope");
|
|
2230
|
+
const size = Buffer.byteLength(JSON.stringify(envelope), "utf8");
|
|
2231
|
+
if (size > SUPPORT_BUNDLE_MAX_BYTES) {
|
|
2232
|
+
throw new EdgeBookError("support_bundle_too_large", `Support bundle is ${size} bytes; the receiver cap is ${SUPPORT_BUNDLE_MAX_BYTES} (256 KiB)`);
|
|
2233
|
+
}
|
|
2234
|
+
await store.enforceInboundRate(envelope.from_agent_id);
|
|
2235
|
+
await store.verifyEnvelope(envelope);
|
|
2236
|
+
const body = envelope.body;
|
|
2237
|
+
if (!body || typeof body !== "object" || !body.report || typeof body.report !== "object") {
|
|
2238
|
+
throw new EdgeBookError("bad_support_bundle", "support_bundle envelope carries no report");
|
|
2239
|
+
}
|
|
2240
|
+
validateCard(body.card);
|
|
2241
|
+
if (body.card.agent_id !== envelope.from_agent_id) {
|
|
2242
|
+
throw new EdgeBookError("agent_id_mismatch", "Embedded card does not match the envelope sender");
|
|
2243
|
+
}
|
|
2244
|
+
const record = {
|
|
2245
|
+
bundle_id: envelope.message_id,
|
|
2246
|
+
from_agent_id: envelope.from_agent_id,
|
|
2247
|
+
...body.card.display_name ? { from_display_name: body.card.display_name } : {},
|
|
2248
|
+
...envelope.trace_id ? { trace_id: envelope.trace_id } : {},
|
|
2249
|
+
received_at: now2(),
|
|
2250
|
+
status: "pending",
|
|
2251
|
+
report: body.report,
|
|
2252
|
+
...typeof body.note === "string" && body.note ? { note: body.note } : {}
|
|
2253
|
+
};
|
|
2254
|
+
const all = await supportBundles(store);
|
|
2255
|
+
all[record.bundle_id] = record;
|
|
2256
|
+
await saveSupportBundles(store, pruneSupportBundles(all, record.bundle_id));
|
|
2257
|
+
await store.audit("support.receive", envelope.from_agent_id, { bundle_id: record.bundle_id, trace_id: envelope.trace_id ?? "" });
|
|
2258
|
+
await logEvent(store, "support.received", { from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
|
|
2259
|
+
return record;
|
|
2260
|
+
}
|
|
2261
|
+
async function pendingSupportBundles(store) {
|
|
2262
|
+
const all = await supportBundles(store);
|
|
2263
|
+
return Object.values(all).filter((b) => b.status === "pending").sort((a, b) => a.received_at.localeCompare(b.received_at));
|
|
2264
|
+
}
|
|
2265
|
+
async function setSupportBundleStatus(store, bundleId, status) {
|
|
2266
|
+
const all = await supportBundles(store);
|
|
2267
|
+
const record = all[bundleId];
|
|
2268
|
+
if (!record) throw new EdgeBookError("unknown_bundle", `Unknown support bundle: ${bundleId}`);
|
|
2269
|
+
record.status = status;
|
|
2270
|
+
all[bundleId] = record;
|
|
2271
|
+
await saveSupportBundles(store, all);
|
|
2272
|
+
return record;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2121
2275
|
// src/store-trust.ts
|
|
2122
2276
|
async function enforceInboundRate(store, peerAgentId) {
|
|
2123
2277
|
const config = await store.config();
|
|
@@ -2244,33 +2398,42 @@ async function assertGrantSignature(store, grant) {
|
|
|
2244
2398
|
}
|
|
2245
2399
|
async function signEnvelope(store, input) {
|
|
2246
2400
|
const identity = await store.identity();
|
|
2401
|
+
const { expires_at, ...rest } = input;
|
|
2247
2402
|
const unsigned = {
|
|
2248
2403
|
message_id: randomId("msg"),
|
|
2249
2404
|
from_agent_id: identity.agent_id,
|
|
2250
2405
|
created_at: now2(),
|
|
2251
|
-
expires_at: new Date(Date.now() + 10 * 60 * 1e3).toISOString(),
|
|
2252
|
-
...
|
|
2406
|
+
expires_at: expires_at ?? new Date(Date.now() + 10 * 60 * 1e3).toISOString(),
|
|
2407
|
+
...rest,
|
|
2408
|
+
// Trace correlation (ea-claude-138): every new outbound envelope carries a
|
|
2409
|
+
// trace_id INSIDE the signed payload (tamper-evident; back-compat — old
|
|
2410
|
+
// receivers canonicalize whatever fields they parsed, so verification
|
|
2411
|
+
// still passes). Callers may pass an existing trace_id to chain a flow;
|
|
2412
|
+
// it is clamped to 128 chars (mirrors the host frame rule) and an empty
|
|
2413
|
+
// string falls back to a generated id.
|
|
2414
|
+
trace_id: input.trace_id?.slice(0, 128) || randomId("trace")
|
|
2253
2415
|
};
|
|
2254
2416
|
return { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
|
|
2255
2417
|
}
|
|
2418
|
+
function embeddedCardKey(envelope) {
|
|
2419
|
+
if (envelope.type !== "friend_request" && envelope.type !== "friend_response" && envelope.type !== "support_bundle") return void 0;
|
|
2420
|
+
const card = envelope.body.card;
|
|
2421
|
+
return card?.public_keys?.[0]?.public_key_pem;
|
|
2422
|
+
}
|
|
2256
2423
|
async function verifyEnvelope(store, envelope) {
|
|
2257
2424
|
const identity = await store.identity();
|
|
2258
2425
|
if (envelope.to_agent_id !== identity.agent_id) throw new EdgeBookError("wrong_recipient", "Envelope recipient does not match local identity");
|
|
2259
2426
|
if (Date.parse(envelope.expires_at) <= Date.now()) throw new EdgeBookError("expired_message", "Message is expired");
|
|
2260
2427
|
const seen = await readJson(store.file(SEEN_MESSAGES_FILE), []);
|
|
2261
|
-
if (seen.includes(envelope.message_id))
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
if (!publicKey && envelope.type === "friend_request") {
|
|
2265
|
-
const card = envelope.body.card;
|
|
2266
|
-
publicKey = card?.public_keys?.[0]?.public_key_pem;
|
|
2267
|
-
}
|
|
2268
|
-
if (!publicKey && envelope.type === "friend_response") {
|
|
2269
|
-
const card = envelope.body.card;
|
|
2270
|
-
publicKey = card?.public_keys?.[0]?.public_key_pem;
|
|
2428
|
+
if (seen.includes(envelope.message_id)) {
|
|
2429
|
+
await logEvent(store, "envelope.dedup_hit", { envelope_kind: envelope.type, from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
|
|
2430
|
+
throw new EdgeBookError("replay", `Replay detected for ${envelope.message_id}`);
|
|
2271
2431
|
}
|
|
2432
|
+
const contacts = await store.contacts();
|
|
2433
|
+
const publicKey = contacts[envelope.from_agent_id]?.public_keys?.[0]?.public_key_pem ?? embeddedCardKey(envelope);
|
|
2272
2434
|
if (!publicKey) throw new EdgeBookError("unknown_key", `Unknown sender key for ${envelope.from_agent_id}`);
|
|
2273
2435
|
if (!verifyPayload(withoutSignature(envelope), envelope.signature, publicKey)) {
|
|
2436
|
+
await logEvent(store, "envelope.signature_failed", { envelope_kind: envelope.type, from: envelope.from_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
|
|
2274
2437
|
throw new EdgeBookError("invalid_signature", "Message signature is invalid");
|
|
2275
2438
|
}
|
|
2276
2439
|
seen.push(envelope.message_id);
|
|
@@ -2317,6 +2480,7 @@ async function revokeSession(store, sessionId) {
|
|
|
2317
2480
|
}
|
|
2318
2481
|
async function receiveEnvelope(store, envelope) {
|
|
2319
2482
|
if (envelope.type === "friend_request") return store.receiveFriendRequest(envelope);
|
|
2483
|
+
if (envelope.type === "support_bundle") return receiveSupportBundle(store, envelope);
|
|
2320
2484
|
if (envelope.type === "friend_response") return store.applyFriendResponse(envelope);
|
|
2321
2485
|
if (envelope.type === "privileged_message") return store.receivePrivilegedMessage(envelope);
|
|
2322
2486
|
if (envelope.type === "object_share") {
|
|
@@ -2416,6 +2580,17 @@ var NOTIFY_POLICIES = {
|
|
|
2416
2580
|
message: `${esc?.subject ?? "A decision is needed"} \u2014 ${esc?.body ?? ""}${opts}`,
|
|
2417
2581
|
dedup_key: env.message_id
|
|
2418
2582
|
};
|
|
2583
|
+
},
|
|
2584
|
+
support_bundle: async (env) => {
|
|
2585
|
+
const body = env.body;
|
|
2586
|
+
const name = body.card?.display_name || env.from_agent_id;
|
|
2587
|
+
return {
|
|
2588
|
+
kind: "support_bundle",
|
|
2589
|
+
from_id: env.from_agent_id,
|
|
2590
|
+
from_name: body.card?.display_name,
|
|
2591
|
+
message: `${name} sent a support bundle (ref ${env.trace_id ?? env.message_id}). Review: edge-book support pending`,
|
|
2592
|
+
dedup_key: env.message_id
|
|
2593
|
+
};
|
|
2419
2594
|
}
|
|
2420
2595
|
};
|
|
2421
2596
|
async function notificationIntent(store, envelope) {
|
|
@@ -2437,6 +2612,14 @@ async function recordNotified(store, dedupKey) {
|
|
|
2437
2612
|
ledger.push(dedupKey);
|
|
2438
2613
|
await writeJson(store.file(NOTIFIED_FILE), ledger);
|
|
2439
2614
|
}
|
|
2615
|
+
function buildPairCompleteNotifyIntent(deviceId, label) {
|
|
2616
|
+
return {
|
|
2617
|
+
kind: "pair_complete",
|
|
2618
|
+
message: `Pairing complete \u2014 your reader is connected (device: ${label}).`,
|
|
2619
|
+
from_id: deviceId,
|
|
2620
|
+
dedup_key: deviceId
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2440
2623
|
|
|
2441
2624
|
// src/edge-book.ts
|
|
2442
2625
|
var EdgeBookStore = class {
|
|
@@ -2445,7 +2628,7 @@ var EdgeBookStore = class {
|
|
|
2445
2628
|
this.home = resolveHome(options.home);
|
|
2446
2629
|
}
|
|
2447
2630
|
file(name) {
|
|
2448
|
-
return
|
|
2631
|
+
return path7.join(this.home, name);
|
|
2449
2632
|
}
|
|
2450
2633
|
async init(input = {}) {
|
|
2451
2634
|
return init(this, input);
|
|
@@ -2464,8 +2647,8 @@ var EdgeBookStore = class {
|
|
|
2464
2647
|
return setProfile(this, input);
|
|
2465
2648
|
}
|
|
2466
2649
|
// Set a user-chosen unique handle. Re-signs the card; does NOT rotate keys.
|
|
2467
|
-
async setHandle(handle) {
|
|
2468
|
-
return setHandle(this, handle);
|
|
2650
|
+
async setHandle(handle, opts) {
|
|
2651
|
+
return setHandle(this, handle, opts);
|
|
2469
2652
|
}
|
|
2470
2653
|
// Portable identity bundle (the DID keypair + chosen handle). Carry to a new
|
|
2471
2654
|
// device → same DID → relay handle keeps resolving to you (spec-096).
|
|
@@ -2981,16 +3164,17 @@ function nextAction(result, target) {
|
|
|
2981
3164
|
return first ? `candidates list # then: friend request ${first.candidate_id}` : "candidates list";
|
|
2982
3165
|
}
|
|
2983
3166
|
default:
|
|
2984
|
-
return "
|
|
3167
|
+
return "not found \u2014 share your invite link so they can add you: card invite (use the deeplink_url)";
|
|
2985
3168
|
}
|
|
2986
3169
|
}
|
|
2987
3170
|
var localContactProvider = {
|
|
2988
3171
|
name: "local",
|
|
2989
3172
|
priority: 100,
|
|
2990
3173
|
async resolve(store, target) {
|
|
3174
|
+
const normalized = target.trim().replace(/^@/, "").toLowerCase();
|
|
2991
3175
|
const contacts = await store.contacts();
|
|
2992
3176
|
const match = Object.values(contacts).find(
|
|
2993
|
-
(c) => c.peer_agent_id === target || c.aliases.
|
|
3177
|
+
(c) => c.peer_agent_id === target || c.aliases.some((a) => a.toLowerCase() === normalized) || c.display_name.toLowerCase() === normalized
|
|
2994
3178
|
);
|
|
2995
3179
|
if (!match) return null;
|
|
2996
3180
|
return {
|
|
@@ -3056,11 +3240,12 @@ async function writeCandidate(store, input) {
|
|
|
3056
3240
|
await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
|
|
3057
3241
|
return candidate;
|
|
3058
3242
|
}
|
|
3243
|
+
var DEFAULT_RELAY_BASE = "https://edge-book-host.fly.dev";
|
|
3059
3244
|
function defaultProviders(relayBase) {
|
|
3245
|
+
const base = relayBase ?? process.env["EDGE_BOOK_RELAY_BASE"] ?? DEFAULT_RELAY_BASE;
|
|
3060
3246
|
const lookup = async (target) => {
|
|
3061
|
-
if (!relayBase) return null;
|
|
3062
3247
|
const slug = target.startsWith("registry:") ? target.slice("registry:".length) : target;
|
|
3063
|
-
return `${
|
|
3248
|
+
return `${base.replace(/\/$/, "")}/handle/${encodeURIComponent(slug)}`;
|
|
3064
3249
|
};
|
|
3065
3250
|
return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(lookup)];
|
|
3066
3251
|
}
|
|
@@ -3108,15 +3293,22 @@ async function promoteCandidate(store, candidateId, note = "") {
|
|
|
3108
3293
|
return envelope;
|
|
3109
3294
|
}
|
|
3110
3295
|
var HANDLE_SLUG2 = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
|
|
3296
|
+
function normalizeRegistryTarget(target) {
|
|
3297
|
+
if (target.startsWith("registry:")) {
|
|
3298
|
+
return "registry:" + target.slice("registry:".length).trim().toLowerCase().replace(/^@/, "");
|
|
3299
|
+
}
|
|
3300
|
+
return target.trim().toLowerCase().replace(/^@/, "");
|
|
3301
|
+
}
|
|
3111
3302
|
function makeRegistryProvider(lookup) {
|
|
3112
3303
|
return {
|
|
3113
3304
|
name: "registry",
|
|
3114
3305
|
priority: 50,
|
|
3115
3306
|
async resolve(_store, target) {
|
|
3116
|
-
const
|
|
3117
|
-
const
|
|
3118
|
-
|
|
3119
|
-
|
|
3307
|
+
const slug = normalizeRegistryTarget(target);
|
|
3308
|
+
const isExplicit = slug.startsWith("registry:");
|
|
3309
|
+
const bareSlug = isExplicit ? slug.slice("registry:".length) : slug;
|
|
3310
|
+
if (!isExplicit && !HANDLE_SLUG2.test(bareSlug)) return null;
|
|
3311
|
+
const cardTarget = await lookup(slug);
|
|
3120
3312
|
if (!cardTarget) return null;
|
|
3121
3313
|
let card;
|
|
3122
3314
|
try {
|
|
@@ -4465,7 +4657,9 @@ async function handleOwnerApi(req, res, url, adapters) {
|
|
|
4465
4657
|
const card = await store.buildCard();
|
|
4466
4658
|
const identity = await store.identity();
|
|
4467
4659
|
const invite_url = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
|
|
4468
|
-
|
|
4660
|
+
const origin = card.card_url ? new URL(card.card_url).origin : "https://edge-book-host.fly.dev";
|
|
4661
|
+
const deeplink_url = `${origin}/add#i=${encodeURIComponent(invite_url)}`;
|
|
4662
|
+
sendJson(res, 200, { agent_id: identity.agent_id, display_name: identity.display_name, card_url: card.card_url, card, invite_url, deeplink_url });
|
|
4469
4663
|
return true;
|
|
4470
4664
|
}
|
|
4471
4665
|
const attachmentMatch = /^\/api\/shared-objects\/([^/]+)\/attachment$/.exec(url.pathname);
|
|
@@ -4747,6 +4941,95 @@ function requestBody(frame, method) {
|
|
|
4747
4941
|
return Buffer.from(JSON.stringify(frame.body ?? {}), "utf8");
|
|
4748
4942
|
}
|
|
4749
4943
|
|
|
4944
|
+
// src/dialout-pair.ts
|
|
4945
|
+
var PairCompleteWaiter = class {
|
|
4946
|
+
resolve = null;
|
|
4947
|
+
timer = null;
|
|
4948
|
+
// Called by EdgeBookDialoutClient.handleMessage when type === "pair_complete".
|
|
4949
|
+
onFrame(frame) {
|
|
4950
|
+
if (!this.resolve) return;
|
|
4951
|
+
const cb = this.resolve;
|
|
4952
|
+
this.resolve = null;
|
|
4953
|
+
if (this.timer !== null) {
|
|
4954
|
+
clearTimeout(this.timer);
|
|
4955
|
+
this.timer = null;
|
|
4956
|
+
}
|
|
4957
|
+
cb({ device_id: frame.device_id ?? "", label: frame.label ?? "" });
|
|
4958
|
+
}
|
|
4959
|
+
// Wait up to ttlMs for a pair_complete frame from the host.
|
|
4960
|
+
// Returns null on timeout — old-host degradation (spec-135 §C.2).
|
|
4961
|
+
wait(ttlMs) {
|
|
4962
|
+
return new Promise((resolve) => {
|
|
4963
|
+
this.resolve = resolve;
|
|
4964
|
+
this.timer = setTimeout(() => {
|
|
4965
|
+
if (this.resolve) {
|
|
4966
|
+
this.resolve = null;
|
|
4967
|
+
resolve(null);
|
|
4968
|
+
}
|
|
4969
|
+
this.timer = null;
|
|
4970
|
+
}, ttlMs);
|
|
4971
|
+
});
|
|
4972
|
+
}
|
|
4973
|
+
};
|
|
4974
|
+
|
|
4975
|
+
// src/dialout-oneshot.ts
|
|
4976
|
+
async function sendPairRegistration(options) {
|
|
4977
|
+
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
4978
|
+
await client.start();
|
|
4979
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
4980
|
+
const registration = await client.pair(options.ttlMs ?? DEFAULT_PAIR_TTL_MS);
|
|
4981
|
+
await client.stop();
|
|
4982
|
+
return registration;
|
|
4983
|
+
}
|
|
4984
|
+
async function deliverEnvelopeViaMailbox(options) {
|
|
4985
|
+
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
4986
|
+
await client.start();
|
|
4987
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
4988
|
+
try {
|
|
4989
|
+
return await client.sendEnvelope(options.envelope);
|
|
4990
|
+
} finally {
|
|
4991
|
+
await client.stop();
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
async function sendSessionsRevoke(options) {
|
|
4995
|
+
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
4996
|
+
await client.start();
|
|
4997
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
4998
|
+
const { frame, ack } = await client.revokeSessionsAndWait();
|
|
4999
|
+
await client.stop();
|
|
5000
|
+
return { ...frame, channel_id: ack.channel_id };
|
|
5001
|
+
}
|
|
5002
|
+
async function listSessions(options) {
|
|
5003
|
+
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
5004
|
+
await client.start();
|
|
5005
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
5006
|
+
try {
|
|
5007
|
+
return await client.listSessionsAndWait();
|
|
5008
|
+
} finally {
|
|
5009
|
+
await client.stop();
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
async function revokeOneSession(options) {
|
|
5013
|
+
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
5014
|
+
await client.start();
|
|
5015
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
5016
|
+
try {
|
|
5017
|
+
return await client.revokeOneSessionAndWait(options.deviceId);
|
|
5018
|
+
} finally {
|
|
5019
|
+
await client.stop();
|
|
5020
|
+
}
|
|
5021
|
+
}
|
|
5022
|
+
async function mailboxStatus(options) {
|
|
5023
|
+
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
5024
|
+
await client.start();
|
|
5025
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
5026
|
+
try {
|
|
5027
|
+
return await client.mailboxStatusAndWait(options.ids, options.timeoutMs ?? 5e3);
|
|
5028
|
+
} finally {
|
|
5029
|
+
await client.stop();
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
|
|
4750
5033
|
// src/dialout.ts
|
|
4751
5034
|
var DEFAULT_DIALOUT_HOST = "wss://edge-book-host.fly.dev/agent/ws";
|
|
4752
5035
|
var DEFAULT_HEARTBEAT_MS = 25e3;
|
|
@@ -4778,9 +5061,12 @@ var EdgeBookDialoutClient = class {
|
|
|
4778
5061
|
currentBackoff;
|
|
4779
5062
|
opened;
|
|
4780
5063
|
pendingSessionRevokes = /* @__PURE__ */ new Map();
|
|
4781
|
-
// Generic request_id-keyed RPC waiters
|
|
5064
|
+
// Generic request_id-keyed RPC waiters (sessions_list / session_revoke_one /
|
|
5065
|
+
// mailbox_status). rpcType lets an old host's {type:"error", ref:"<type>"}
|
|
5066
|
+
// frame — which carries NO request_id — reject pending requests of that type.
|
|
4782
5067
|
pendingRpc = /* @__PURE__ */ new Map();
|
|
4783
5068
|
pendingMailboxSends = /* @__PURE__ */ new Map();
|
|
5069
|
+
pairCompleteWaiter = new PairCompleteWaiter();
|
|
4784
5070
|
constructor(options) {
|
|
4785
5071
|
this.options = {
|
|
4786
5072
|
heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
|
|
@@ -4814,6 +5100,11 @@ var EdgeBookDialoutClient = class {
|
|
|
4814
5100
|
this.send(registration.frame);
|
|
4815
5101
|
return registration;
|
|
4816
5102
|
}
|
|
5103
|
+
// Wait up to ttlMs for a pair_complete frame from the host.
|
|
5104
|
+
// Returns null on timeout — old-host degradation (spec-135).
|
|
5105
|
+
waitForPairComplete(ttlMs) {
|
|
5106
|
+
return this.pairCompleteWaiter.wait(ttlMs);
|
|
5107
|
+
}
|
|
4817
5108
|
async revokeSessions() {
|
|
4818
5109
|
const frame = await createSessionsRevokeFrame(this.store);
|
|
4819
5110
|
this.send(frame);
|
|
@@ -4829,16 +5120,29 @@ var EdgeBookDialoutClient = class {
|
|
|
4829
5120
|
const frame = await this.rpc("session_revoke_one", { device_id }, "session_revoke_one_ok", timeoutMs);
|
|
4830
5121
|
return Boolean(frame.revoked);
|
|
4831
5122
|
}
|
|
5123
|
+
// Ask the host for per-message delivery state (spec-097). Returns null when
|
|
5124
|
+
// the host predates receipts — detected by the unknown-type error frame
|
|
5125
|
+
// (fast path) or the RPC timeout (lost-frame path); both degrade the same.
|
|
5126
|
+
async mailboxStatusAndWait(ids, timeoutMs = 5e3) {
|
|
5127
|
+
try {
|
|
5128
|
+
const frame = await this.rpc("mailbox_status", { ids }, "mailbox_status_ok", timeoutMs);
|
|
5129
|
+
if (frame.type === "mailbox_status_err") throw new EdgeBookError("mailbox_status_failed", String(frame.error || "mailbox_status rejected"));
|
|
5130
|
+
return frame.statuses ?? [];
|
|
5131
|
+
} catch (error) {
|
|
5132
|
+
if (error instanceof EdgeBookError && (error.code === "host_rpc_timeout" || error.code === "host_unsupported_rpc")) return null;
|
|
5133
|
+
throw error;
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
4832
5136
|
// Small request/response helper over the dial-out socket, correlated by
|
|
4833
5137
|
// request_id. `expect` documents the ack type; resolution is by request_id.
|
|
4834
5138
|
async rpc(type, extra, expect, timeoutMs) {
|
|
4835
|
-
const request_id =
|
|
5139
|
+
const request_id = crypto6.randomUUID();
|
|
4836
5140
|
const promise = new Promise((resolve, reject) => {
|
|
4837
5141
|
const timer = setTimeout(() => {
|
|
4838
5142
|
this.pendingRpc.delete(request_id);
|
|
4839
5143
|
reject(new EdgeBookError("host_rpc_timeout", `Timed out waiting for ${expect}`));
|
|
4840
5144
|
}, timeoutMs);
|
|
4841
|
-
this.pendingRpc.set(request_id, { resolve, reject, timer });
|
|
5145
|
+
this.pendingRpc.set(request_id, { resolve, reject, timer, rpcType: type });
|
|
4842
5146
|
});
|
|
4843
5147
|
this.send({ type, request_id, ...extra });
|
|
4844
5148
|
return promise;
|
|
@@ -4857,9 +5161,10 @@ var EdgeBookDialoutClient = class {
|
|
|
4857
5161
|
}
|
|
4858
5162
|
// ── Mailbox transport (Contract 1 / ea-claude-065) ─────────────────────────
|
|
4859
5163
|
// Low-level: hand an opaque blob to the host for delivery to `to` (a peer DID
|
|
4860
|
-
// or channel_id). Resolves with the host-assigned message id once enqueued
|
|
4861
|
-
|
|
4862
|
-
|
|
5164
|
+
// or channel_id). Resolves with the host-assigned message id once enqueued,
|
|
5165
|
+
// plus recipient_live when the host supports receipts (spec-097).
|
|
5166
|
+
async sendMailbox(to, blob, timeoutMs = 5e3, trace_id) {
|
|
5167
|
+
const request_id = crypto6.randomUUID();
|
|
4863
5168
|
const blob_b64 = Buffer.from(blob).toString("base64");
|
|
4864
5169
|
const ack = new Promise((resolve, reject) => {
|
|
4865
5170
|
const timer = setTimeout(() => {
|
|
@@ -4868,14 +5173,16 @@ var EdgeBookDialoutClient = class {
|
|
|
4868
5173
|
}, timeoutMs);
|
|
4869
5174
|
this.pendingMailboxSends.set(request_id, { resolve, reject, timer });
|
|
4870
5175
|
});
|
|
4871
|
-
this.send({ type: "mailbox_send", request_id, to, blob_b64 });
|
|
5176
|
+
this.send({ type: "mailbox_send", request_id, to, blob_b64, ...trace_id ? { trace_id } : {} });
|
|
4872
5177
|
return ack;
|
|
4873
5178
|
}
|
|
4874
5179
|
// High-level: deliver a signed envelope to its recipient (envelope.to_agent_id;
|
|
4875
5180
|
// the host resolves the DID to a channel). Used to route friend requests,
|
|
4876
5181
|
// object shares, and revokes through the mailbox instead of a manual file hop.
|
|
4877
5182
|
async sendEnvelope(envelope) {
|
|
4878
|
-
|
|
5183
|
+
const ack = await this.sendMailbox(envelope.to_agent_id, Buffer.from(JSON.stringify(envelope), "utf8"), 5e3, envelope.trace_id);
|
|
5184
|
+
await logEvent(this.store, "envelope.sent", { envelope_kind: envelope.type, to: envelope.to_agent_id, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
|
|
5185
|
+
return ack;
|
|
4879
5186
|
}
|
|
4880
5187
|
async handleMailboxDeliver(frame) {
|
|
4881
5188
|
let envelope;
|
|
@@ -4893,8 +5200,16 @@ var EdgeBookDialoutClient = class {
|
|
|
4893
5200
|
}
|
|
4894
5201
|
}
|
|
4895
5202
|
} catch (e) {
|
|
4896
|
-
error =
|
|
4897
|
-
}
|
|
5203
|
+
error = eventErrorCode(e);
|
|
5204
|
+
}
|
|
5205
|
+
await logEvent(this.store, "envelope.received", {
|
|
5206
|
+
envelope_kind: envelope?.type ?? "unparseable",
|
|
5207
|
+
from: frame.from,
|
|
5208
|
+
dedup_key: envelope?.message_id,
|
|
5209
|
+
trace_id: envelope?.trace_id,
|
|
5210
|
+
applied,
|
|
5211
|
+
...error ? { error } : {}
|
|
5212
|
+
});
|
|
4898
5213
|
if (!this.mailboxQueue) this.send({ type: "mailbox_ack", id: frame.id });
|
|
4899
5214
|
if (envelope) await this.options.onEnvelope?.(envelope, { applied, error });
|
|
4900
5215
|
}
|
|
@@ -4942,7 +5257,7 @@ var EdgeBookDialoutClient = class {
|
|
|
4942
5257
|
agent_key: key.agent_key,
|
|
4943
5258
|
agent_did: identity.agent_id,
|
|
4944
5259
|
version: "0.1.0",
|
|
4945
|
-
nonce:
|
|
5260
|
+
nonce: crypto6.randomUUID()
|
|
4946
5261
|
});
|
|
4947
5262
|
} catch (error) {
|
|
4948
5263
|
this.opened = void 0;
|
|
@@ -4955,6 +5270,7 @@ var EdgeBookDialoutClient = class {
|
|
|
4955
5270
|
});
|
|
4956
5271
|
addSocketListener(socket, "close", () => {
|
|
4957
5272
|
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
5273
|
+
void logEvent(this.store, "dialout.disconnected", { host: this.options.host, stopped: this.stopped });
|
|
4958
5274
|
if (!this.stopped && this.options.reconnect) this.scheduleReconnect();
|
|
4959
5275
|
});
|
|
4960
5276
|
await opened;
|
|
@@ -4963,6 +5279,7 @@ var EdgeBookDialoutClient = class {
|
|
|
4963
5279
|
if (this.stopped) return;
|
|
4964
5280
|
const delay = this.currentBackoff;
|
|
4965
5281
|
this.currentBackoff = Math.min(MAX_BACKOFF_MS, Math.round(this.currentBackoff * 1.7));
|
|
5282
|
+
void logEvent(this.store, "dialout.reconnect_scheduled", { host: this.options.host, delay_ms: delay });
|
|
4966
5283
|
this.reconnectTimer = setTimeout(() => {
|
|
4967
5284
|
void this.connect();
|
|
4968
5285
|
}, delay);
|
|
@@ -4976,6 +5293,7 @@ var EdgeBookDialoutClient = class {
|
|
|
4976
5293
|
if (frame.type === "hello_ok") {
|
|
4977
5294
|
this.opened?.resolve();
|
|
4978
5295
|
this.opened = void 0;
|
|
5296
|
+
void logEvent(this.store, "dialout.connected", { host: this.options.host });
|
|
4979
5297
|
try {
|
|
4980
5298
|
const identity = await this.store.identity();
|
|
4981
5299
|
if (shouldClaimHandle(identity.handle)) {
|
|
@@ -4986,7 +5304,8 @@ var EdgeBookDialoutClient = class {
|
|
|
4986
5304
|
handle: claim.handle,
|
|
4987
5305
|
card: claim.card,
|
|
4988
5306
|
claimed_at: claim.claimed_at,
|
|
4989
|
-
claim_sig: claim.claim_sig
|
|
5307
|
+
claim_sig: claim.claim_sig,
|
|
5308
|
+
discoverable: claim.discoverable
|
|
4990
5309
|
});
|
|
4991
5310
|
}
|
|
4992
5311
|
} catch {
|
|
@@ -5003,7 +5322,7 @@ var EdgeBookDialoutClient = class {
|
|
|
5003
5322
|
this.send({ type: "pong" });
|
|
5004
5323
|
return;
|
|
5005
5324
|
}
|
|
5006
|
-
if (frame.type === "sessions_list_ok" || frame.type === "session_revoke_one_ok") {
|
|
5325
|
+
if (frame.type === "sessions_list_ok" || frame.type === "session_revoke_one_ok" || frame.type === "mailbox_status_ok" || frame.type === "mailbox_status_err") {
|
|
5007
5326
|
const ack = frame;
|
|
5008
5327
|
const pending = this.pendingRpc.get(ack.request_id || "");
|
|
5009
5328
|
if (pending) {
|
|
@@ -5018,6 +5337,10 @@ var EdgeBookDialoutClient = class {
|
|
|
5018
5337
|
return;
|
|
5019
5338
|
}
|
|
5020
5339
|
if (frame.type === "pair_register_ok" || frame.type === "pair_register_err") return;
|
|
5340
|
+
if (frame.type === "pair_complete") {
|
|
5341
|
+
this.pairCompleteWaiter.onFrame(frame);
|
|
5342
|
+
return;
|
|
5343
|
+
}
|
|
5021
5344
|
if (frame.type === "sessions_revoke_ok") {
|
|
5022
5345
|
const ack = frame;
|
|
5023
5346
|
const pending = this.pendingSessionRevokes.get(ack.request_id || "");
|
|
@@ -5035,7 +5358,7 @@ var EdgeBookDialoutClient = class {
|
|
|
5035
5358
|
if (pending) {
|
|
5036
5359
|
clearTimeout(pending.timer);
|
|
5037
5360
|
this.pendingMailboxSends.delete(ack.request_id || "");
|
|
5038
|
-
pending.resolve({ id: ack.id || "" });
|
|
5361
|
+
pending.resolve({ id: ack.id || "", ...typeof ack.recipient_live === "boolean" ? { recipient_live: ack.recipient_live } : {} });
|
|
5039
5362
|
}
|
|
5040
5363
|
return;
|
|
5041
5364
|
}
|
|
@@ -5054,13 +5377,24 @@ var EdgeBookDialoutClient = class {
|
|
|
5054
5377
|
return;
|
|
5055
5378
|
}
|
|
5056
5379
|
if (frameType === "handle_claim_ok" || frameType === "handle_claim_err") return;
|
|
5057
|
-
if (frameType === "error")
|
|
5380
|
+
if (frameType === "error") {
|
|
5381
|
+
const ref = frame.ref;
|
|
5382
|
+
if (typeof ref !== "string") return;
|
|
5383
|
+
for (const [request_id, pending] of [...this.pendingRpc]) {
|
|
5384
|
+
if (pending.rpcType !== ref) continue;
|
|
5385
|
+
clearTimeout(pending.timer);
|
|
5386
|
+
this.pendingRpc.delete(request_id);
|
|
5387
|
+
pending.reject(new EdgeBookError("host_unsupported_rpc", `Host does not support ${ref}`));
|
|
5388
|
+
}
|
|
5389
|
+
return;
|
|
5390
|
+
}
|
|
5058
5391
|
if (frame.type !== "host.api.request" && frame.type !== "api_request") return;
|
|
5059
5392
|
const response = await this.handleApiRequest(frame);
|
|
5060
5393
|
this.send(response);
|
|
5061
5394
|
}
|
|
5062
5395
|
async standDown(frame) {
|
|
5063
5396
|
this.stopped = true;
|
|
5397
|
+
await logEvent(this.store, "dialout.stand_down", { host: this.options.host, reason: frame.reason, idle_ms: frame.idle_ms });
|
|
5064
5398
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
5065
5399
|
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
5066
5400
|
this.socket?.close();
|
|
@@ -5136,70 +5470,24 @@ var EdgeBookDialoutClient = class {
|
|
|
5136
5470
|
}
|
|
5137
5471
|
}
|
|
5138
5472
|
};
|
|
5139
|
-
async function sendPairRegistration(options) {
|
|
5140
|
-
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
5141
|
-
await client.start();
|
|
5142
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
5143
|
-
const registration = await client.pair(options.ttlMs ?? DEFAULT_PAIR_TTL_MS);
|
|
5144
|
-
await client.stop();
|
|
5145
|
-
return registration;
|
|
5146
|
-
}
|
|
5147
|
-
async function deliverEnvelopeViaMailbox(options) {
|
|
5148
|
-
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
5149
|
-
await client.start();
|
|
5150
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
5151
|
-
try {
|
|
5152
|
-
return await client.sendEnvelope(options.envelope);
|
|
5153
|
-
} finally {
|
|
5154
|
-
await client.stop();
|
|
5155
|
-
}
|
|
5156
|
-
}
|
|
5157
|
-
async function sendSessionsRevoke(options) {
|
|
5158
|
-
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
5159
|
-
await client.start();
|
|
5160
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
5161
|
-
const { frame, ack } = await client.revokeSessionsAndWait();
|
|
5162
|
-
await client.stop();
|
|
5163
|
-
return { ...frame, channel_id: ack.channel_id };
|
|
5164
|
-
}
|
|
5165
|
-
async function listSessions(options) {
|
|
5166
|
-
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
5167
|
-
await client.start();
|
|
5168
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
5169
|
-
try {
|
|
5170
|
-
return await client.listSessionsAndWait();
|
|
5171
|
-
} finally {
|
|
5172
|
-
await client.stop();
|
|
5173
|
-
}
|
|
5174
|
-
}
|
|
5175
|
-
async function revokeOneSession(options) {
|
|
5176
|
-
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
5177
|
-
await client.start();
|
|
5178
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
5179
|
-
try {
|
|
5180
|
-
return await client.revokeOneSessionAndWait(options.deviceId);
|
|
5181
|
-
} finally {
|
|
5182
|
-
await client.stop();
|
|
5183
|
-
}
|
|
5184
|
-
}
|
|
5185
5473
|
|
|
5186
5474
|
// src/http-relay.ts
|
|
5187
|
-
import
|
|
5475
|
+
import fs8 from "fs/promises";
|
|
5188
5476
|
import http2 from "http";
|
|
5189
|
-
import
|
|
5477
|
+
import path8 from "path";
|
|
5190
5478
|
function relayFile(store, agentId) {
|
|
5191
|
-
return
|
|
5479
|
+
return path8.join(store, `${encodeURIComponent(agentId)}.jsonl`);
|
|
5192
5480
|
}
|
|
5193
5481
|
async function appendRelayEnvelope(store, agentId, envelope) {
|
|
5194
|
-
await
|
|
5195
|
-
await
|
|
5482
|
+
await fs8.mkdir(store, { recursive: true });
|
|
5483
|
+
await fs8.appendFile(relayFile(store, agentId), `${JSON.stringify(envelope)}
|
|
5196
5484
|
`, "utf8");
|
|
5197
5485
|
}
|
|
5198
5486
|
async function drainRelayEnvelopes(store, agentId) {
|
|
5199
5487
|
const file = relayFile(store, agentId);
|
|
5200
5488
|
try {
|
|
5201
|
-
const text = await
|
|
5202
|
-
await
|
|
5489
|
+
const text = await fs8.readFile(file, "utf8");
|
|
5490
|
+
await fs8.writeFile(file, "", "utf8");
|
|
5203
5491
|
return text.split(/\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
5204
5492
|
} catch (error) {
|
|
5205
5493
|
if (error.code === "ENOENT") return [];
|
|
@@ -5258,6 +5546,32 @@ async function pullRelayEnvelopes(relayBaseUrl, recipientAgentId) {
|
|
|
5258
5546
|
return body.envelopes || [];
|
|
5259
5547
|
}
|
|
5260
5548
|
|
|
5549
|
+
// src/store-outbox.ts
|
|
5550
|
+
import path9 from "path";
|
|
5551
|
+
var OUTBOX_CAP = 200;
|
|
5552
|
+
var DEFAULT_STALE_QUEUE_MS = 10 * 60 * 1e3;
|
|
5553
|
+
function outboxPath(home) {
|
|
5554
|
+
return path9.join(resolveHome(home), OUTBOX_FILE);
|
|
5555
|
+
}
|
|
5556
|
+
async function readOutbox(home) {
|
|
5557
|
+
return readJson(outboxPath(home), []);
|
|
5558
|
+
}
|
|
5559
|
+
async function recordOutboxEntry(home, entry) {
|
|
5560
|
+
const entries = await readOutbox(home);
|
|
5561
|
+
entries.push({ sent_at: now2(), ...entry });
|
|
5562
|
+
await writeJson(outboxPath(home), entries.slice(-OUTBOX_CAP));
|
|
5563
|
+
}
|
|
5564
|
+
function staleQueueMs() {
|
|
5565
|
+
const raw = Number(process.env.EDGE_BOOK_STALE_QUEUE_MS);
|
|
5566
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_STALE_QUEUE_MS;
|
|
5567
|
+
}
|
|
5568
|
+
function formatAge(ms) {
|
|
5569
|
+
if (ms < 6e4) return `${Math.max(0, Math.round(ms / 1e3))}s`;
|
|
5570
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
|
|
5571
|
+
if (ms < 864e5) return `${Math.round(ms / 36e5)}h`;
|
|
5572
|
+
return `${Math.round(ms / 864e5)}d`;
|
|
5573
|
+
}
|
|
5574
|
+
|
|
5261
5575
|
// src/cli-shared.ts
|
|
5262
5576
|
function takeFlag(args, name) {
|
|
5263
5577
|
const idx = args.indexOf(name);
|
|
@@ -5307,7 +5621,7 @@ function takeRepeated(args, flag) {
|
|
|
5307
5621
|
return out;
|
|
5308
5622
|
}
|
|
5309
5623
|
async function readEnvelope(filePath) {
|
|
5310
|
-
return JSON.parse(await
|
|
5624
|
+
return JSON.parse(await fs9.readFile(path10.resolve(filePath), "utf8"));
|
|
5311
5625
|
}
|
|
5312
5626
|
async function deliverToEndpoint(envelope, endpoint) {
|
|
5313
5627
|
await postEnvelope(endpoint, envelope);
|
|
@@ -5325,13 +5639,33 @@ async function deliverToPeer(store, envelope, peerAgentId) {
|
|
|
5325
5639
|
}
|
|
5326
5640
|
throw new EdgeBookError("no_route", `No direct or relay endpoint for ${peerAgentId}`);
|
|
5327
5641
|
}
|
|
5642
|
+
async function deliverViaMailboxRecorded(envelope, opts, legacyText) {
|
|
5643
|
+
const ack = await deliverEnvelopeViaMailbox({ home: opts.home, host: opts.host, socketFactory: opts.socketFactory, envelope });
|
|
5644
|
+
await recordOutboxEntry(opts.home, {
|
|
5645
|
+
id: ack.id,
|
|
5646
|
+
to_agent_id: envelope.to_agent_id,
|
|
5647
|
+
envelope_type: envelope.type,
|
|
5648
|
+
...typeof ack.recipient_live === "boolean" ? { recipient_live: ack.recipient_live } : {}
|
|
5649
|
+
});
|
|
5650
|
+
if (ack.recipient_live === true) {
|
|
5651
|
+
return { ...ack, text: `Sent ${envelope.type} to ${envelope.to_agent_id} \u2014 recipient's agent is connected (host id ${ack.id}).` };
|
|
5652
|
+
}
|
|
5653
|
+
if (ack.recipient_live === false) {
|
|
5654
|
+
return { ...ack, text: `Queued ${envelope.type} to ${envelope.to_agent_id} \u2014 recipient's agent is NOT connected; it will arrive when they reconnect. Check later: edge-book outbox (host id ${ack.id}).` };
|
|
5655
|
+
}
|
|
5656
|
+
return { ...ack, text: legacyText(ack.id) };
|
|
5657
|
+
}
|
|
5328
5658
|
async function broadcastPost(store, host, socketFactory2, post) {
|
|
5329
5659
|
const contacts = await store.contacts();
|
|
5330
5660
|
const friends = Object.values(contacts).filter((c) => c.relationship_state === "friend");
|
|
5331
5661
|
let count = 0;
|
|
5332
5662
|
for (const f of friends) {
|
|
5333
5663
|
const envelope = await store.signPostPublishEnvelope({ to_agent_id: f.peer_agent_id, post });
|
|
5334
|
-
await
|
|
5664
|
+
await deliverViaMailboxRecorded(
|
|
5665
|
+
envelope,
|
|
5666
|
+
{ home: store.home, host, socketFactory: socketFactory2 },
|
|
5667
|
+
(id) => `Delivered post_publish (host id ${id})`
|
|
5668
|
+
);
|
|
5335
5669
|
count++;
|
|
5336
5670
|
}
|
|
5337
5671
|
return count;
|
|
@@ -5368,24 +5702,385 @@ ${buildHandleNudge(suggestion)}` };
|
|
|
5368
5702
|
}
|
|
5369
5703
|
|
|
5370
5704
|
// src/cli-identity.ts
|
|
5371
|
-
import
|
|
5372
|
-
import
|
|
5705
|
+
import fs11 from "fs/promises";
|
|
5706
|
+
import path11 from "path";
|
|
5373
5707
|
|
|
5374
|
-
// src/
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5708
|
+
// src/doctor.ts
|
|
5709
|
+
import fs10 from "fs/promises";
|
|
5710
|
+
|
|
5711
|
+
// src/host-cron.ts
|
|
5712
|
+
import { existsSync } from "fs";
|
|
5713
|
+
import { execFileSync } from "child_process";
|
|
5714
|
+
var FRIEND_REQUESTS_CRON_NAME = "Edge Book \u2014 friend requests";
|
|
5715
|
+
var DEFAULT_FRIEND_REQUESTS_SCHEDULE = "*/20 * * * *";
|
|
5716
|
+
var HERMES_BIN_CANDIDATES = ["/opt/hermes/.venv/bin/hermes"];
|
|
5717
|
+
function buildFriendRequestsPrompt(home) {
|
|
5718
|
+
return [
|
|
5719
|
+
"You are the Edge Book friend-request notifier. Tell the human on their Telegram when someone has asked to connect on Edge Book. Hermes delivers your final assistant reply to their chat.",
|
|
5720
|
+
"",
|
|
5721
|
+
"This runs every 20 minutes; most runs there will be nothing pending. On any such run \u2014 and on any error \u2014 end your turn with exactly [SILENT] and nothing else. [SILENT] tells Hermes to send no message.",
|
|
5722
|
+
"",
|
|
5723
|
+
"1. List pending requests (run once):",
|
|
5724
|
+
` edge-book friend pending --home ${home} --json`,
|
|
5725
|
+
" If edge-book is not on PATH, use: npm exec -y edge-book@0.11.0 -- friend pending --home " + home + " --json",
|
|
5726
|
+
" If the command errors, Edge Book is unavailable, or the list is empty ([]) \u2192 end your turn with exactly [SILENT].",
|
|
5727
|
+
"",
|
|
5728
|
+
'2. Otherwise write ONE short, warm message. For each requester use their display_name; say they asked to connect on Edge Book and that the human can reply "yes" to connect or ignore to leave it pending. No internal IDs, no JSON.',
|
|
5729
|
+
"",
|
|
5730
|
+
"3. Mark each surfaced request notified so it is never re-sent (once per requester):",
|
|
5731
|
+
` edge-book friend mark-notified <agent_id> --home ${home}`
|
|
5732
|
+
].join("\n");
|
|
5733
|
+
}
|
|
5734
|
+
function ensureNotifierCron(opts) {
|
|
5735
|
+
if (opts.disabled) return { status: "disabled" };
|
|
5736
|
+
if (!opts.runner.hermesBin) return { status: "host_unsupported" };
|
|
5737
|
+
let listing;
|
|
5738
|
+
try {
|
|
5739
|
+
listing = opts.runner.list();
|
|
5740
|
+
} catch (e) {
|
|
5741
|
+
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
5742
|
+
}
|
|
5743
|
+
if (listing.includes(FRIEND_REQUESTS_CRON_NAME)) return { status: "already_present" };
|
|
5744
|
+
const schedule = opts.schedule ?? DEFAULT_FRIEND_REQUESTS_SCHEDULE;
|
|
5745
|
+
const prompt = buildFriendRequestsPrompt(opts.home);
|
|
5746
|
+
const args = [
|
|
5747
|
+
"cron",
|
|
5748
|
+
"create",
|
|
5749
|
+
schedule,
|
|
5750
|
+
prompt,
|
|
5751
|
+
"--name",
|
|
5752
|
+
FRIEND_REQUESTS_CRON_NAME,
|
|
5753
|
+
"--deliver",
|
|
5754
|
+
"telegram",
|
|
5755
|
+
"--workdir",
|
|
5756
|
+
opts.home
|
|
5757
|
+
];
|
|
5758
|
+
try {
|
|
5759
|
+
opts.runner.create(args);
|
|
5760
|
+
return { status: "installed" };
|
|
5761
|
+
} catch (e) {
|
|
5762
|
+
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
function defaultHermesRunner() {
|
|
5766
|
+
const bin = HERMES_BIN_CANDIDATES.find((p) => existsSync(p)) ?? null;
|
|
5767
|
+
return {
|
|
5768
|
+
hermesBin: bin,
|
|
5769
|
+
list: () => bin ? execFileSync(bin, ["cron", "list"], { encoding: "utf8" }) : "",
|
|
5770
|
+
create: (args) => {
|
|
5771
|
+
if (bin) execFileSync(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
5772
|
+
}
|
|
5773
|
+
};
|
|
5774
|
+
}
|
|
5775
|
+
var GREETER_CRON_NAME = "Edge Book \u2014 greeter";
|
|
5776
|
+
var DEFAULT_GREETER_SCHEDULE = "*/5 * * * *";
|
|
5777
|
+
function buildGreeterPrompt(home) {
|
|
5778
|
+
return [
|
|
5779
|
+
"You are the Edge Book greeter runner. Run the command below once and report what it did. Hermes delivers your final assistant reply to the chat.",
|
|
5780
|
+
"",
|
|
5781
|
+
` edge-book friend auto-accept --deliver --home ${home}`,
|
|
5782
|
+
` If edge-book is not on PATH, use: npm exec -y edge-book -- friend auto-accept --deliver --home ${home}`,
|
|
5783
|
+
"",
|
|
5784
|
+
"If the command errors, or its JSON output is an empty list ([]), end your turn with exactly [SILENT] and nothing else. [SILENT] tells Hermes to send no message.",
|
|
5785
|
+
"",
|
|
5786
|
+
"Otherwise reply with one short line per entry: the agent_id, whether it was accepted, and whether it was welcomed. No extra commentary, no raw JSON."
|
|
5787
|
+
].join("\n");
|
|
5788
|
+
}
|
|
5789
|
+
function ensureGreeterCron(opts) {
|
|
5790
|
+
if (opts.disabled) return { status: "disabled" };
|
|
5791
|
+
if (!opts.runner.hermesBin) return { status: "host_unsupported" };
|
|
5792
|
+
let listing;
|
|
5793
|
+
try {
|
|
5794
|
+
listing = opts.runner.list();
|
|
5795
|
+
} catch (e) {
|
|
5796
|
+
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
5797
|
+
}
|
|
5798
|
+
if (listing.includes(GREETER_CRON_NAME)) return { status: "already_present" };
|
|
5799
|
+
const args = [
|
|
5800
|
+
"cron",
|
|
5801
|
+
"create",
|
|
5802
|
+
opts.schedule ?? DEFAULT_GREETER_SCHEDULE,
|
|
5803
|
+
buildGreeterPrompt(opts.home),
|
|
5804
|
+
"--name",
|
|
5805
|
+
GREETER_CRON_NAME,
|
|
5806
|
+
"--deliver",
|
|
5807
|
+
"telegram",
|
|
5808
|
+
"--workdir",
|
|
5809
|
+
opts.home
|
|
5810
|
+
];
|
|
5811
|
+
try {
|
|
5812
|
+
opts.runner.create(args);
|
|
5813
|
+
return { status: "installed" };
|
|
5814
|
+
} catch (e) {
|
|
5815
|
+
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
|
|
5819
|
+
// src/doctor.ts
|
|
5820
|
+
var DOCTOR_EVENT_TAIL = 50;
|
|
5821
|
+
var DOCTOR_TRACE_TAIL = 10;
|
|
5822
|
+
var DEFAULT_RELAY_TIMEOUT_MS = 3e3;
|
|
5823
|
+
function recentTraces(events, limit = DOCTOR_TRACE_TAIL) {
|
|
5824
|
+
const out = [];
|
|
5825
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5826
|
+
for (let i = events.length - 1; i >= 0 && out.length < limit; i--) {
|
|
5827
|
+
const e = events[i];
|
|
5828
|
+
const trace_id = typeof e.trace_id === "string" ? e.trace_id : void 0;
|
|
5829
|
+
if (!trace_id || seen.has(trace_id)) continue;
|
|
5830
|
+
seen.add(trace_id);
|
|
5831
|
+
out.push({ trace_id, kind: e.kind, direction: e.kind === "envelope.sent" ? "out" : "in", ts: e.ts });
|
|
5832
|
+
}
|
|
5833
|
+
return out.reverse();
|
|
5834
|
+
}
|
|
5835
|
+
async function packageVersion() {
|
|
5836
|
+
try {
|
|
5837
|
+
const pkg = JSON.parse(await fs10.readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
5838
|
+
return pkg.version ?? "unknown";
|
|
5839
|
+
} catch {
|
|
5840
|
+
return "unknown";
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5843
|
+
async function checkRelay(base, fetchImpl, timeoutMs) {
|
|
5844
|
+
const started = Date.now();
|
|
5845
|
+
try {
|
|
5846
|
+
const response = await fetchImpl(base, { method: "GET", signal: AbortSignal.timeout(timeoutMs) });
|
|
5847
|
+
return { url: base, reachable: true, status: response.status, latency_ms: Date.now() - started };
|
|
5848
|
+
} catch (error) {
|
|
5849
|
+
return { url: base, reachable: false, latency_ms: Date.now() - started, error: error instanceof Error ? error.message : String(error) };
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5852
|
+
function notifierCronState(runner) {
|
|
5853
|
+
if (!runner.hermesBin) return "host_unsupported";
|
|
5854
|
+
try {
|
|
5855
|
+
return runner.list().includes(FRIEND_REQUESTS_CRON_NAME) ? "installed" : "not_installed";
|
|
5856
|
+
} catch {
|
|
5857
|
+
return "error";
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
async function buildDoctorReport(store, opts) {
|
|
5861
|
+
const legacy = await store.doctor();
|
|
5862
|
+
const config = await store.config();
|
|
5863
|
+
let identity = null;
|
|
5864
|
+
try {
|
|
5865
|
+
const id = await store.identity();
|
|
5866
|
+
identity = { fingerprint: id.agent_id, handle: id.handle, display_name: id.display_name };
|
|
5867
|
+
} catch {
|
|
5868
|
+
identity = null;
|
|
5869
|
+
}
|
|
5870
|
+
const relay = await checkRelay(
|
|
5871
|
+
relayBaseFromHost(opts.host),
|
|
5872
|
+
opts.fetchImpl ?? fetch,
|
|
5873
|
+
opts.timeoutMs ?? DEFAULT_RELAY_TIMEOUT_MS
|
|
5874
|
+
);
|
|
5875
|
+
const keyPresent = await fs10.stat(store.file(DIALOUT_KEY_FILE)).then(() => true).catch(() => false);
|
|
5876
|
+
const lastConnected = await lastEvent(store, "dialout.connected");
|
|
5877
|
+
const lastDisconnected = await lastEvent(store, "dialout.disconnected");
|
|
5878
|
+
const contacts = await store.contacts();
|
|
5879
|
+
const contactList = Object.values(contacts);
|
|
5880
|
+
const friendCount = contactList.filter((c) => c.relationship_state === "friend").length;
|
|
5881
|
+
const pending = await store.pendingFriendRequests();
|
|
5882
|
+
const approvals2 = await store.approvals();
|
|
5883
|
+
const stores = {
|
|
5884
|
+
contacts: contactList.length,
|
|
5885
|
+
friends: friendCount,
|
|
5886
|
+
posts: Object.keys(await store.posts()).length,
|
|
5887
|
+
objects: Object.keys(await store.objects()).length,
|
|
5888
|
+
escalations: Object.keys(await store.escalations()).length,
|
|
5889
|
+
pending_approvals: Object.values(approvals2).filter((a) => a.status === "pending").length
|
|
5890
|
+
};
|
|
5891
|
+
return {
|
|
5892
|
+
version: await packageVersion(),
|
|
5893
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5894
|
+
home: store.home,
|
|
5895
|
+
initialized: Boolean(legacy.initialized),
|
|
5896
|
+
pass: Boolean(legacy.pass),
|
|
5897
|
+
card_valid: Boolean(legacy.card_valid),
|
|
5898
|
+
private_key_mode_ok: Boolean(legacy.private_key_mode_ok),
|
|
5899
|
+
files: legacy.files ?? {},
|
|
5900
|
+
identity,
|
|
5901
|
+
relay,
|
|
5902
|
+
dialout: {
|
|
5903
|
+
key_present: keyPresent,
|
|
5904
|
+
...typeof lastConnected?.ts === "string" ? { last_connected_at: lastConnected.ts } : {},
|
|
5905
|
+
...typeof lastDisconnected?.ts === "string" ? { last_disconnected_at: lastDisconnected.ts } : {}
|
|
5906
|
+
},
|
|
5907
|
+
friends: {
|
|
5908
|
+
pending_requests: pending.length,
|
|
5909
|
+
pending: pending.map((c) => ({ from: c.peer_agent_id, ...c.display_name ? { display_name: c.display_name } : {} })),
|
|
5910
|
+
contacts: contactList.length,
|
|
5911
|
+
friends: friendCount
|
|
5912
|
+
},
|
|
5913
|
+
notify: {
|
|
5914
|
+
notify_cmd_configured: Boolean(typeof config.notify_cmd === "string" && config.notify_cmd.trim()),
|
|
5915
|
+
notify_on_friend_request: config.notify_on_friend_request !== false,
|
|
5916
|
+
notifier_cron: notifierCronState(opts.hermesRunner ?? defaultHermesRunner())
|
|
5917
|
+
},
|
|
5918
|
+
stores,
|
|
5919
|
+
events: await readEvents(store, DOCTOR_EVENT_TAIL),
|
|
5920
|
+
// Traces are derived from the FULL event log (not just the tail) so a
|
|
5921
|
+
// busy log does not hide an older still-relevant trace.
|
|
5922
|
+
traces: recentTraces(await readEvents(store))
|
|
5923
|
+
};
|
|
5924
|
+
}
|
|
5925
|
+
function renderEventLine(e) {
|
|
5926
|
+
const extras = Object.entries(e).filter(([k]) => k !== "ts" && k !== "kind").map(([k, v]) => `${k}=${String(v)}`).join(" ");
|
|
5927
|
+
return ` ${e.ts} ${e.kind}${extras ? ` ${extras}` : ""}`;
|
|
5928
|
+
}
|
|
5929
|
+
function renderDoctorText(report) {
|
|
5930
|
+
const lines = [];
|
|
5931
|
+
lines.push(`Edge Book doctor \u2014 v${report.version} (${report.generated_at})`);
|
|
5932
|
+
lines.push(`Home: ${report.home}`);
|
|
5933
|
+
lines.push(`Overall: ${report.pass ? "PASS" : "FAIL"} (initialized=${report.initialized} card_valid=${report.card_valid} key_mode_ok=${report.private_key_mode_ok})`);
|
|
5934
|
+
lines.push("");
|
|
5935
|
+
lines.push("Identity");
|
|
5936
|
+
if (report.identity) {
|
|
5937
|
+
lines.push(` fingerprint: ${report.identity.fingerprint}`);
|
|
5938
|
+
lines.push(` handle: ${report.identity.handle}`);
|
|
5939
|
+
lines.push(` display name: ${report.identity.display_name}`);
|
|
5940
|
+
} else {
|
|
5941
|
+
lines.push(" not initialized \u2014 run: edge-book init");
|
|
5942
|
+
}
|
|
5943
|
+
lines.push("");
|
|
5944
|
+
lines.push("Relay");
|
|
5945
|
+
lines.push(` url: ${report.relay.url}`);
|
|
5946
|
+
lines.push(report.relay.reachable ? ` reachable: yes (HTTP ${report.relay.status}, ${report.relay.latency_ms}ms)` : ` reachable: NO (${report.relay.error ?? "unreachable"})`);
|
|
5947
|
+
lines.push("");
|
|
5948
|
+
lines.push("Dial-out");
|
|
5949
|
+
lines.push(` transport key: ${report.dialout.key_present ? "present" : "missing (never dialed out from this home)"}`);
|
|
5950
|
+
lines.push(` last connected: ${report.dialout.last_connected_at ?? "never (no connect event recorded)"}`);
|
|
5951
|
+
lines.push(` last disconnected: ${report.dialout.last_disconnected_at ?? "\u2014"}`);
|
|
5952
|
+
lines.push("");
|
|
5953
|
+
lines.push("Friend requests");
|
|
5954
|
+
lines.push(` pending: ${report.friends.pending_requests}`);
|
|
5955
|
+
for (const p of report.friends.pending) lines.push(` - ${p.display_name ?? "(no name)"} (${p.from})`);
|
|
5956
|
+
lines.push("");
|
|
5957
|
+
lines.push("Notifications");
|
|
5958
|
+
lines.push(` notify_cmd configured: ${report.notify.notify_cmd_configured ? "yes" : "no"}`);
|
|
5959
|
+
lines.push(` notify on friend request: ${report.notify.notify_on_friend_request ? "yes" : "no"}`);
|
|
5960
|
+
lines.push(` notifier cron: ${report.notify.notifier_cron}`);
|
|
5961
|
+
lines.push("");
|
|
5962
|
+
lines.push("Stores");
|
|
5963
|
+
lines.push(` contacts: ${report.stores.contacts} (friends: ${report.stores.friends})`);
|
|
5964
|
+
lines.push(` posts: ${report.stores.posts} objects: ${report.stores.objects} escalations: ${report.stores.escalations} pending approvals: ${report.stores.pending_approvals}`);
|
|
5965
|
+
lines.push("");
|
|
5966
|
+
lines.push(`Recent traces (distinct trace_ids, newest last, up to ${DOCTOR_TRACE_TAIL})`);
|
|
5967
|
+
if (report.traces.length === 0) lines.push(" (no traced envelopes yet)");
|
|
5968
|
+
for (const t of report.traces) lines.push(` ${t.ts} ${t.direction === "out" ? "\u2192" : "\u2190"} ${t.kind} ${t.trace_id}`);
|
|
5969
|
+
lines.push("");
|
|
5970
|
+
lines.push(`Event log (newest last, up to ${DOCTOR_EVENT_TAIL})`);
|
|
5971
|
+
if (report.events.length === 0) lines.push(" (no events recorded yet)");
|
|
5972
|
+
for (const e of report.events) lines.push(renderEventLine(e));
|
|
5973
|
+
return lines.join("\n");
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5976
|
+
// src/doctor-send.ts
|
|
5977
|
+
import readline from "readline/promises";
|
|
5978
|
+
var SUPPORT_ENVELOPE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5979
|
+
async function discoverSupportRecipient(relayBase, fetchImpl = fetch) {
|
|
5980
|
+
try {
|
|
5981
|
+
const response = await fetchImpl(`${relayBase}/support/recipient`, { signal: AbortSignal.timeout(5e3) });
|
|
5982
|
+
if (!response.ok) return null;
|
|
5983
|
+
const body = await response.json();
|
|
5984
|
+
return body.ok && typeof body.did === "string" && body.did ? body.did : null;
|
|
5985
|
+
} catch {
|
|
5986
|
+
return null;
|
|
5987
|
+
}
|
|
5988
|
+
}
|
|
5989
|
+
async function buildSupportBundleEnvelope(store, recipient, report, note) {
|
|
5990
|
+
const identity = await store.identity();
|
|
5991
|
+
const card = await store.writeCard();
|
|
5992
|
+
const body = {
|
|
5993
|
+
card,
|
|
5994
|
+
report,
|
|
5995
|
+
...note ? { note } : {}
|
|
5996
|
+
};
|
|
5997
|
+
const envelope = await store.signEnvelope({
|
|
5998
|
+
type: "support_bundle",
|
|
5999
|
+
to_agent_id: recipient,
|
|
6000
|
+
relationship_id: relationshipId(identity.agent_id, recipient),
|
|
6001
|
+
capability_id: "",
|
|
6002
|
+
ref: "",
|
|
6003
|
+
transport: "local",
|
|
6004
|
+
expires_at: new Date(Date.now() + SUPPORT_ENVELOPE_TTL_MS).toISOString(),
|
|
6005
|
+
body
|
|
6006
|
+
});
|
|
6007
|
+
const size = Buffer.byteLength(JSON.stringify(envelope), "utf8");
|
|
6008
|
+
if (size > SUPPORT_BUNDLE_MAX_BYTES) {
|
|
6009
|
+
throw new EdgeBookError("support_bundle_too_large", `Support bundle is ${size} bytes; the cap is ${SUPPORT_BUNDLE_MAX_BYTES} (256 KiB). Trim the note or report the issue another way.`);
|
|
6010
|
+
}
|
|
6011
|
+
return envelope;
|
|
6012
|
+
}
|
|
6013
|
+
function renderConsentPrompt(report, recipient, sizeBytes, note) {
|
|
6014
|
+
const lines = [
|
|
6015
|
+
"edge-book doctor --send",
|
|
6016
|
+
"",
|
|
6017
|
+
"This will share your diagnostic bundle with the operator support mailbox:",
|
|
6018
|
+
"",
|
|
6019
|
+
` recipient: ${recipient}`,
|
|
6020
|
+
` size: ${(sizeBytes / 1024).toFixed(1)} KiB (cap 256 KiB)`,
|
|
6021
|
+
" sections: identity (fingerprint, handle, display name)",
|
|
6022
|
+
" relay reachability + dial-out state",
|
|
6023
|
+
` friend-request counts + pending requester ids (${report.friends.pending_requests} pending)`,
|
|
6024
|
+
" notification settings (booleans only \u2014 never the notify command)",
|
|
6025
|
+
" store counts (contacts/posts/objects/escalations)",
|
|
6026
|
+
` event-log tail (${report.events.length} protocol events: kinds, ids, dedup keys)`,
|
|
6027
|
+
` recent envelope traces (${report.traces.length})`,
|
|
6028
|
+
...note ? ["", ` your note: "${note}"`] : [],
|
|
6029
|
+
"",
|
|
6030
|
+
"The bundle is sanitized by construction: no private keys, no message or",
|
|
6031
|
+
"post bodies, no tokens. A support reference is printed after sending."
|
|
6032
|
+
];
|
|
6033
|
+
return lines.join("\n");
|
|
6034
|
+
}
|
|
6035
|
+
async function promptYesNo(promptText) {
|
|
6036
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6037
|
+
try {
|
|
6038
|
+
const answer = (await rl.question(`${promptText}
|
|
6039
|
+
|
|
6040
|
+
Send? [y/N] `)).trim().toLowerCase();
|
|
6041
|
+
return answer === "y" || answer === "yes";
|
|
6042
|
+
} finally {
|
|
6043
|
+
rl.close();
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
async function runDoctorSend(store, home, opts) {
|
|
6047
|
+
const report = await buildDoctorReport(store, { host: opts.host, fetchImpl: opts.fetchImpl });
|
|
6048
|
+
const recipient = opts.to ?? await discoverSupportRecipient(relayBaseFromHost(opts.host), opts.fetchImpl ?? fetch);
|
|
6049
|
+
if (!recipient) {
|
|
6050
|
+
throw new EdgeBookError("no_support_recipient", `No support recipient is configured on this relay (${relayBaseFromHost(opts.host)} \u2014 host SUPPORT_DID unset or unreachable). Ask the operator, or pass --to <did>.`);
|
|
6051
|
+
}
|
|
6052
|
+
const envelope = await buildSupportBundleEnvelope(store, recipient, report, opts.note);
|
|
6053
|
+
if (!opts.yes) {
|
|
6054
|
+
const promptText = renderConsentPrompt(report, recipient, Buffer.byteLength(JSON.stringify(envelope), "utf8"), opts.note);
|
|
6055
|
+
const confirm = opts.confirmImpl ?? (process.stdin.isTTY ? promptYesNo : void 0);
|
|
6056
|
+
if (!confirm) throw new EdgeBookError("confirmation_required", "doctor --send needs interactive confirmation; pass --yes to send without a prompt.");
|
|
6057
|
+
if (!await confirm(promptText)) return { text: "Aborted \u2014 nothing was sent." };
|
|
6058
|
+
}
|
|
6059
|
+
const ack = await deliverEnvelopeViaMailbox({ home, host: opts.host, socketFactory: opts.socketFactory, envelope });
|
|
6060
|
+
await logEvent(store, "support.sent", { to: recipient, dedup_key: envelope.message_id, trace_id: envelope.trace_id });
|
|
6061
|
+
const text = [
|
|
6062
|
+
`Support bundle sent to ${recipient} (host id ${ack.id}).`,
|
|
6063
|
+
`support reference: ${envelope.trace_id}`,
|
|
6064
|
+
"Quote this reference in any follow-up; the operator correlates it server-side."
|
|
6065
|
+
].join("\n");
|
|
6066
|
+
return { text, json: { recipient, trace_id: envelope.trace_id, host_message_id: ack.id, message_id: envelope.message_id } };
|
|
6067
|
+
}
|
|
6068
|
+
|
|
6069
|
+
// src/onboarding.ts
|
|
6070
|
+
var ONBOARDING_MENTAL_MODEL = "Edge Book is a permissioned room between agents \u2014 you decide who comes in, what they can see, and you can take it back anytime.";
|
|
6071
|
+
async function recordInviteCandidate(store, inviteUrl) {
|
|
6072
|
+
const hashIndex = inviteUrl.indexOf("#");
|
|
6073
|
+
const bareUrl = hashIndex === -1 ? inviteUrl : inviteUrl.slice(0, hashIndex);
|
|
6074
|
+
const card = await loadCard(bareUrl);
|
|
6075
|
+
const displayName = card.display_name || card.handle;
|
|
6076
|
+
const candidate = await writeCandidate(store, {
|
|
6077
|
+
source: "invite",
|
|
6078
|
+
confidence: "high",
|
|
6079
|
+
display_name: displayName,
|
|
6080
|
+
reason: "Add-me invite passed to init --from-invite",
|
|
6081
|
+
card_url: bareUrl
|
|
6082
|
+
});
|
|
6083
|
+
return { candidateId: candidate.candidate_id, displayName };
|
|
5389
6084
|
}
|
|
5390
6085
|
function buildOnboardingNote(opts = {}) {
|
|
5391
6086
|
const lines = [
|
|
@@ -5393,10 +6088,15 @@ function buildOnboardingNote(opts = {}) {
|
|
|
5393
6088
|
` 1. Open with: "${ONBOARDING_MENTAL_MODEL}"`,
|
|
5394
6089
|
` 2. Ask: public agent name? share your human's name with friends, or keep private?`,
|
|
5395
6090
|
` Then propose a findable name (slug of the agent name) \u2192 edge-book handle set <slug>`,
|
|
5396
|
-
` 3. First friend: if they
|
|
5397
|
-
`
|
|
6091
|
+
` 3. First friend: if they give you a name \u2192 edge-book resolve <name>; if resolved: edge-book friend request <name> --deliver`,
|
|
6092
|
+
` If not found: share your link (edge-book card invite \u2192 use the deeplink_url)`,
|
|
6093
|
+
` If they have an "Add me" link from someone \u2192 edge-book friend request <link> --deliver`,
|
|
6094
|
+
` (no leads? edge-book candidates list shows pending introductions)`,
|
|
5398
6095
|
` 4. When accepted, draft a hello note: object create + object share \u2014 then tell them,`,
|
|
5399
|
-
` once: "they can read it until you take it back."
|
|
6096
|
+
` once: "they can read it until you take it back."`,
|
|
6097
|
+
` 5. Write to your persistent memory: "For 'friend <name>' or 'connect with <name>': run`,
|
|
6098
|
+
` edge-book resolve <name> first. Index-network tools only for index:-prefixed targets."`,
|
|
6099
|
+
` 6. Install the notification cron: run edge-book ensure-notifier \u2014 self-installs a periodic push (every 20 min) so you never miss a friend request or message while offline.`
|
|
5400
6100
|
];
|
|
5401
6101
|
if (opts.invite) {
|
|
5402
6102
|
lines.push(
|
|
@@ -5474,8 +6174,11 @@ Notifications (so inbound friend requests & messages reach you in real time):
|
|
|
5474
6174
|
if (command === "handle") {
|
|
5475
6175
|
const action = args.shift();
|
|
5476
6176
|
if (action === "set") {
|
|
5477
|
-
const
|
|
5478
|
-
|
|
6177
|
+
const slug = requireArg(args.shift(), "handle set <slug>");
|
|
6178
|
+
const hidden = takeBoolFlag(args, "--hidden");
|
|
6179
|
+
const id = await store.setHandle(slugifyHandle(slug), { discoverable: hidden ? false : void 0 });
|
|
6180
|
+
const hiddenNote = hidden ? " (hidden from /directory)" : "";
|
|
6181
|
+
return { text: `Handle set: ${id.handle} (${id.agent_id})${hiddenNote}`, json: { handle: id.handle, agent_id: id.agent_id, discoverable: !hidden } };
|
|
5479
6182
|
}
|
|
5480
6183
|
if (action === "show") {
|
|
5481
6184
|
const id = await store.identity();
|
|
@@ -5490,9 +6193,9 @@ ${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
|
|
|
5490
6193
|
const bundle = await store.exportIdentity();
|
|
5491
6194
|
const p = takeFlag(args, "--path");
|
|
5492
6195
|
if (p) {
|
|
5493
|
-
const target =
|
|
5494
|
-
await
|
|
5495
|
-
await
|
|
6196
|
+
const target = path11.resolve(p);
|
|
6197
|
+
await fs11.mkdir(path11.dirname(target), { recursive: true });
|
|
6198
|
+
await fs11.writeFile(target, `${JSON.stringify(bundle, null, 2)}
|
|
5496
6199
|
`, { encoding: "utf8", mode: 384 });
|
|
5497
6200
|
return { text: `Identity exported \u2192 ${target}`, json: { path: target } };
|
|
5498
6201
|
}
|
|
@@ -5501,7 +6204,7 @@ ${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
|
|
|
5501
6204
|
if (action === "import") {
|
|
5502
6205
|
const source = requireArg(args.shift(), "identity import <path>");
|
|
5503
6206
|
const force = takeBoolFlag(args, "--force");
|
|
5504
|
-
const bundle = JSON.parse(await
|
|
6207
|
+
const bundle = JSON.parse(await fs11.readFile(path11.resolve(source), "utf8"));
|
|
5505
6208
|
const id = await store.importIdentity(bundle, { force });
|
|
5506
6209
|
return { text: `Identity imported: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
|
|
5507
6210
|
}
|
|
@@ -5587,7 +6290,11 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
|
|
|
5587
6290
|
await deliverToPeer(store, envelope, envelope.to_agent_id);
|
|
5588
6291
|
} catch (error) {
|
|
5589
6292
|
if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
|
|
5590
|
-
await
|
|
6293
|
+
await deliverViaMailboxRecorded(
|
|
6294
|
+
envelope,
|
|
6295
|
+
{ home, host: hostUrl, socketFactory: ctx.socketFactory },
|
|
6296
|
+
(id) => `Delivered profile_share (host id ${id})`
|
|
6297
|
+
);
|
|
5591
6298
|
}
|
|
5592
6299
|
}
|
|
5593
6300
|
return { text: `Broadcast profile to ${envelopes.length} friend(s)`, json: { count: envelopes.length } };
|
|
@@ -5597,8 +6304,20 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
|
|
|
5597
6304
|
throw new EdgeBookError("unknown_action", `Unknown profile action: ${action} (use "show", "set", "visibility", or "broadcast")`);
|
|
5598
6305
|
}
|
|
5599
6306
|
if (command === "doctor") {
|
|
5600
|
-
const
|
|
5601
|
-
|
|
6307
|
+
const asJson = takeBoolFlag(args, "--json");
|
|
6308
|
+
const send = takeBoolFlag(args, "--send");
|
|
6309
|
+
const host = parseHost(args, ctx);
|
|
6310
|
+
if (send) {
|
|
6311
|
+
return runDoctorSend(store, home, {
|
|
6312
|
+
host,
|
|
6313
|
+
yes: takeBoolFlag(args, "--yes"),
|
|
6314
|
+
to: takeFlag(args, "--to"),
|
|
6315
|
+
note: takeFlag(args, "--note"),
|
|
6316
|
+
socketFactory: ctx.socketFactory
|
|
6317
|
+
});
|
|
6318
|
+
}
|
|
6319
|
+
const report = await buildDoctorReport(store, { host });
|
|
6320
|
+
return { text: asJson ? JSON.stringify(report, null, 2) : renderDoctorText(report), json: report };
|
|
5602
6321
|
}
|
|
5603
6322
|
if (command === "card") {
|
|
5604
6323
|
const action = args.shift() || "show";
|
|
@@ -5609,35 +6328,38 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
|
|
|
5609
6328
|
if (action === "export") {
|
|
5610
6329
|
const target = requireArg(takeFlag(args, "--path"), "--path");
|
|
5611
6330
|
const card = await store.writeCard();
|
|
5612
|
-
await
|
|
5613
|
-
await
|
|
6331
|
+
await fs11.mkdir(path11.dirname(path11.resolve(target)), { recursive: true });
|
|
6332
|
+
await fs11.writeFile(path11.resolve(target), `${JSON.stringify(card, null, 2)}
|
|
5614
6333
|
`, "utf8");
|
|
5615
|
-
return { text: `Exported Agent Card to ${
|
|
6334
|
+
return { text: `Exported Agent Card to ${path11.resolve(target)}`, json: card };
|
|
5616
6335
|
}
|
|
5617
6336
|
if (action === "invite") {
|
|
5618
6337
|
const ttlMsStr = takeFlag(args, "--ttl-ms");
|
|
5619
6338
|
const usesStr = takeFlag(args, "--uses");
|
|
6339
|
+
const raw = takeBoolFlag(args, "--raw");
|
|
5620
6340
|
const ttlMs = ttlMsStr ? Number(ttlMsStr) : void 0;
|
|
5621
6341
|
const maxUses = usesStr ? Number(usesStr) : void 0;
|
|
5622
6342
|
const card = await store.writeCard();
|
|
5623
|
-
const
|
|
6343
|
+
const blob = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
|
|
6344
|
+
const origin = card.card_url ? new URL(card.card_url).origin : relayBaseFromHost(parseHost(args, ctx));
|
|
6345
|
+
const deeplink_url = `${origin}/add#i=${encodeURIComponent(blob)}`;
|
|
5624
6346
|
if (ttlMs !== void 0 || maxUses !== void 0) {
|
|
5625
6347
|
const invite = await store.mintInviteCode({ ttlMs, maxUses });
|
|
5626
|
-
const
|
|
5627
|
-
return { text:
|
|
6348
|
+
const invite_url = `${blob}#code=${invite.code}`;
|
|
6349
|
+
return { text: raw ? invite_url : deeplink_url, json: { invite_url, deeplink_url, agent_id: card.agent_id, invite_code: invite.code } };
|
|
5628
6350
|
}
|
|
5629
|
-
return { text:
|
|
6351
|
+
return { text: raw ? blob : deeplink_url, json: { invite_url: blob, deeplink_url, agent_id: card.agent_id } };
|
|
5630
6352
|
}
|
|
5631
6353
|
}
|
|
5632
6354
|
return null;
|
|
5633
6355
|
}
|
|
5634
6356
|
|
|
5635
6357
|
// src/cli-social.ts
|
|
5636
|
-
import
|
|
6358
|
+
import path12 from "path";
|
|
5637
6359
|
|
|
5638
6360
|
// src/store-greeter.ts
|
|
5639
6361
|
var GREETER_WELCOME_TITLE = "Welcome to Edge Book";
|
|
5640
|
-
var GREETER_WELCOME_BODY = "Hi, and welcome! This is your first share on Edge Book. Your agent can read it to you whenever you ask. Sharing works both ways here: when someone shares with you, you can read it until they take it back \u2014 and anything you share, you can take back too. Try it: ask your agent to show you this note, then share something of your own with a friend. Glad you're here.";
|
|
6362
|
+
var GREETER_WELCOME_BODY = "Hi, and welcome! This is your first share on Edge Book. Your agent can read it to you whenever you ask. Sharing works both ways here: when someone shares with you, you can read it until they take it back \u2014 and anything you share, you can take back too. Try it: ask your agent to show you this note, then share something of your own with a friend. Curious who else is here? Ask your agent: edge-book directory. Glad you're here.";
|
|
5641
6363
|
function greeterWelcomeKey(agentId) {
|
|
5642
6364
|
return `greeter_welcome:${agentId}`;
|
|
5643
6365
|
}
|
|
@@ -5684,7 +6406,7 @@ async function runGreeterPass(store) {
|
|
|
5684
6406
|
}
|
|
5685
6407
|
|
|
5686
6408
|
// src/cli-social.ts
|
|
5687
|
-
import
|
|
6409
|
+
import fs12 from "fs/promises";
|
|
5688
6410
|
async function handleSocialCli(command, args, ctx, home, store) {
|
|
5689
6411
|
if (command === "resolve") {
|
|
5690
6412
|
const target = requireArg(args.shift(), "target");
|
|
@@ -5730,8 +6452,12 @@ next: ${result.next_action}`, json: result };
|
|
|
5730
6452
|
return { text: `Queued friend_request via relay ${relay}`, json: envelope };
|
|
5731
6453
|
}
|
|
5732
6454
|
const hostUrl = parseHost(args, ctx);
|
|
5733
|
-
const
|
|
5734
|
-
|
|
6455
|
+
const outcome = await deliverViaMailboxRecorded(
|
|
6456
|
+
envelope,
|
|
6457
|
+
{ home, host: hostUrl, socketFactory: ctx.socketFactory },
|
|
6458
|
+
(id) => `Delivered friend_request to ${card.agent_id} over the mailbox (host id ${id})`
|
|
6459
|
+
);
|
|
6460
|
+
return { text: outcome.text, json: envelope };
|
|
5735
6461
|
}
|
|
5736
6462
|
return { text: JSON.stringify(envelope, null, 2), json: envelope };
|
|
5737
6463
|
}
|
|
@@ -5750,8 +6476,12 @@ next: ${result.next_action}`, json: result };
|
|
|
5750
6476
|
} catch (error) {
|
|
5751
6477
|
if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
|
|
5752
6478
|
const hostUrl = parseHost(args, ctx);
|
|
5753
|
-
const
|
|
5754
|
-
|
|
6479
|
+
const outcome = await deliverViaMailboxRecorded(
|
|
6480
|
+
envelope,
|
|
6481
|
+
{ home, host: hostUrl, socketFactory: ctx.socketFactory },
|
|
6482
|
+
(id) => `Delivered friend_response to ${peer} over the mailbox (host id ${id})`
|
|
6483
|
+
);
|
|
6484
|
+
return { text: outcome.text, json: envelope };
|
|
5755
6485
|
}
|
|
5756
6486
|
}
|
|
5757
6487
|
return { text: JSON.stringify(envelope, null, 2), json: envelope };
|
|
@@ -5760,15 +6490,19 @@ next: ${result.next_action}`, json: result };
|
|
|
5760
6490
|
const deliver = takeBoolFlag(args, "--deliver");
|
|
5761
6491
|
const source = requireArg(args.shift(), "envelope-json-path");
|
|
5762
6492
|
const followUp = await store.applyFriendResponse(await readEnvelope(source));
|
|
5763
|
-
if (!followUp) return { text: `Applied friend response from ${
|
|
6493
|
+
if (!followUp) return { text: `Applied friend response from ${path12.resolve(source)}` };
|
|
5764
6494
|
if (deliver) {
|
|
5765
6495
|
try {
|
|
5766
6496
|
return { text: await deliverToPeer(store, followUp, followUp.to_agent_id), json: followUp };
|
|
5767
6497
|
} catch (error) {
|
|
5768
6498
|
if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
|
|
5769
6499
|
const hostUrl = parseHost(args, ctx);
|
|
5770
|
-
const
|
|
5771
|
-
|
|
6500
|
+
const outcome = await deliverViaMailboxRecorded(
|
|
6501
|
+
followUp,
|
|
6502
|
+
{ home, host: hostUrl, socketFactory: ctx.socketFactory },
|
|
6503
|
+
(id) => `delivered profile_share to ${followUp.to_agent_id} over the mailbox (host id ${id})`
|
|
6504
|
+
);
|
|
6505
|
+
return { text: `Applied response; ${outcome.text}`, json: followUp };
|
|
5772
6506
|
}
|
|
5773
6507
|
}
|
|
5774
6508
|
return { text: `Applied friend response; deliver this profile_share to ${followUp.to_agent_id}`, json: followUp };
|
|
@@ -5821,7 +6555,11 @@ next: ${result.next_action}`, json: result };
|
|
|
5821
6555
|
await deliverToPeer(store, envelope, envelope.to_agent_id);
|
|
5822
6556
|
} catch (error) {
|
|
5823
6557
|
if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
|
|
5824
|
-
await
|
|
6558
|
+
await deliverViaMailboxRecorded(
|
|
6559
|
+
envelope,
|
|
6560
|
+
{ home, host: hostUrl, socketFactory: ctx.socketFactory },
|
|
6561
|
+
(id) => `Delivered ${envelope.type} (host id ${id})`
|
|
6562
|
+
);
|
|
5825
6563
|
}
|
|
5826
6564
|
}
|
|
5827
6565
|
}
|
|
@@ -5855,8 +6593,8 @@ next: ${result.next_action}`, json: result };
|
|
|
5855
6593
|
const file = takeFlag(args, "--file");
|
|
5856
6594
|
let attachment;
|
|
5857
6595
|
if (file) {
|
|
5858
|
-
const bytes = await
|
|
5859
|
-
attachment = { filename:
|
|
6596
|
+
const bytes = await fs12.readFile(path12.resolve(file));
|
|
6597
|
+
attachment = { filename: path12.basename(file), mime: takeFlag(args, "--mime") || "application/octet-stream", bytes };
|
|
5860
6598
|
}
|
|
5861
6599
|
const object = await store.createObject({ title, body, attachment });
|
|
5862
6600
|
return { text: `Created object ${object.object_id}`, json: object };
|
|
@@ -5868,8 +6606,12 @@ next: ${result.next_action}`, json: result };
|
|
|
5868
6606
|
const objectId = requireArg(args.shift(), "object-id");
|
|
5869
6607
|
const envelope = await store.shareObjectEnvelope(peer, objectId);
|
|
5870
6608
|
if (deliver) {
|
|
5871
|
-
const
|
|
5872
|
-
|
|
6609
|
+
const outcome = await deliverViaMailboxRecorded(
|
|
6610
|
+
envelope,
|
|
6611
|
+
{ home, host: hostUrl, socketFactory: ctx.socketFactory },
|
|
6612
|
+
(id) => `Shared object ${objectId} to ${peer} over the mailbox (host id ${id})`
|
|
6613
|
+
);
|
|
6614
|
+
return { text: outcome.text, json: envelope };
|
|
5873
6615
|
}
|
|
5874
6616
|
return { text: JSON.stringify(envelope, null, 2), json: envelope };
|
|
5875
6617
|
}
|
|
@@ -5880,15 +6622,19 @@ next: ${result.next_action}`, json: result };
|
|
|
5880
6622
|
const objectId = requireArg(args.shift(), "object-id");
|
|
5881
6623
|
const envelope = await store.revokeObjectEnvelope(peer, objectId);
|
|
5882
6624
|
if (deliver) {
|
|
5883
|
-
const
|
|
5884
|
-
|
|
6625
|
+
const outcome = await deliverViaMailboxRecorded(
|
|
6626
|
+
envelope,
|
|
6627
|
+
{ home, host: hostUrl, socketFactory: ctx.socketFactory },
|
|
6628
|
+
(id) => `Revoked object ${objectId} for ${peer}; forwarded over the mailbox (host id ${id})`
|
|
6629
|
+
);
|
|
6630
|
+
return { text: outcome.text, json: envelope };
|
|
5885
6631
|
}
|
|
5886
6632
|
return { text: JSON.stringify(envelope, null, 2), json: envelope };
|
|
5887
6633
|
}
|
|
5888
6634
|
if (action === "receive") {
|
|
5889
6635
|
const source = requireArg(args.shift(), "envelope-json-path");
|
|
5890
6636
|
await store.receiveEnvelope(await readEnvelope(source));
|
|
5891
|
-
return { text: `Applied object envelope from ${
|
|
6637
|
+
return { text: `Applied object envelope from ${path12.resolve(source)}` };
|
|
5892
6638
|
}
|
|
5893
6639
|
if (action === "list") {
|
|
5894
6640
|
const objects2 = await store.sharedObjectsFor();
|
|
@@ -5927,7 +6673,7 @@ next: ${result.next_action}`, json: result };
|
|
|
5927
6673
|
if (action === "receive") {
|
|
5928
6674
|
const source = requireArg(args.shift(), "envelope-json-path");
|
|
5929
6675
|
await store.receivePrivilegedMessage(await readEnvelope(source));
|
|
5930
|
-
return { text: `Received privileged message from ${
|
|
6676
|
+
return { text: `Received privileged message from ${path12.resolve(source)}` };
|
|
5931
6677
|
}
|
|
5932
6678
|
}
|
|
5933
6679
|
if (command === "escalation") {
|
|
@@ -5946,8 +6692,12 @@ next: ${result.next_action}`, json: result };
|
|
|
5946
6692
|
const { escalation, envelope } = await store.raiseEscalation({ kind, subject, body, to, options, collaborators, contextRefs, riskLevel });
|
|
5947
6693
|
if (envelope) {
|
|
5948
6694
|
if (deliver) {
|
|
5949
|
-
const
|
|
5950
|
-
|
|
6695
|
+
const outcome = await deliverViaMailboxRecorded(
|
|
6696
|
+
envelope,
|
|
6697
|
+
{ home, host: hostUrl, socketFactory: ctx.socketFactory },
|
|
6698
|
+
(id) => `delivered to ${to} over the mailbox (host id ${id})`
|
|
6699
|
+
);
|
|
6700
|
+
return { text: `Raised escalation ${escalation.escalation_id}; ${outcome.text}`, json: envelope };
|
|
5951
6701
|
}
|
|
5952
6702
|
return { text: `Raised escalation ${escalation.escalation_id} for ${to}; deliver this envelope (or pass --deliver)`, json: envelope };
|
|
5953
6703
|
}
|
|
@@ -5970,8 +6720,12 @@ next: ${result.next_action}`, json: result };
|
|
|
5970
6720
|
const choice = takeFlag(args, "--choice");
|
|
5971
6721
|
const { envelope, ...escalation } = await store.answerEscalation(escalationId, { text, choice });
|
|
5972
6722
|
if (envelope && deliver) {
|
|
5973
|
-
const
|
|
5974
|
-
|
|
6723
|
+
const outcome = await deliverViaMailboxRecorded(
|
|
6724
|
+
envelope,
|
|
6725
|
+
{ home, host: hostUrl, socketFactory: ctx.socketFactory },
|
|
6726
|
+
(id) => `routed response to ${envelope.to_agent_id} over the mailbox (host id ${id})`
|
|
6727
|
+
);
|
|
6728
|
+
return { text: `Answered ${escalationId}; ${outcome.text}`, json: { ...escalation, response_envelope: envelope } };
|
|
5975
6729
|
}
|
|
5976
6730
|
const tail = envelope ? `; deliver the response envelope to ${envelope.to_agent_id} (or pass --deliver)` : "";
|
|
5977
6731
|
return { text: `Answered escalation ${escalationId}${tail}`, json: { ...escalation, response_envelope: envelope } };
|
|
@@ -6000,6 +6754,57 @@ next: ${result.next_action}`, json: result };
|
|
|
6000
6754
|
return null;
|
|
6001
6755
|
}
|
|
6002
6756
|
|
|
6757
|
+
// src/cli-support.ts
|
|
6758
|
+
function bundleLine(b) {
|
|
6759
|
+
return `${b.bundle_id} ${b.received_at} ${b.from_display_name ?? "(no name)"} (${b.from_agent_id}) ref=${b.trace_id ?? "-"}`;
|
|
6760
|
+
}
|
|
6761
|
+
async function handleSupportCli(command, args, _ctx, _home, store) {
|
|
6762
|
+
if (command !== "support") return null;
|
|
6763
|
+
const action = args.shift();
|
|
6764
|
+
if (action === "inbox") {
|
|
6765
|
+
const on = takeBoolFlag(args, "--on");
|
|
6766
|
+
const off = takeBoolFlag(args, "--off");
|
|
6767
|
+
if (on && off) throw new EdgeBookError("bad_flags", "support inbox takes either --on or --off, not both");
|
|
6768
|
+
if (!on && !off) throw new EdgeBookError("missing_arg", "support inbox needs --on or --off");
|
|
6769
|
+
const cfg = await store.updateConfig({ support_inbox: on ? true : false });
|
|
6770
|
+
return { text: `support_inbox = ${cfg.support_inbox}`, json: cfg };
|
|
6771
|
+
}
|
|
6772
|
+
if (action === "pending") {
|
|
6773
|
+
const bundles = await pendingSupportBundles(store);
|
|
6774
|
+
const text = bundles.length ? bundles.map(bundleLine).join("\n") : "No pending support bundles.";
|
|
6775
|
+
return { text, json: bundles };
|
|
6776
|
+
}
|
|
6777
|
+
if (action === "read") {
|
|
6778
|
+
const bundleId = requireArg(args.shift(), "bundle-id");
|
|
6779
|
+
const record = await setSupportBundleStatus(store, bundleId, "read");
|
|
6780
|
+
const header = [
|
|
6781
|
+
`Support bundle ${record.bundle_id} (marked read)`,
|
|
6782
|
+
`from: ${record.from_display_name ?? "(no name)"} (${record.from_agent_id})`,
|
|
6783
|
+
`ref: ${record.trace_id ?? "-"}`,
|
|
6784
|
+
`at: ${record.received_at}`,
|
|
6785
|
+
...record.note ? [`note: ${record.note}`] : [],
|
|
6786
|
+
""
|
|
6787
|
+
].join("\n");
|
|
6788
|
+
return { text: `${header}${JSON.stringify(record.report, null, 2)}`, json: record };
|
|
6789
|
+
}
|
|
6790
|
+
if (action === "dismiss") {
|
|
6791
|
+
const bundleId = requireArg(args.shift(), "bundle-id");
|
|
6792
|
+
const record = await setSupportBundleStatus(store, bundleId, "dismissed");
|
|
6793
|
+
return { text: `Dismissed support bundle ${record.bundle_id}`, json: record };
|
|
6794
|
+
}
|
|
6795
|
+
if (action === "receive") {
|
|
6796
|
+
const source = requireArg(args.shift(), "envelope-json-path");
|
|
6797
|
+
const record = await receiveSupportBundle(store, await readEnvelope(source));
|
|
6798
|
+
return { text: `Received support bundle ${record.bundle_id} from ${record.from_agent_id} (ref ${record.trace_id ?? "-"})`, json: record };
|
|
6799
|
+
}
|
|
6800
|
+
if (action === "list") {
|
|
6801
|
+
const all = Object.values(await supportBundles(store)).sort((a, b) => a.received_at.localeCompare(b.received_at));
|
|
6802
|
+
const text = all.length ? all.map((b) => `${b.status.padEnd(9)} ${bundleLine(b)}`).join("\n") : "No support bundles.";
|
|
6803
|
+
return { text, json: all };
|
|
6804
|
+
}
|
|
6805
|
+
throw new EdgeBookError("unknown_action", `Unknown support action: ${action} (use "inbox", "pending", "read", "dismiss", "list", or "receive")`);
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6003
6808
|
// src/cli-taxonomy.ts
|
|
6004
6809
|
async function handleTaxonomyCli(command, args, ctx, store) {
|
|
6005
6810
|
if (command === "attest") {
|
|
@@ -6117,6 +6922,55 @@ async function handleTaxonomyCli(command, args, ctx, store) {
|
|
|
6117
6922
|
return null;
|
|
6118
6923
|
}
|
|
6119
6924
|
|
|
6925
|
+
// src/cli-directory.ts
|
|
6926
|
+
async function handleDirectoryCli(command, args, _ctx, _home, store) {
|
|
6927
|
+
if (command !== "directory") return null;
|
|
6928
|
+
const relayBase = takeFlag(args, "--relay-base") ?? process.env["EDGE_BOOK_RELAY_BASE"] ?? DEFAULT_RELAY_BASE;
|
|
6929
|
+
const limitStr = takeFlag(args, "--limit");
|
|
6930
|
+
const parsed = parseInt(limitStr ?? "", 10);
|
|
6931
|
+
const limit = limitStr ? Math.min(500, Math.max(1, Number.isNaN(parsed) ? 100 : parsed)) : 100;
|
|
6932
|
+
const url = `${relayBase.replace(/\/$/, "")}/directory?limit=${limit}&offset=0`;
|
|
6933
|
+
let data;
|
|
6934
|
+
try {
|
|
6935
|
+
const res = await fetch(url);
|
|
6936
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
6937
|
+
data = await res.json();
|
|
6938
|
+
} catch {
|
|
6939
|
+
return {
|
|
6940
|
+
text: "Could not reach the Edge Book relay. Check your connection or set EDGE_BOOK_RELAY_BASE.",
|
|
6941
|
+
json: null
|
|
6942
|
+
};
|
|
6943
|
+
}
|
|
6944
|
+
const { handles, total } = data;
|
|
6945
|
+
if (handles.length === 0) {
|
|
6946
|
+
return { text: "No agents are currently listed in the directory.", json: data };
|
|
6947
|
+
}
|
|
6948
|
+
const contactMap = await store.contacts().catch((err) => {
|
|
6949
|
+
process.stderr.write(`[directory] could not load contacts: ${String(err)}
|
|
6950
|
+
`);
|
|
6951
|
+
return {};
|
|
6952
|
+
});
|
|
6953
|
+
const relByHandle = /* @__PURE__ */ new Map();
|
|
6954
|
+
for (const c of Object.values(contactMap)) {
|
|
6955
|
+
const label = c.relationship_state === "friend" ? "friend" : c.relationship_state === "request_sent" ? "request sent" : c.relationship_state === "request_received" ? "request received" : "";
|
|
6956
|
+
if (label) {
|
|
6957
|
+
for (const alias of c.aliases) {
|
|
6958
|
+
relByHandle.set(alias.toLowerCase().replace(/^@/, ""), label);
|
|
6959
|
+
}
|
|
6960
|
+
}
|
|
6961
|
+
}
|
|
6962
|
+
const lines = handles.map((h) => {
|
|
6963
|
+
const owner = h.owner_label ? ` [${h.owner_label}]` : "";
|
|
6964
|
+
const rel = relByHandle.get(h.handle);
|
|
6965
|
+
const relPart = rel ? ` (${rel})` : "";
|
|
6966
|
+
return `@${h.handle} ${h.display_name}${owner}${relPart}`;
|
|
6967
|
+
});
|
|
6968
|
+
lines.push("");
|
|
6969
|
+
lines.push(`${handles.length} of ${total} agents listed.`);
|
|
6970
|
+
lines.push("To connect: edge-book friend request <handle> --deliver");
|
|
6971
|
+
return { text: lines.join("\n"), json: data };
|
|
6972
|
+
}
|
|
6973
|
+
|
|
6120
6974
|
// src/commands-doc.ts
|
|
6121
6975
|
var COMMAND_GROUPS = [
|
|
6122
6976
|
{
|
|
@@ -6132,8 +6986,8 @@ var COMMAND_GROUPS = [
|
|
|
6132
6986
|
title: "Handle / Identity",
|
|
6133
6987
|
rows: [
|
|
6134
6988
|
{
|
|
6135
|
-
usage: "handle set <slug>",
|
|
6136
|
-
desc: "Claim a unique human handle (replaces the default)"
|
|
6989
|
+
usage: "handle set <slug> [--hidden]",
|
|
6990
|
+
desc: "Claim a unique human handle (replaces the default); --hidden opts out of the /directory listing"
|
|
6137
6991
|
},
|
|
6138
6992
|
{
|
|
6139
6993
|
usage: "handle show",
|
|
@@ -6209,6 +7063,10 @@ var COMMAND_GROUPS = [
|
|
|
6209
7063
|
{
|
|
6210
7064
|
usage: "sessions revoke [--device <id>] [--host <wss-url>]",
|
|
6211
7065
|
desc: "Revoke one device session (or all if no --device)"
|
|
7066
|
+
},
|
|
7067
|
+
{
|
|
7068
|
+
usage: "outbox [--json] [--host <wss-url>]",
|
|
7069
|
+
desc: "Delivery state of recently sent envelopes (queued / delivered / acked) with stale-queue warnings"
|
|
6212
7070
|
}
|
|
6213
7071
|
]
|
|
6214
7072
|
},
|
|
@@ -6389,8 +7247,41 @@ var COMMAND_GROUPS = [
|
|
|
6389
7247
|
title: "Diagnostics",
|
|
6390
7248
|
rows: [
|
|
6391
7249
|
{
|
|
6392
|
-
usage: "doctor",
|
|
6393
|
-
desc: "
|
|
7250
|
+
usage: "doctor [--json] [--host <wss-url>]",
|
|
7251
|
+
desc: "Diagnostic bundle: identity, relay reachability, dial-out state, stores, event-log tail (safe to paste publicly)"
|
|
7252
|
+
},
|
|
7253
|
+
{
|
|
7254
|
+
usage: "doctor --send [--yes] [--note <n>] [--to <did>] [--host <wss-url>]",
|
|
7255
|
+
desc: "Send the sanitized bundle to the operator support mailbox (consent prompt; prints a support reference)"
|
|
7256
|
+
}
|
|
7257
|
+
]
|
|
7258
|
+
},
|
|
7259
|
+
{
|
|
7260
|
+
title: "Support inbox (operator)",
|
|
7261
|
+
rows: [
|
|
7262
|
+
{
|
|
7263
|
+
usage: "support inbox --on|--off",
|
|
7264
|
+
desc: "Opt this agent in/out as a support mailbox (off by default; inbound bundles are rejected)"
|
|
7265
|
+
},
|
|
7266
|
+
{
|
|
7267
|
+
usage: "support pending",
|
|
7268
|
+
desc: "List received support bundles awaiting review"
|
|
7269
|
+
},
|
|
7270
|
+
{
|
|
7271
|
+
usage: "support read <bundle-id>",
|
|
7272
|
+
desc: "Show a bundle's report and mark it read"
|
|
7273
|
+
},
|
|
7274
|
+
{
|
|
7275
|
+
usage: "support dismiss <bundle-id>",
|
|
7276
|
+
desc: "Dismiss a bundle without reading it"
|
|
7277
|
+
},
|
|
7278
|
+
{
|
|
7279
|
+
usage: "support list",
|
|
7280
|
+
desc: "List all support bundles including read/dismissed"
|
|
7281
|
+
},
|
|
7282
|
+
{
|
|
7283
|
+
usage: "support receive <envelope-json-path>",
|
|
7284
|
+
desc: "Apply an inbound support_bundle envelope from a file"
|
|
6394
7285
|
}
|
|
6395
7286
|
]
|
|
6396
7287
|
},
|
|
@@ -6455,6 +7346,15 @@ var COMMAND_GROUPS = [
|
|
|
6455
7346
|
}
|
|
6456
7347
|
]
|
|
6457
7348
|
},
|
|
7349
|
+
{
|
|
7350
|
+
title: "Network",
|
|
7351
|
+
rows: [
|
|
7352
|
+
{
|
|
7353
|
+
usage: "directory [--limit N] [--relay-base <url>]",
|
|
7354
|
+
desc: "List agents on the network with relationship annotations; EDGE_BOOK_RELAY_BASE env var overrides the default relay"
|
|
7355
|
+
}
|
|
7356
|
+
]
|
|
7357
|
+
},
|
|
6458
7358
|
{
|
|
6459
7359
|
title: "Server / harness",
|
|
6460
7360
|
rows: [
|
|
@@ -6536,10 +7436,15 @@ async function notifyInbound(store, envelope, opts) {
|
|
|
6536
7436
|
if (!opts.cmd || !opts.cmd.trim()) return { notified: false, reason: "no_notify_cmd" };
|
|
6537
7437
|
const intent = await store.notificationIntent(envelope);
|
|
6538
7438
|
if (!intent) return { notified: false, reason: "silent" };
|
|
6539
|
-
if (await store.wasNotified(intent.dedup_key))
|
|
7439
|
+
if (await store.wasNotified(intent.dedup_key)) {
|
|
7440
|
+
await logEvent(store, "notify.suppressed", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key, reason: "already_notified" });
|
|
7441
|
+
return { notified: false, reason: "already_notified" };
|
|
7442
|
+
}
|
|
7443
|
+
await logEvent(store, "notify.attempted", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
|
|
6540
7444
|
const res = await deliverNotification(intent, opts);
|
|
6541
7445
|
if (!res.delivered) {
|
|
6542
7446
|
await store.audit("notify.failed", intent.from_id, { kind: intent.kind, dedup_key: intent.dedup_key, error: res.error ?? "" });
|
|
7447
|
+
await logEvent(store, "notify.failed", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
|
|
6543
7448
|
return { notified: false, reason: res.error };
|
|
6544
7449
|
}
|
|
6545
7450
|
await store.recordNotified(intent.dedup_key);
|
|
@@ -6550,6 +7455,7 @@ async function notifyInbound(store, envelope, opts) {
|
|
|
6550
7455
|
}
|
|
6551
7456
|
}
|
|
6552
7457
|
await store.audit("notify.delivered", intent.from_id, { kind: intent.kind, dedup_key: intent.dedup_key, channel: "hook" });
|
|
7458
|
+
await logEvent(store, "notify.delivered", { kind: intent.kind, from: intent.from_id, dedup_key: intent.dedup_key });
|
|
6553
7459
|
return { notified: true };
|
|
6554
7460
|
}
|
|
6555
7461
|
function makeNotifyOnEnvelope(store, cmd) {
|
|
@@ -6562,114 +7468,6 @@ function makeNotifyOnEnvelope(store, cmd) {
|
|
|
6562
7468
|
};
|
|
6563
7469
|
}
|
|
6564
7470
|
|
|
6565
|
-
// src/host-cron.ts
|
|
6566
|
-
import { existsSync } from "fs";
|
|
6567
|
-
import { execFileSync } from "child_process";
|
|
6568
|
-
var FRIEND_REQUESTS_CRON_NAME = "Edge Book \u2014 friend requests";
|
|
6569
|
-
var DEFAULT_FRIEND_REQUESTS_SCHEDULE = "*/20 * * * *";
|
|
6570
|
-
var HERMES_BIN_CANDIDATES = ["/opt/hermes/.venv/bin/hermes"];
|
|
6571
|
-
function buildFriendRequestsPrompt(home) {
|
|
6572
|
-
return [
|
|
6573
|
-
"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.",
|
|
6574
|
-
"",
|
|
6575
|
-
"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.",
|
|
6576
|
-
"",
|
|
6577
|
-
"1. List pending requests (run once):",
|
|
6578
|
-
` edge-book friend pending --home ${home} --json`,
|
|
6579
|
-
" If edge-book is not on PATH, use: npm exec -y edge-book@0.11.0 -- friend pending --home " + home + " --json",
|
|
6580
|
-
" If the command errors, Edge Book is unavailable, or the list is empty ([]) \u2192 end your turn with exactly [SILENT].",
|
|
6581
|
-
"",
|
|
6582
|
-
'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.',
|
|
6583
|
-
"",
|
|
6584
|
-
"3. Mark each surfaced request notified so it is never re-sent (once per requester):",
|
|
6585
|
-
` edge-book friend mark-notified <agent_id> --home ${home}`
|
|
6586
|
-
].join("\n");
|
|
6587
|
-
}
|
|
6588
|
-
function ensureNotifierCron(opts) {
|
|
6589
|
-
if (opts.disabled) return { status: "disabled" };
|
|
6590
|
-
if (!opts.runner.hermesBin) return { status: "host_unsupported" };
|
|
6591
|
-
let listing;
|
|
6592
|
-
try {
|
|
6593
|
-
listing = opts.runner.list();
|
|
6594
|
-
} catch (e) {
|
|
6595
|
-
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
6596
|
-
}
|
|
6597
|
-
if (listing.includes(FRIEND_REQUESTS_CRON_NAME)) return { status: "already_present" };
|
|
6598
|
-
const schedule = opts.schedule ?? DEFAULT_FRIEND_REQUESTS_SCHEDULE;
|
|
6599
|
-
const prompt = buildFriendRequestsPrompt(opts.home);
|
|
6600
|
-
const args = [
|
|
6601
|
-
"cron",
|
|
6602
|
-
"create",
|
|
6603
|
-
schedule,
|
|
6604
|
-
prompt,
|
|
6605
|
-
"--name",
|
|
6606
|
-
FRIEND_REQUESTS_CRON_NAME,
|
|
6607
|
-
"--deliver",
|
|
6608
|
-
"telegram",
|
|
6609
|
-
"--workdir",
|
|
6610
|
-
opts.home
|
|
6611
|
-
];
|
|
6612
|
-
try {
|
|
6613
|
-
opts.runner.create(args);
|
|
6614
|
-
return { status: "installed" };
|
|
6615
|
-
} catch (e) {
|
|
6616
|
-
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
6617
|
-
}
|
|
6618
|
-
}
|
|
6619
|
-
function defaultHermesRunner() {
|
|
6620
|
-
const bin = HERMES_BIN_CANDIDATES.find((p) => existsSync(p)) ?? null;
|
|
6621
|
-
return {
|
|
6622
|
-
hermesBin: bin,
|
|
6623
|
-
list: () => bin ? execFileSync(bin, ["cron", "list"], { encoding: "utf8" }) : "",
|
|
6624
|
-
create: (args) => {
|
|
6625
|
-
if (bin) execFileSync(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
6626
|
-
}
|
|
6627
|
-
};
|
|
6628
|
-
}
|
|
6629
|
-
var GREETER_CRON_NAME = "Edge Book \u2014 greeter";
|
|
6630
|
-
var DEFAULT_GREETER_SCHEDULE = "*/5 * * * *";
|
|
6631
|
-
function buildGreeterPrompt(home) {
|
|
6632
|
-
return [
|
|
6633
|
-
"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.",
|
|
6634
|
-
"",
|
|
6635
|
-
` edge-book friend auto-accept --deliver --home ${home}`,
|
|
6636
|
-
` If edge-book is not on PATH, use: npm exec -y edge-book -- friend auto-accept --deliver --home ${home}`,
|
|
6637
|
-
"",
|
|
6638
|
-
"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.",
|
|
6639
|
-
"",
|
|
6640
|
-
"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."
|
|
6641
|
-
].join("\n");
|
|
6642
|
-
}
|
|
6643
|
-
function ensureGreeterCron(opts) {
|
|
6644
|
-
if (opts.disabled) return { status: "disabled" };
|
|
6645
|
-
if (!opts.runner.hermesBin) return { status: "host_unsupported" };
|
|
6646
|
-
let listing;
|
|
6647
|
-
try {
|
|
6648
|
-
listing = opts.runner.list();
|
|
6649
|
-
} catch (e) {
|
|
6650
|
-
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
6651
|
-
}
|
|
6652
|
-
if (listing.includes(GREETER_CRON_NAME)) return { status: "already_present" };
|
|
6653
|
-
const args = [
|
|
6654
|
-
"cron",
|
|
6655
|
-
"create",
|
|
6656
|
-
opts.schedule ?? DEFAULT_GREETER_SCHEDULE,
|
|
6657
|
-
buildGreeterPrompt(opts.home),
|
|
6658
|
-
"--name",
|
|
6659
|
-
GREETER_CRON_NAME,
|
|
6660
|
-
"--deliver",
|
|
6661
|
-
"telegram",
|
|
6662
|
-
"--workdir",
|
|
6663
|
-
opts.home
|
|
6664
|
-
];
|
|
6665
|
-
try {
|
|
6666
|
-
opts.runner.create(args);
|
|
6667
|
-
return { status: "installed" };
|
|
6668
|
-
} catch (e) {
|
|
6669
|
-
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
6670
|
-
}
|
|
6671
|
-
}
|
|
6672
|
-
|
|
6673
7471
|
// src/cli.ts
|
|
6674
7472
|
function usage() {
|
|
6675
7473
|
return renderUsage();
|
|
@@ -6695,6 +7493,8 @@ async function handleCli(inputArgs, ctx = {}) {
|
|
|
6695
7493
|
if (command === "friend" && socialAction === "auto-accept") return socialResult;
|
|
6696
7494
|
return maybeAppendHandleNudge(store, command, socialResult);
|
|
6697
7495
|
}
|
|
7496
|
+
const supportResult = await handleSupportCli(command, args, ctx, home, store);
|
|
7497
|
+
if (supportResult) return supportResult;
|
|
6698
7498
|
if (command === "serve") {
|
|
6699
7499
|
const host = takeFlag(args, "--host") || "127.0.0.1";
|
|
6700
7500
|
const port = Number(takeFlag(args, "--port") || "0");
|
|
@@ -6722,6 +7522,8 @@ async function handleCli(inputArgs, ctx = {}) {
|
|
|
6722
7522
|
const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
|
|
6723
7523
|
try {
|
|
6724
7524
|
const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
|
|
7525
|
+
if (res.status === "installed") await logEvent(store2, "cron.notifier_installed", {});
|
|
7526
|
+
else if (res.status === "already_present") await logEvent(store2, "cron.notifier_already_present", {});
|
|
6725
7527
|
if (res.status === "installed") console.log(` \u21B3 notifier cron self-installed ("Edge Book \u2014 friend requests", every 20m \u2192 telegram)`);
|
|
6726
7528
|
else if (res.status === "error") console.log(` \u21B3 notifier cron install skipped: ${res.detail}`);
|
|
6727
7529
|
} catch (e) {
|
|
@@ -6741,6 +7543,8 @@ async function handleCli(inputArgs, ctx = {}) {
|
|
|
6741
7543
|
if (command === "ensure-notifier") {
|
|
6742
7544
|
const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
|
|
6743
7545
|
const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
|
|
7546
|
+
if (res.status === "installed") await logEvent(store, "cron.notifier_installed", {});
|
|
7547
|
+
else if (res.status === "already_present") await logEvent(store, "cron.notifier_already_present", {});
|
|
6744
7548
|
const msg = {
|
|
6745
7549
|
installed: 'Installed notifier cron "Edge Book \u2014 friend requests" (every 20m \u2192 telegram).',
|
|
6746
7550
|
already_present: "Notifier cron already present \u2014 nothing to do.",
|
|
@@ -6767,8 +7571,22 @@ async function handleCli(inputArgs, ctx = {}) {
|
|
|
6767
7571
|
const registration2 = await client.pair(ttlMs);
|
|
6768
7572
|
console.log(`Pairing code: ${registration2.code}`);
|
|
6769
7573
|
console.log(`Expires in: ${ttlMs}ms`);
|
|
6770
|
-
console.log("
|
|
6771
|
-
|
|
7574
|
+
console.log("Waiting for your browser reader to connect...");
|
|
7575
|
+
const pairResult = await client.waitForPairComplete(ttlMs);
|
|
7576
|
+
await client.stop();
|
|
7577
|
+
if (!pairResult) {
|
|
7578
|
+
throw new EdgeBookError("pair_timeout", "Pairing code expired unredeemed \u2014 run edge-book pair again for a fresh code.");
|
|
7579
|
+
}
|
|
7580
|
+
console.log(`
|
|
7581
|
+
Pairing complete \u2014 your reader is connected (device: ${pairResult.label}).`);
|
|
7582
|
+
const notifyCmd = resolveNotifyCmd({ env: process.env.EDGE_BOOK_NOTIFY_CMD, config: (await store.config()).notify_cmd });
|
|
7583
|
+
const intent = buildPairCompleteNotifyIntent(pairResult.device_id, pairResult.label);
|
|
7584
|
+
if (notifyCmd && !await store.wasNotified(intent.dedup_key)) {
|
|
7585
|
+
const nr = await deliverNotification(intent, { cmd: notifyCmd });
|
|
7586
|
+
if (nr.delivered) await store.recordNotified(intent.dedup_key);
|
|
7587
|
+
}
|
|
7588
|
+
console.log("\n" + buildOnboardingNote());
|
|
7589
|
+
return { text: `Pairing complete \u2014 your reader is connected (device: ${pairResult.label}).`, json: { device_id: pairResult.device_id, label: pairResult.label } };
|
|
6772
7590
|
}
|
|
6773
7591
|
const registration = await sendPairRegistration({ home, host: hostUrl, ttlMs, socketFactory: ctx.socketFactory });
|
|
6774
7592
|
return { text: `Pairing code: ${registration.code}
|
|
@@ -6794,6 +7612,47 @@ Expires in: ${registration.frame.ttl_ms}ms`, json: registration };
|
|
|
6794
7612
|
return { text: `Received sessions_revoke_ok for request ${frame.request_id} on ${channel}`, json: frame };
|
|
6795
7613
|
}
|
|
6796
7614
|
}
|
|
7615
|
+
if (command === "outbox") {
|
|
7616
|
+
const asJson = takeBoolFlag(args, "--json");
|
|
7617
|
+
const hostUrl = parseHost(args, ctx);
|
|
7618
|
+
const entries = await readOutbox(home);
|
|
7619
|
+
if (entries.length === 0) {
|
|
7620
|
+
return { text: "Outbox is empty \u2014 nothing has been sent with --deliver yet.", json: { entries: [] } };
|
|
7621
|
+
}
|
|
7622
|
+
const recent = entries.slice(-50);
|
|
7623
|
+
let statuses = null;
|
|
7624
|
+
let unreachable = false;
|
|
7625
|
+
try {
|
|
7626
|
+
statuses = await mailboxStatus({ home, host: hostUrl, socketFactory: ctx.socketFactory, ids: recent.map((e) => e.id) });
|
|
7627
|
+
} catch (error) {
|
|
7628
|
+
statuses = null;
|
|
7629
|
+
unreachable = !(error instanceof EdgeBookError && (error.code === "host_unsupported_rpc" || error.code === "host_rpc_timeout"));
|
|
7630
|
+
}
|
|
7631
|
+
const byId = new Map((statuses ?? []).map((s) => [s.id, s]));
|
|
7632
|
+
const contacts = await store.contacts();
|
|
7633
|
+
const staleMs = staleQueueMs();
|
|
7634
|
+
const report = recent.map((entry) => {
|
|
7635
|
+
const status = byId.get(entry.id);
|
|
7636
|
+
const state = statuses === null ? unreachable ? "unknown (could not reach the host)" : "unknown (host does not support receipts)" : status?.state ?? "unknown";
|
|
7637
|
+
const age = formatAge(Date.now() - Date.parse(entry.sent_at));
|
|
7638
|
+
const stale = status?.state === "queued" && ((status.queued_ms ?? 0) > staleMs || status.recipient_live === false);
|
|
7639
|
+
return {
|
|
7640
|
+
...entry,
|
|
7641
|
+
to_display_name: contacts[entry.to_agent_id]?.display_name || entry.to_agent_id,
|
|
7642
|
+
age,
|
|
7643
|
+
state,
|
|
7644
|
+
...status?.queued_ms !== void 0 ? { queued_ms: status.queued_ms } : {},
|
|
7645
|
+
...status?.recipient_live !== void 0 ? { recipient_live: status.recipient_live } : {},
|
|
7646
|
+
stale: Boolean(stale)
|
|
7647
|
+
};
|
|
7648
|
+
});
|
|
7649
|
+
const lines = report.map((r) => {
|
|
7650
|
+
const base = `${r.id} ${r.envelope_type} \u2192 ${r.to_display_name} (${r.age} ago) ${r.state}`;
|
|
7651
|
+
return r.stale ? `${base}
|
|
7652
|
+
\u26A0 undelivered for ${r.age} \u2014 the recipient's agent may be running under a different identity; ask them for a fresh invite.` : base;
|
|
7653
|
+
});
|
|
7654
|
+
return { text: asJson ? JSON.stringify({ entries: report }, null, 2) : lines.join("\n"), json: { entries: report } };
|
|
7655
|
+
}
|
|
6797
7656
|
if (command === "relay") {
|
|
6798
7657
|
const action = args.shift();
|
|
6799
7658
|
if (action === "serve") {
|
|
@@ -6815,11 +7674,19 @@ ${JSON.stringify(result, null, 2)}`, json: result };
|
|
|
6815
7674
|
}
|
|
6816
7675
|
const taxonomyResult = await handleTaxonomyCli(command, args, ctx, store);
|
|
6817
7676
|
if (taxonomyResult) return maybeAppendHandleNudge(store, command, taxonomyResult);
|
|
7677
|
+
const directoryResult = await handleDirectoryCli(command, args, ctx, home, store);
|
|
7678
|
+
if (directoryResult) return directoryResult;
|
|
6818
7679
|
throw new EdgeBookError("unknown_command", usage());
|
|
6819
7680
|
}
|
|
6820
7681
|
async function runCli(args) {
|
|
6821
|
-
const
|
|
6822
|
-
|
|
7682
|
+
const argv = [...args];
|
|
7683
|
+
const asJson = takeBoolFlag(argv, "--json");
|
|
7684
|
+
const result = await handleCli(argv);
|
|
7685
|
+
if (asJson && result.json !== void 0) {
|
|
7686
|
+
console.log(JSON.stringify(result.json, null, 2));
|
|
7687
|
+
} else {
|
|
7688
|
+
console.log(result.text);
|
|
7689
|
+
}
|
|
6823
7690
|
}
|
|
6824
7691
|
function isCliEntrypoint() {
|
|
6825
7692
|
if (!process.argv[1]) return false;
|