edge-book 0.11.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 +2 -1
- package/dist/edge-book.js +280 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,7 +162,8 @@ edge-book report <peer-agent-id> --block # report and block in one step
|
|
|
162
162
|
| `card export --path <file>` | Write your Agent Card to a JSON file |
|
|
163
163
|
| `card invite [--uses <n>] [--ttl-ms <ms>]` | Print an "Add me" invite link; --uses/--ttl-ms mints a consumable code |
|
|
164
164
|
| **Hosted reader** | |
|
|
165
|
-
| `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) |
|
|
166
167
|
| `pair [--host <wss-url>] [--ttl-ms <ms>]` | Mint a pairing code for the hosted browser reader |
|
|
167
168
|
| `sessions list [--host <wss-url>]` | List remembered reader sessions |
|
|
168
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";
|
|
@@ -330,6 +397,8 @@ var EdgeBookStore = class {
|
|
|
330
397
|
if (input.direct_url !== void 0) next.direct_url = input.direct_url;
|
|
331
398
|
if (input.relay_url !== void 0) next.relay_url = input.relay_url;
|
|
332
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;
|
|
333
402
|
if (input.open_friend_requests !== void 0) next.open_friend_requests = input.open_friend_requests;
|
|
334
403
|
if (input.inbound_max_per_peer !== void 0) next.inbound_max_per_peer = input.inbound_max_per_peer;
|
|
335
404
|
if (input.inbound_max_global !== void 0) next.inbound_max_global = input.inbound_max_global;
|
|
@@ -1690,6 +1759,30 @@ var EdgeBookStore = class {
|
|
|
1690
1759
|
if (envelope.type === "escalation_response") return this.applyEscalationResponse(envelope);
|
|
1691
1760
|
throw new EdgeBookError("unsupported_envelope", `Unsupported envelope type: ${envelope.type}`);
|
|
1692
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
|
+
}
|
|
1693
1786
|
async audit(action, peerAgentId, details) {
|
|
1694
1787
|
const audit_id = randomId("audit");
|
|
1695
1788
|
await appendJsonl(this.file(AUDIT_FILE), {
|
|
@@ -4781,9 +4874,13 @@ var COMMAND_GROUPS = [
|
|
|
4781
4874
|
title: "Hosted reader",
|
|
4782
4875
|
rows: [
|
|
4783
4876
|
{
|
|
4784
|
-
usage: "dialout [--host <wss-url>]",
|
|
4877
|
+
usage: "dialout [--host <wss-url>] [--notify-cmd <cmd>] [--no-cron-install]",
|
|
4785
4878
|
desc: "Connect to the host mailbox (keeps your reader online; leave running)"
|
|
4786
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
|
+
},
|
|
4787
4884
|
{
|
|
4788
4885
|
usage: "pair [--host <wss-url>] [--ttl-ms <ms>]",
|
|
4789
4886
|
desc: "Mint a pairing code for the hosted browser reader"
|
|
@@ -5058,6 +5155,148 @@ function renderUsage() {
|
|
|
5058
5155
|
return lines.join("\n");
|
|
5059
5156
|
}
|
|
5060
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
|
+
|
|
5061
5300
|
// src/cli.ts
|
|
5062
5301
|
function usage() {
|
|
5063
5302
|
return renderUsage();
|
|
@@ -5167,7 +5406,13 @@ Two-tier profile:
|
|
|
5167
5406
|
\u2022 agent name (display_name): "${identity.display_name}" \u2014 always public on your card.
|
|
5168
5407
|
\u2022 your profile (name, bio, location, socials): default visible to FRIENDS only, hidden on the public card.
|
|
5169
5408
|
Set it: edge-book profile set --name "<you>" --bio "..." --social telegram=@you
|
|
5170
|
-
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.`;
|
|
5171
5416
|
return { text: note, json: identity };
|
|
5172
5417
|
}
|
|
5173
5418
|
if (command === "handle") {
|
|
@@ -5629,11 +5874,42 @@ next: ${result.next_action}`, json: result };
|
|
|
5629
5874
|
}
|
|
5630
5875
|
if (command === "dialout") {
|
|
5631
5876
|
const hostUrl = parseHost(args, ctx);
|
|
5632
|
-
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
|
+
});
|
|
5633
5889
|
await client.start();
|
|
5634
|
-
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
|
+
}
|
|
5635
5899
|
await new Promise(() => void 0);
|
|
5636
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
|
+
}
|
|
5637
5913
|
if (command === "pair") {
|
|
5638
5914
|
const hostUrl = parseHost(args, ctx);
|
|
5639
5915
|
const ttlMs = Number(takeFlag(args, "--ttl-ms") || `${5 * 60 * 1e3}`);
|