edge-book 0.10.0 → 0.12.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 +7 -1
- package/dist/edge-book.js +442 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,11 @@ edge-book report <peer-agent-id> --block # report and block in one step
|
|
|
147
147
|
|---|---|
|
|
148
148
|
| **Setup** | |
|
|
149
149
|
| `init [--handle <h>] [--name <agent>] [--owner <you>] [--share-owner]` | Create your agent identity + signed card |
|
|
150
|
+
| **Handle / Identity** | |
|
|
151
|
+
| `handle set <slug>` | Claim a unique human handle (replaces the default) |
|
|
152
|
+
| `handle show` | Show your handle + DID fingerprint |
|
|
153
|
+
| `identity export [--path <file>]` | Export your identity keypair to carry to a new device |
|
|
154
|
+
| `identity import <path> [--force]` | Restore an exported identity (same DID, same handle) |
|
|
150
155
|
| **Profile** | |
|
|
151
156
|
| `profile show` | Show your two-tier profile (agent name + friend-only details) |
|
|
152
157
|
| `profile set [--agent-name <n>] [--name <you>] [--bio <b>] [--location <l>] [--social label=value ...]` | Set profile fields; friends-only by default, use profile visibility to tune |
|
|
@@ -157,7 +162,8 @@ edge-book report <peer-agent-id> --block # report and block in one step
|
|
|
157
162
|
| `card export --path <file>` | Write your Agent Card to a JSON file |
|
|
158
163
|
| `card invite [--uses <n>] [--ttl-ms <ms>]` | Print an "Add me" invite link; --uses/--ttl-ms mints a consumable code |
|
|
159
164
|
| **Hosted reader** | |
|
|
160
|
-
| `dialout [--host <wss-url>]` | Connect to the host mailbox (keeps your reader online; leave running) |
|
|
165
|
+
| `dialout [--host <wss-url>] [--notify-cmd <cmd>] [--no-cron-install]` | Connect to the host mailbox (keeps your reader online; leave running) |
|
|
166
|
+
| `ensure-notifier [--no-cron-install]` | Provision the host friend-request notifier (auto-runs on dialout; Hermes installs a cron) |
|
|
161
167
|
| `pair [--host <wss-url>] [--ttl-ms <ms>]` | Mint a pairing code for the hosted browser reader |
|
|
162
168
|
| `sessions list [--host <wss-url>]` | List remembered reader sessions |
|
|
163
169
|
| `sessions revoke [--device <id>] [--host <wss-url>]` | Revoke one device session (or all if no --device) |
|
package/dist/edge-book.js
CHANGED
|
@@ -23,6 +23,72 @@ var EPHEMERAL_TTL_POLICY = {
|
|
|
23
23
|
share: { hard: false },
|
|
24
24
|
coordinate: { hard: false }
|
|
25
25
|
};
|
|
26
|
+
async function peerName(store, agentId) {
|
|
27
|
+
return (await store.contacts())[agentId]?.display_name || void 0;
|
|
28
|
+
}
|
|
29
|
+
var NOTIFY_POLICIES = {
|
|
30
|
+
friend_request: async (env, store) => {
|
|
31
|
+
if ((await store.config()).notify_on_friend_request === false) return null;
|
|
32
|
+
const body = env.body;
|
|
33
|
+
const name = body.card?.display_name || env.from_agent_id;
|
|
34
|
+
const note = body.note ? ` \u2014 \u201C${body.note}\u201D` : "";
|
|
35
|
+
return {
|
|
36
|
+
kind: "friend_request",
|
|
37
|
+
from_id: env.from_agent_id,
|
|
38
|
+
from_name: body.card?.display_name,
|
|
39
|
+
message: `${name} wants to connect on Edge Book${note}. Reply \u201Cyes\u201D to connect, or ignore to leave it pending.`,
|
|
40
|
+
dedup_key: env.message_id
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
privileged_message: async (env, store) => {
|
|
44
|
+
const body = env.body;
|
|
45
|
+
const name = await peerName(store, env.from_agent_id) || env.from_agent_id;
|
|
46
|
+
const text = typeof body.text === "string" ? body.text : "";
|
|
47
|
+
const preview = text.length > 280 ? `${text.slice(0, 279)}\u2026` : text;
|
|
48
|
+
return {
|
|
49
|
+
kind: "privileged_message",
|
|
50
|
+
from_id: env.from_agent_id,
|
|
51
|
+
from_name: await peerName(store, env.from_agent_id),
|
|
52
|
+
message: `${name}: ${preview}`,
|
|
53
|
+
dedup_key: env.message_id
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
friend_response: async (env) => {
|
|
57
|
+
const body = env.body;
|
|
58
|
+
const name = body.card?.display_name || env.from_agent_id;
|
|
59
|
+
const verb = body.accepted ? "accepted" : "declined";
|
|
60
|
+
return {
|
|
61
|
+
kind: "friend_response",
|
|
62
|
+
from_id: env.from_agent_id,
|
|
63
|
+
from_name: body.card?.display_name,
|
|
64
|
+
message: `${name} ${verb} your friend request on Edge Book.`,
|
|
65
|
+
dedup_key: env.message_id
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
object_share: async (env, store) => {
|
|
69
|
+
const body = env.body;
|
|
70
|
+
const name = await peerName(store, env.from_agent_id) || env.from_agent_id;
|
|
71
|
+
const title = body.object?.request?.title || "an item";
|
|
72
|
+
return {
|
|
73
|
+
kind: "object_share",
|
|
74
|
+
from_id: env.from_agent_id,
|
|
75
|
+
from_name: await peerName(store, env.from_agent_id),
|
|
76
|
+
message: `${name} shared a request: \u201C${title}\u201D.`,
|
|
77
|
+
dedup_key: env.message_id
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
escalation: async (env) => {
|
|
81
|
+
const body = env.body;
|
|
82
|
+
const esc = body.escalation;
|
|
83
|
+
const opts = esc?.options?.length ? ` (options: ${esc.options.join(" / ")})` : "";
|
|
84
|
+
return {
|
|
85
|
+
kind: "escalation",
|
|
86
|
+
from_id: env.from_agent_id,
|
|
87
|
+
message: `${esc?.subject ?? "A decision is needed"} \u2014 ${esc?.body ?? ""}${opts}`,
|
|
88
|
+
dedup_key: env.message_id
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
26
92
|
var EdgeBookError = class extends Error {
|
|
27
93
|
code;
|
|
28
94
|
constructor(code, message) {
|
|
@@ -47,6 +113,7 @@ var SESSIONS_FILE = "web-sessions.json";
|
|
|
47
113
|
var POSTS_FILE = "posts.json";
|
|
48
114
|
var FEED_FILE = "feed-items.json";
|
|
49
115
|
var APPROVALS_FILE = "approvals.json";
|
|
116
|
+
var NOTIFIED_FILE = "notified.json";
|
|
50
117
|
var ESCALATIONS_FILE = "escalations.json";
|
|
51
118
|
var CONTACT_MUTES_FILE = "contact-mutes.json";
|
|
52
119
|
var REPORTS_FILE = "reports.json";
|
|
@@ -72,6 +139,14 @@ function now() {
|
|
|
72
139
|
function randomId(prefix) {
|
|
73
140
|
return `${prefix}_${crypto.randomBytes(16).toString("base64url")}`;
|
|
74
141
|
}
|
|
142
|
+
var HANDLE_SLUG = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
|
|
143
|
+
var RESERVED_HANDLES = /* @__PURE__ */ new Set(["add", "healthz", "metrics", "agent", "api", "handle", "auth"]);
|
|
144
|
+
function isValidHandle(handle) {
|
|
145
|
+
return HANDLE_SLUG.test(handle) && !RESERVED_HANDLES.has(handle);
|
|
146
|
+
}
|
|
147
|
+
function slugifyHandle(input) {
|
|
148
|
+
return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30);
|
|
149
|
+
}
|
|
75
150
|
function stableIdFromPublicKey(publicKeyPem) {
|
|
76
151
|
const digest = crypto.createHash("sha256").update(publicKeyPem).digest("base64url").slice(0, 32);
|
|
77
152
|
return `did:openclaw:${digest}`;
|
|
@@ -279,6 +354,40 @@ var EdgeBookStore = class {
|
|
|
279
354
|
await this.audit("identity.update", identity.agent_id, { display_name: identity.display_name, profile_version: profile.profile_version });
|
|
280
355
|
return identity;
|
|
281
356
|
}
|
|
357
|
+
// Set a user-chosen unique handle. Re-signs the card; does NOT rotate keys.
|
|
358
|
+
async setHandle(handle) {
|
|
359
|
+
if (!isValidHandle(handle)) {
|
|
360
|
+
throw new EdgeBookError("invalid_handle", `invalid_handle: must be 3-30 chars [a-z0-9-], not reserved: ${handle}`);
|
|
361
|
+
}
|
|
362
|
+
const identity = await this.identity();
|
|
363
|
+
identity.handle = handle;
|
|
364
|
+
identity.updated_at = now();
|
|
365
|
+
await writeJson(this.file(IDENTITY_FILE), identity, 384);
|
|
366
|
+
await this.writeCard();
|
|
367
|
+
await this.audit("identity.set_handle", identity.agent_id, { handle });
|
|
368
|
+
return identity;
|
|
369
|
+
}
|
|
370
|
+
// Portable identity bundle (the DID keypair + chosen handle). Carry to a new
|
|
371
|
+
// device → same DID → relay handle keeps resolving to you (spec-096).
|
|
372
|
+
async exportIdentity() {
|
|
373
|
+
return { schema: "edge-book-identity-export/0.1", identity: await this.identity() };
|
|
374
|
+
}
|
|
375
|
+
async importIdentity(bundle, opts = {}) {
|
|
376
|
+
await ensureHome(this.home);
|
|
377
|
+
const existing = await readJson(this.file(IDENTITY_FILE), null);
|
|
378
|
+
if (existing && !opts.force) throw new EdgeBookError("identity_exists", `identity_exists: an identity already exists at ${this.home} (use --force to overwrite)`);
|
|
379
|
+
const id = bundle.identity;
|
|
380
|
+
if (!id?.public_key_pem || id.agent_id !== stableIdFromPublicKey(id.public_key_pem)) {
|
|
381
|
+
throw new EdgeBookError("invalid_import", "Bundle agent_id does not match its public key");
|
|
382
|
+
}
|
|
383
|
+
await writeJson(this.file(IDENTITY_FILE), id, 384);
|
|
384
|
+
if (!await readJson(this.file(CONTACTS_FILE), null)) await writeJson(this.file(CONTACTS_FILE), {});
|
|
385
|
+
if (!await readJson(this.file(GRANTS_FILE), null)) await writeJson(this.file(GRANTS_FILE), {});
|
|
386
|
+
if (!await readJson(this.file(SEEN_MESSAGES_FILE), null)) await writeJson(this.file(SEEN_MESSAGES_FILE), []);
|
|
387
|
+
await this.writeCard();
|
|
388
|
+
await this.audit("identity.import", id.agent_id, { handle: id.handle });
|
|
389
|
+
return id;
|
|
390
|
+
}
|
|
282
391
|
async config() {
|
|
283
392
|
return readJson(this.file(CONFIG_FILE), {});
|
|
284
393
|
}
|
|
@@ -288,6 +397,8 @@ var EdgeBookStore = class {
|
|
|
288
397
|
if (input.direct_url !== void 0) next.direct_url = input.direct_url;
|
|
289
398
|
if (input.relay_url !== void 0) next.relay_url = input.relay_url;
|
|
290
399
|
if (input.notify_on_friend_request !== void 0) next.notify_on_friend_request = input.notify_on_friend_request;
|
|
400
|
+
if (input.notify_cmd !== void 0) next.notify_cmd = input.notify_cmd;
|
|
401
|
+
if (input.notify_types !== void 0) next.notify_types = input.notify_types;
|
|
291
402
|
if (input.open_friend_requests !== void 0) next.open_friend_requests = input.open_friend_requests;
|
|
292
403
|
if (input.inbound_max_per_peer !== void 0) next.inbound_max_per_peer = input.inbound_max_per_peer;
|
|
293
404
|
if (input.inbound_max_global !== void 0) next.inbound_max_global = input.inbound_max_global;
|
|
@@ -331,6 +442,18 @@ var EdgeBookStore = class {
|
|
|
331
442
|
await writeJson(this.file(CARD_FILE), card);
|
|
332
443
|
return card;
|
|
333
444
|
}
|
|
445
|
+
// Build a signed handle claim for the relay registry (spec-096). The relay
|
|
446
|
+
// verifies claim_sig + the card against the identity key before binding.
|
|
447
|
+
async buildHandleClaim() {
|
|
448
|
+
const identity = await this.identity();
|
|
449
|
+
if (!isValidHandle(identity.handle)) {
|
|
450
|
+
throw new EdgeBookError("invalid_handle", `invalid_handle: set a handle first (current: ${identity.handle})`);
|
|
451
|
+
}
|
|
452
|
+
const card = await loadCard(this.file(CARD_FILE));
|
|
453
|
+
const claimed_at = Date.now();
|
|
454
|
+
const claim_sig = signPayload({ handle: identity.handle, agent_did: identity.agent_id, claimed_at }, identity.private_key_pem);
|
|
455
|
+
return { handle: identity.handle, agent_did: identity.agent_id, card, claimed_at, claim_sig };
|
|
456
|
+
}
|
|
334
457
|
// The friend-only profile: every field whose visibility resolves to "friends"
|
|
335
458
|
// or "public". Signed; shared only with confirmed friends.
|
|
336
459
|
async buildFriendProfile() {
|
|
@@ -1636,6 +1759,30 @@ var EdgeBookStore = class {
|
|
|
1636
1759
|
if (envelope.type === "escalation_response") return this.applyEscalationResponse(envelope);
|
|
1637
1760
|
throw new EdgeBookError("unsupported_envelope", `Unsupported envelope type: ${envelope.type}`);
|
|
1638
1761
|
}
|
|
1762
|
+
// Compute the transport-free notification intent for an applied inbound envelope,
|
|
1763
|
+
// or null when the type is silent / unregistered. Delivery (invoking the host
|
|
1764
|
+
// notify command) is the entry point's job — this stays transport-free.
|
|
1765
|
+
async notificationIntent(envelope) {
|
|
1766
|
+
const policy = NOTIFY_POLICIES[envelope.type];
|
|
1767
|
+
if (!policy) return null;
|
|
1768
|
+
const intent = await policy(envelope, this);
|
|
1769
|
+
if (!intent) return null;
|
|
1770
|
+
const types = (await this.config()).notify_types;
|
|
1771
|
+
if (Array.isArray(types) && !types.includes(intent.kind)) return null;
|
|
1772
|
+
return intent;
|
|
1773
|
+
}
|
|
1774
|
+
// Notification dedup ledger (keyed by NotificationIntent.dedup_key). Guards
|
|
1775
|
+
// against double-notify across entry points, hook+cron, and mailbox redelivery.
|
|
1776
|
+
async wasNotified(dedupKey) {
|
|
1777
|
+
const ledger = await readJson(this.file(NOTIFIED_FILE), []);
|
|
1778
|
+
return ledger.includes(dedupKey);
|
|
1779
|
+
}
|
|
1780
|
+
async recordNotified(dedupKey) {
|
|
1781
|
+
const ledger = await readJson(this.file(NOTIFIED_FILE), []);
|
|
1782
|
+
if (ledger.includes(dedupKey)) return;
|
|
1783
|
+
ledger.push(dedupKey);
|
|
1784
|
+
await writeJson(this.file(NOTIFIED_FILE), ledger);
|
|
1785
|
+
}
|
|
1639
1786
|
async audit(action, peerAgentId, details) {
|
|
1640
1787
|
const audit_id = randomId("audit");
|
|
1641
1788
|
await appendJsonl(this.file(AUDIT_FILE), {
|
|
@@ -2197,6 +2344,12 @@ var EdgeBookStore = class {
|
|
|
2197
2344
|
};
|
|
2198
2345
|
function validateCard(card) {
|
|
2199
2346
|
if (card.schema !== "openclaw-agent-card/0.1") throw new EdgeBookError("invalid_card", "Unsupported Agent Card schema");
|
|
2347
|
+
if (card.expires_at) {
|
|
2348
|
+
const exp = Date.parse(card.expires_at);
|
|
2349
|
+
if (!Number.isNaN(exp) && exp <= Date.now()) {
|
|
2350
|
+
throw new EdgeBookError("card_expired", "Card/invite expired \u2014 ask the peer for a fresh handle or invite");
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2200
2353
|
if (!card.agent_id || !card.public_keys?.[0]?.public_key_pem) throw new EdgeBookError("invalid_card", "Agent Card is missing identity key");
|
|
2201
2354
|
const expectedId = stableIdFromPublicKey(card.public_keys[0].public_key_pem);
|
|
2202
2355
|
if (card.agent_id !== expectedId) throw new EdgeBookError("invalid_card", "Agent Card agent_id does not match public key");
|
|
@@ -2390,8 +2543,13 @@ async function writeCandidate(store, input) {
|
|
|
2390
2543
|
await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
|
|
2391
2544
|
return candidate;
|
|
2392
2545
|
}
|
|
2393
|
-
function defaultProviders(
|
|
2394
|
-
|
|
2546
|
+
function defaultProviders(relayBase) {
|
|
2547
|
+
const lookup = async (target) => {
|
|
2548
|
+
if (!relayBase) return null;
|
|
2549
|
+
const slug = target.startsWith("registry:") ? target.slice("registry:".length) : target;
|
|
2550
|
+
return `${relayBase.replace(/\/$/, "")}/handle/${encodeURIComponent(slug)}`;
|
|
2551
|
+
};
|
|
2552
|
+
return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(lookup)];
|
|
2395
2553
|
}
|
|
2396
2554
|
async function resolveTarget(store, target, opts) {
|
|
2397
2555
|
const ordered = [...opts.providers].sort((a, b) => b.priority - a.priority);
|
|
@@ -2436,20 +2594,29 @@ async function promoteCandidate(store, candidateId, note = "") {
|
|
|
2436
2594
|
await store.audit("candidate.promoted", card.agent_id, { candidate_id: candidateId });
|
|
2437
2595
|
return envelope;
|
|
2438
2596
|
}
|
|
2597
|
+
var HANDLE_SLUG2 = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
|
|
2439
2598
|
function makeRegistryProvider(lookup) {
|
|
2440
2599
|
return {
|
|
2441
2600
|
name: "registry",
|
|
2442
2601
|
priority: 50,
|
|
2443
2602
|
async resolve(_store, target) {
|
|
2444
|
-
|
|
2603
|
+
const isExplicit = target.startsWith("registry:");
|
|
2604
|
+
const slug = isExplicit ? target.slice("registry:".length) : target;
|
|
2605
|
+
if (!isExplicit && !HANDLE_SLUG2.test(slug)) return null;
|
|
2445
2606
|
const cardTarget = await lookup(target);
|
|
2446
2607
|
if (!cardTarget) return null;
|
|
2447
|
-
|
|
2608
|
+
let card;
|
|
2609
|
+
try {
|
|
2610
|
+
card = await loadCard(cardTarget);
|
|
2611
|
+
} catch (e) {
|
|
2612
|
+
if (e instanceof EdgeBookError && e.code === "card_fetch_failed") return null;
|
|
2613
|
+
throw e;
|
|
2614
|
+
}
|
|
2448
2615
|
return {
|
|
2449
2616
|
kind: "card",
|
|
2450
2617
|
card,
|
|
2451
2618
|
agent_id: card.agent_id,
|
|
2452
|
-
provenance: { source: "registry", confidence: "medium", display_name: card.handle, reason: "registry
|
|
2619
|
+
provenance: { source: "registry", confidence: "medium", display_name: card.handle, reason: "handle registry lookup" }
|
|
2453
2620
|
};
|
|
2454
2621
|
}
|
|
2455
2622
|
};
|
|
@@ -4096,6 +4263,9 @@ function now2() {
|
|
|
4096
4263
|
function keyId(agentKey) {
|
|
4097
4264
|
return `agent_${crypto2.createHash("sha256").update(agentKey).digest("base64url").slice(0, 32)}`;
|
|
4098
4265
|
}
|
|
4266
|
+
function shouldClaimHandle(handle) {
|
|
4267
|
+
return !!handle && handle !== "agent.openclaw.local" && /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/.test(handle);
|
|
4268
|
+
}
|
|
4099
4269
|
async function chmodBestEffort2(file, mode) {
|
|
4100
4270
|
if (process.platform === "win32") return;
|
|
4101
4271
|
try {
|
|
@@ -4423,6 +4593,21 @@ var EdgeBookDialoutClient = class {
|
|
|
4423
4593
|
if (frame.type === "hello_ok") {
|
|
4424
4594
|
this.opened?.resolve();
|
|
4425
4595
|
this.opened = void 0;
|
|
4596
|
+
try {
|
|
4597
|
+
const identity = await this.store.identity();
|
|
4598
|
+
if (shouldClaimHandle(identity.handle)) {
|
|
4599
|
+
const claim = await this.store.buildHandleClaim();
|
|
4600
|
+
this.send({
|
|
4601
|
+
type: "handle_claim",
|
|
4602
|
+
request_id: `hc-${claim.claimed_at}`,
|
|
4603
|
+
handle: claim.handle,
|
|
4604
|
+
card: claim.card,
|
|
4605
|
+
claimed_at: claim.claimed_at,
|
|
4606
|
+
claim_sig: claim.claim_sig
|
|
4607
|
+
});
|
|
4608
|
+
}
|
|
4609
|
+
} catch {
|
|
4610
|
+
}
|
|
4426
4611
|
return;
|
|
4427
4612
|
}
|
|
4428
4613
|
if (frame.type === "hello_err") {
|
|
@@ -4485,6 +4670,7 @@ var EdgeBookDialoutClient = class {
|
|
|
4485
4670
|
await this.handleMailboxDeliver(frame);
|
|
4486
4671
|
return;
|
|
4487
4672
|
}
|
|
4673
|
+
if (frameType === "handle_claim_ok" || frameType === "handle_claim_err") return;
|
|
4488
4674
|
if (frameType === "error") return;
|
|
4489
4675
|
if (frame.type !== "host.api.request" && frame.type !== "api_request") return;
|
|
4490
4676
|
const response = await this.handleApiRequest(frame);
|
|
@@ -4625,6 +4811,27 @@ var COMMAND_GROUPS = [
|
|
|
4625
4811
|
}
|
|
4626
4812
|
]
|
|
4627
4813
|
},
|
|
4814
|
+
{
|
|
4815
|
+
title: "Handle / Identity",
|
|
4816
|
+
rows: [
|
|
4817
|
+
{
|
|
4818
|
+
usage: "handle set <slug>",
|
|
4819
|
+
desc: "Claim a unique human handle (replaces the default)"
|
|
4820
|
+
},
|
|
4821
|
+
{
|
|
4822
|
+
usage: "handle show",
|
|
4823
|
+
desc: "Show your handle + DID fingerprint"
|
|
4824
|
+
},
|
|
4825
|
+
{
|
|
4826
|
+
usage: "identity export [--path <file>]",
|
|
4827
|
+
desc: "Export your identity keypair to carry to a new device"
|
|
4828
|
+
},
|
|
4829
|
+
{
|
|
4830
|
+
usage: "identity import <path> [--force]",
|
|
4831
|
+
desc: "Restore an exported identity (same DID, same handle)"
|
|
4832
|
+
}
|
|
4833
|
+
]
|
|
4834
|
+
},
|
|
4628
4835
|
{
|
|
4629
4836
|
title: "Profile",
|
|
4630
4837
|
rows: [
|
|
@@ -4667,9 +4874,13 @@ var COMMAND_GROUPS = [
|
|
|
4667
4874
|
title: "Hosted reader",
|
|
4668
4875
|
rows: [
|
|
4669
4876
|
{
|
|
4670
|
-
usage: "dialout [--host <wss-url>]",
|
|
4877
|
+
usage: "dialout [--host <wss-url>] [--notify-cmd <cmd>] [--no-cron-install]",
|
|
4671
4878
|
desc: "Connect to the host mailbox (keeps your reader online; leave running)"
|
|
4672
4879
|
},
|
|
4880
|
+
{
|
|
4881
|
+
usage: "ensure-notifier [--no-cron-install]",
|
|
4882
|
+
desc: "Provision the host friend-request notifier (auto-runs on dialout; Hermes installs a cron)"
|
|
4883
|
+
},
|
|
4673
4884
|
{
|
|
4674
4885
|
usage: "pair [--host <wss-url>] [--ttl-ms <ms>]",
|
|
4675
4886
|
desc: "Mint a pairing code for the hosted browser reader"
|
|
@@ -4944,6 +5155,148 @@ function renderUsage() {
|
|
|
4944
5155
|
return lines.join("\n");
|
|
4945
5156
|
}
|
|
4946
5157
|
|
|
5158
|
+
// src/notify.ts
|
|
5159
|
+
import { spawn } from "child_process";
|
|
5160
|
+
async function deliverNotification(intent, opts) {
|
|
5161
|
+
if (!opts.cmd || !opts.cmd.trim()) return { delivered: false, error: "no_notify_cmd" };
|
|
5162
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
5163
|
+
const env = {
|
|
5164
|
+
...process.env,
|
|
5165
|
+
EB_NOTIFY_KIND: intent.kind,
|
|
5166
|
+
EB_NOTIFY_FROM_ID: intent.from_id,
|
|
5167
|
+
EB_NOTIFY_FROM_NAME: intent.from_name ?? "",
|
|
5168
|
+
EB_NOTIFY_DEDUP_KEY: intent.dedup_key
|
|
5169
|
+
};
|
|
5170
|
+
for (const [k, v] of Object.entries(intent.meta ?? {})) {
|
|
5171
|
+
env[`EB_NOTIFY_${k.toUpperCase()}`] = v;
|
|
5172
|
+
}
|
|
5173
|
+
return new Promise((resolve) => {
|
|
5174
|
+
let settled = false;
|
|
5175
|
+
const done = (r) => {
|
|
5176
|
+
if (settled) return;
|
|
5177
|
+
settled = true;
|
|
5178
|
+
clearTimeout(timer);
|
|
5179
|
+
resolve(r);
|
|
5180
|
+
};
|
|
5181
|
+
const child = spawn("/bin/sh", ["-c", opts.cmd], { env, stdio: ["pipe", "ignore", "pipe"] });
|
|
5182
|
+
const timer = setTimeout(() => {
|
|
5183
|
+
child.kill("SIGKILL");
|
|
5184
|
+
done({ delivered: false, error: `timeout after ${timeoutMs}ms` });
|
|
5185
|
+
}, timeoutMs);
|
|
5186
|
+
let stderr = "";
|
|
5187
|
+
child.stderr?.on("data", (d) => {
|
|
5188
|
+
stderr += d.toString();
|
|
5189
|
+
});
|
|
5190
|
+
child.on("error", (e) => done({ delivered: false, error: e.message }));
|
|
5191
|
+
child.on("close", (code) => {
|
|
5192
|
+
if (code === 0) done({ delivered: true });
|
|
5193
|
+
else done({ delivered: false, error: `exit ${code}${stderr ? `: ${stderr.trim()}` : ""}` });
|
|
5194
|
+
});
|
|
5195
|
+
child.stdin?.on("error", () => void 0);
|
|
5196
|
+
child.stdin?.end(intent.message ?? "");
|
|
5197
|
+
});
|
|
5198
|
+
}
|
|
5199
|
+
function resolveNotifyCmd(input) {
|
|
5200
|
+
for (const v of [input.flag, input.env, input.config]) {
|
|
5201
|
+
if (typeof v === "string" && v.trim()) return v;
|
|
5202
|
+
}
|
|
5203
|
+
return void 0;
|
|
5204
|
+
}
|
|
5205
|
+
async function notifyInbound(store, envelope, opts) {
|
|
5206
|
+
if (!opts.cmd || !opts.cmd.trim()) return { notified: false, reason: "no_notify_cmd" };
|
|
5207
|
+
const intent = await store.notificationIntent(envelope);
|
|
5208
|
+
if (!intent) return { notified: false, reason: "silent" };
|
|
5209
|
+
if (await store.wasNotified(intent.dedup_key)) return { notified: false, reason: "already_notified" };
|
|
5210
|
+
const res = await deliverNotification(intent, opts);
|
|
5211
|
+
if (!res.delivered) {
|
|
5212
|
+
await store.audit("notify.failed", intent.from_id, { kind: intent.kind, dedup_key: intent.dedup_key, error: res.error ?? "" });
|
|
5213
|
+
return { notified: false, reason: res.error };
|
|
5214
|
+
}
|
|
5215
|
+
await store.recordNotified(intent.dedup_key);
|
|
5216
|
+
if (intent.kind === "friend_request") {
|
|
5217
|
+
try {
|
|
5218
|
+
await store.markFriendRequestNotified(intent.from_id);
|
|
5219
|
+
} catch {
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
await store.audit("notify.delivered", intent.from_id, { kind: intent.kind, dedup_key: intent.dedup_key, channel: "hook" });
|
|
5223
|
+
return { notified: true };
|
|
5224
|
+
}
|
|
5225
|
+
function makeNotifyOnEnvelope(store, cmd) {
|
|
5226
|
+
return async (envelope, result) => {
|
|
5227
|
+
if (!result.applied || !cmd) return;
|
|
5228
|
+
try {
|
|
5229
|
+
await notifyInbound(store, envelope, { cmd });
|
|
5230
|
+
} catch {
|
|
5231
|
+
}
|
|
5232
|
+
};
|
|
5233
|
+
}
|
|
5234
|
+
|
|
5235
|
+
// src/host-cron.ts
|
|
5236
|
+
import { existsSync } from "fs";
|
|
5237
|
+
import { execFileSync } from "child_process";
|
|
5238
|
+
var FRIEND_REQUESTS_CRON_NAME = "Edge Book \u2014 friend requests";
|
|
5239
|
+
var DEFAULT_FRIEND_REQUESTS_SCHEDULE = "*/20 * * * *";
|
|
5240
|
+
var HERMES_BIN_CANDIDATES = ["/opt/hermes/.venv/bin/hermes"];
|
|
5241
|
+
function buildFriendRequestsPrompt(home) {
|
|
5242
|
+
return [
|
|
5243
|
+
"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.",
|
|
5244
|
+
"",
|
|
5245
|
+
"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.",
|
|
5246
|
+
"",
|
|
5247
|
+
"1. List pending requests (run once):",
|
|
5248
|
+
` edge-book friend pending --home ${home} --json`,
|
|
5249
|
+
" If edge-book is not on PATH, use: npm exec -y edge-book@0.11.0 -- friend pending --home " + home + " --json",
|
|
5250
|
+
" If the command errors, Edge Book is unavailable, or the list is empty ([]) \u2192 end your turn with exactly [SILENT].",
|
|
5251
|
+
"",
|
|
5252
|
+
'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.',
|
|
5253
|
+
"",
|
|
5254
|
+
"3. Mark each surfaced request notified so it is never re-sent (once per requester):",
|
|
5255
|
+
` edge-book friend mark-notified <agent_id> --home ${home}`
|
|
5256
|
+
].join("\n");
|
|
5257
|
+
}
|
|
5258
|
+
function ensureNotifierCron(opts) {
|
|
5259
|
+
if (opts.disabled) return { status: "disabled" };
|
|
5260
|
+
if (!opts.runner.hermesBin) return { status: "host_unsupported" };
|
|
5261
|
+
let listing;
|
|
5262
|
+
try {
|
|
5263
|
+
listing = opts.runner.list();
|
|
5264
|
+
} catch (e) {
|
|
5265
|
+
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
5266
|
+
}
|
|
5267
|
+
if (listing.includes(FRIEND_REQUESTS_CRON_NAME)) return { status: "already_present" };
|
|
5268
|
+
const schedule = opts.schedule ?? DEFAULT_FRIEND_REQUESTS_SCHEDULE;
|
|
5269
|
+
const prompt = buildFriendRequestsPrompt(opts.home);
|
|
5270
|
+
const args = [
|
|
5271
|
+
"cron",
|
|
5272
|
+
"create",
|
|
5273
|
+
schedule,
|
|
5274
|
+
prompt,
|
|
5275
|
+
"--name",
|
|
5276
|
+
FRIEND_REQUESTS_CRON_NAME,
|
|
5277
|
+
"--deliver",
|
|
5278
|
+
"telegram",
|
|
5279
|
+
"--workdir",
|
|
5280
|
+
opts.home
|
|
5281
|
+
];
|
|
5282
|
+
try {
|
|
5283
|
+
opts.runner.create(args);
|
|
5284
|
+
return { status: "installed" };
|
|
5285
|
+
} catch (e) {
|
|
5286
|
+
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
function defaultHermesRunner() {
|
|
5290
|
+
const bin = HERMES_BIN_CANDIDATES.find((p) => existsSync(p)) ?? null;
|
|
5291
|
+
return {
|
|
5292
|
+
hermesBin: bin,
|
|
5293
|
+
list: () => bin ? execFileSync(bin, ["cron", "list"], { encoding: "utf8" }) : "",
|
|
5294
|
+
create: (args) => {
|
|
5295
|
+
if (bin) execFileSync(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
5296
|
+
}
|
|
5297
|
+
};
|
|
5298
|
+
}
|
|
5299
|
+
|
|
4947
5300
|
// src/cli.ts
|
|
4948
5301
|
function usage() {
|
|
4949
5302
|
return renderUsage();
|
|
@@ -4961,6 +5314,9 @@ function parseHome(args, ctx) {
|
|
|
4961
5314
|
function parseHost(args, ctx) {
|
|
4962
5315
|
return takeFlag(args, "--host") || ctx.defaultHost || process.env.EDGE_BOOK_HOST || DEFAULT_DIALOUT_HOST;
|
|
4963
5316
|
}
|
|
5317
|
+
function relayBaseFromHost(host) {
|
|
5318
|
+
return host.replace(/\/agent\/ws\/?$/, "").replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://");
|
|
5319
|
+
}
|
|
4964
5320
|
function requireArg(value, label) {
|
|
4965
5321
|
if (!value) throw new EdgeBookError("missing_arg", `Missing ${label}`);
|
|
4966
5322
|
return value;
|
|
@@ -5036,7 +5392,8 @@ async function handleCli(inputArgs, ctx = {}) {
|
|
|
5036
5392
|
return { text: usage() };
|
|
5037
5393
|
}
|
|
5038
5394
|
if (command === "init") {
|
|
5039
|
-
const
|
|
5395
|
+
const rawHandle = takeFlag(args, "--handle");
|
|
5396
|
+
const handle = rawHandle !== void 0 ? slugifyHandle(rawHandle) : void 0;
|
|
5040
5397
|
const displayName = takeFlag(args, "--name");
|
|
5041
5398
|
const ownerLabel = takeFlag(args, "--owner");
|
|
5042
5399
|
const shareOwner = takeBoolFlag(args, "--share-owner");
|
|
@@ -5049,9 +5406,51 @@ Two-tier profile:
|
|
|
5049
5406
|
\u2022 agent name (display_name): "${identity.display_name}" \u2014 always public on your card.
|
|
5050
5407
|
\u2022 your profile (name, bio, location, socials): default visible to FRIENDS only, hidden on the public card.
|
|
5051
5408
|
Set it: edge-book profile set --name "<you>" --bio "..." --social telegram=@you
|
|
5052
|
-
Tune visibility: edge-book profile visibility bio=off telegram=public name=public
|
|
5409
|
+
Tune visibility: edge-book profile visibility bio=off telegram=public name=public
|
|
5410
|
+
|
|
5411
|
+
Notifications (so inbound friend requests & messages reach you in real time):
|
|
5412
|
+
Set a host notify command \u2014 Edge Book stays transport-free and pipes the message to it.
|
|
5413
|
+
edge-book dialout --notify-cmd "<deliver-to-your-channel>"
|
|
5414
|
+
(or set EDGE_BOOK_NOTIFY_CMD, or config.notify_cmd). Without it, inbound items are silent
|
|
5415
|
+
until a fallback poller surfaces them.`;
|
|
5053
5416
|
return { text: note, json: identity };
|
|
5054
5417
|
}
|
|
5418
|
+
if (command === "handle") {
|
|
5419
|
+
const action = args.shift();
|
|
5420
|
+
if (action === "set") {
|
|
5421
|
+
const id = await store.setHandle(slugifyHandle(requireArg(args.shift(), "handle set <slug>")));
|
|
5422
|
+
return { text: `Handle set: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
|
|
5423
|
+
}
|
|
5424
|
+
if (action === "show") {
|
|
5425
|
+
const id = await store.identity();
|
|
5426
|
+
return { text: `${id.handle}
|
|
5427
|
+
${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
|
|
5428
|
+
}
|
|
5429
|
+
throw new EdgeBookError("unknown_action", `Unknown handle action: ${action} (use "set" or "show")`);
|
|
5430
|
+
}
|
|
5431
|
+
if (command === "identity") {
|
|
5432
|
+
const action = args.shift();
|
|
5433
|
+
if (action === "export") {
|
|
5434
|
+
const bundle = await store.exportIdentity();
|
|
5435
|
+
const p = takeFlag(args, "--path");
|
|
5436
|
+
if (p) {
|
|
5437
|
+
const target = path4.resolve(p);
|
|
5438
|
+
await fs4.mkdir(path4.dirname(target), { recursive: true });
|
|
5439
|
+
await fs4.writeFile(target, `${JSON.stringify(bundle, null, 2)}
|
|
5440
|
+
`, { encoding: "utf8", mode: 384 });
|
|
5441
|
+
return { text: `Identity exported \u2192 ${target}`, json: { path: target } };
|
|
5442
|
+
}
|
|
5443
|
+
return { text: JSON.stringify(bundle), json: bundle };
|
|
5444
|
+
}
|
|
5445
|
+
if (action === "import") {
|
|
5446
|
+
const source = requireArg(args.shift(), "identity import <path>");
|
|
5447
|
+
const force = takeBoolFlag(args, "--force");
|
|
5448
|
+
const bundle = JSON.parse(await fs4.readFile(path4.resolve(source), "utf8"));
|
|
5449
|
+
const id = await store.importIdentity(bundle, { force });
|
|
5450
|
+
return { text: `Identity imported: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
|
|
5451
|
+
}
|
|
5452
|
+
throw new EdgeBookError("unknown_action", `Unknown identity action: ${action} (use "export" or "import")`);
|
|
5453
|
+
}
|
|
5055
5454
|
if (command === "profile") {
|
|
5056
5455
|
const action = args.shift() || "show";
|
|
5057
5456
|
if (action === "show") {
|
|
@@ -5176,7 +5575,8 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
|
|
|
5176
5575
|
}
|
|
5177
5576
|
if (command === "resolve") {
|
|
5178
5577
|
const target = requireArg(args.shift(), "target");
|
|
5179
|
-
const
|
|
5578
|
+
const relayBase = relayBaseFromHost(parseHost(args, ctx));
|
|
5579
|
+
const result = await resolveTarget(store, target, { providers: defaultProviders(relayBase) });
|
|
5180
5580
|
const label = result.agent_id ?? result.candidates?.[0]?.candidate_id ?? "";
|
|
5181
5581
|
return { text: `${result.status} ${label}
|
|
5182
5582
|
next: ${result.next_action}`, json: result };
|
|
@@ -5474,11 +5874,42 @@ next: ${result.next_action}`, json: result };
|
|
|
5474
5874
|
}
|
|
5475
5875
|
if (command === "dialout") {
|
|
5476
5876
|
const hostUrl = parseHost(args, ctx);
|
|
5477
|
-
const
|
|
5877
|
+
const store2 = new EdgeBookStore({ home });
|
|
5878
|
+
const notifyCmd = resolveNotifyCmd({
|
|
5879
|
+
flag: takeFlag(args, "--notify-cmd"),
|
|
5880
|
+
env: process.env.EDGE_BOOK_NOTIFY_CMD,
|
|
5881
|
+
config: (await store2.config()).notify_cmd
|
|
5882
|
+
});
|
|
5883
|
+
const client = new EdgeBookDialoutClient({
|
|
5884
|
+
home,
|
|
5885
|
+
host: hostUrl,
|
|
5886
|
+
socketFactory: ctx.socketFactory,
|
|
5887
|
+
onEnvelope: makeNotifyOnEnvelope(store2, notifyCmd)
|
|
5888
|
+
});
|
|
5478
5889
|
await client.start();
|
|
5479
|
-
console.log(`Edge Book dial-out connected to ${hostUrl}`);
|
|
5890
|
+
console.log(`Edge Book dial-out connected to ${hostUrl}${notifyCmd ? " (notify hook active)" : ""}`);
|
|
5891
|
+
try {
|
|
5892
|
+
const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
|
|
5893
|
+
const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
|
|
5894
|
+
if (res.status === "installed") console.log(` \u21B3 notifier cron self-installed ("Edge Book \u2014 friend requests", every 20m \u2192 telegram)`);
|
|
5895
|
+
else if (res.status === "error") console.log(` \u21B3 notifier cron install skipped: ${res.detail}`);
|
|
5896
|
+
} catch (e) {
|
|
5897
|
+
console.log(` \u21B3 notifier cron install skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
5898
|
+
}
|
|
5480
5899
|
await new Promise(() => void 0);
|
|
5481
5900
|
}
|
|
5901
|
+
if (command === "ensure-notifier") {
|
|
5902
|
+
const disabled = takeBoolFlag(args, "--no-cron-install") || process.env.EDGE_BOOK_NO_CRON_INSTALL === "1";
|
|
5903
|
+
const res = ensureNotifierCron({ runner: defaultHermesRunner(), home, disabled });
|
|
5904
|
+
const msg = {
|
|
5905
|
+
installed: 'Installed notifier cron "Edge Book \u2014 friend requests" (every 20m \u2192 telegram).',
|
|
5906
|
+
already_present: "Notifier cron already present \u2014 nothing to do.",
|
|
5907
|
+
host_unsupported: "No recognized host (Hermes) detected \u2014 nothing installed. Set notify_cmd for real-time delivery on hosts with a sender.",
|
|
5908
|
+
disabled: "Cron self-install disabled.",
|
|
5909
|
+
error: `Could not install notifier cron: ${res.detail ?? ""}`
|
|
5910
|
+
};
|
|
5911
|
+
return { text: msg[res.status] ?? res.status, json: res };
|
|
5912
|
+
}
|
|
5482
5913
|
if (command === "pair") {
|
|
5483
5914
|
const hostUrl = parseHost(args, ctx);
|
|
5484
5915
|
const ttlMs = Number(takeFlag(args, "--ttl-ms") || `${5 * 60 * 1e3}`);
|