beeperbox 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server.js +82 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeperbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Lite mode for beeperbox — the opinionated MCP verb server for Beeper Desktop, run standalone against a Beeper you already have open (no Docker, no Electron). The full headless-Beeper-in-Docker build lives at github.com/hamr0/beeperbox.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"beeperbox": "server.js"
|
package/server.js
CHANGED
|
@@ -95,6 +95,21 @@ const ASSET_TIMEOUT_MS = envIntNonNeg('BEEPERBOX_ASSET_TIMEOUT_MS', 30000);
|
|
|
95
95
|
// neither, so this is its boot sanity check. Opt out with BEEPERBOX_PREFLIGHT=0.
|
|
96
96
|
const PREFLIGHT_TIMEOUT_MS = envIntNonNeg('BEEPERBOX_PREFLIGHT_TIMEOUT_MS', 5000);
|
|
97
97
|
|
|
98
|
+
// The accountID -> network map is cached so chat normalizers don't re-fetch
|
|
99
|
+
// /v1/accounts on every call. But an UNBOUNDED cache is a resilience bug: an
|
|
100
|
+
// account added at runtime (e.g. WhatsApp via noVNC) would render network
|
|
101
|
+
// "unknown" in every chat verb until the MCP *process* restarts, and an empty/
|
|
102
|
+
// partial map captured during the backend's post-restart sync window would
|
|
103
|
+
// freeze for the whole process life — so a plain `docker restart` re-poisons it
|
|
104
|
+
// from a still-syncing backend and the account list stays wrong (the exact wedge
|
|
105
|
+
// multis reported). Bound it with a TTL, and never cache an EMPTY result. Set to
|
|
106
|
+
// 0 to disable caching (always read live).
|
|
107
|
+
const ACCOUNT_CACHE_TTL_MS = envIntNonNeg('BEEPERBOX_ACCOUNT_CACHE_TTL_MS', 60000);
|
|
108
|
+
|
|
109
|
+
// Injectable clock — overridable in tests so the TTL is verifiable without a
|
|
110
|
+
// real sleep. Production reads the wall clock.
|
|
111
|
+
let nowFn = () => Date.now();
|
|
112
|
+
|
|
98
113
|
// Real attachment src_urls come in two shapes: remote Matrix content
|
|
99
114
|
// (mxc:// / localmxc://) and, once Beeper caches the file locally,
|
|
100
115
|
// file:///root/.config/BeeperTexts/media/... — verified against a live account.
|
|
@@ -293,17 +308,59 @@ async function getNoteToSelfChatID() {
|
|
|
293
308
|
throw rpcError(-32002, 'note-to-self chat not found in top 100 chats — open Beeper Desktop and verify a "Note to self" chat exists');
|
|
294
309
|
}
|
|
295
310
|
|
|
311
|
+
// Unwrap /v1/accounts into a bare array — Beeper returns a bare array today, but
|
|
312
|
+
// tolerate a {items:[...]} envelope. One place so list_accounts, getAccountMap,
|
|
313
|
+
// and preflight agree.
|
|
314
|
+
function accountList(accounts) {
|
|
315
|
+
return Array.isArray(accounts) ? accounts : (accounts?.items || []);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// accountCache: { map, at } | null. Bounded by ACCOUNT_CACHE_TTL_MS and NEVER
|
|
319
|
+
// populated from an empty result — see ACCOUNT_CACHE_TTL_MS above for why the old
|
|
320
|
+
// unbounded cache wedged after a runtime account-add / post-restart sync window.
|
|
296
321
|
async function getAccountMap() {
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
accountCache
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
322
|
+
const now = nowFn();
|
|
323
|
+
const cacheValid =
|
|
324
|
+
accountCache &&
|
|
325
|
+
ACCOUNT_CACHE_TTL_MS > 0 &&
|
|
326
|
+
(now - accountCache.at) < ACCOUNT_CACHE_TTL_MS;
|
|
327
|
+
if (cacheValid) return accountCache.map;
|
|
328
|
+
|
|
329
|
+
const list = accountList(await beeperFetch('/v1/accounts'));
|
|
330
|
+
const map = {};
|
|
331
|
+
for (const a of list) {
|
|
332
|
+
map[a.accountID] = { network: networkSlug(a.network), network_label: a.network };
|
|
333
|
+
}
|
|
334
|
+
// An empty /v1/accounts is almost always the backend mid-sync (just after a
|
|
335
|
+
// restart or an account-add), not a real "no accounts" state. Caching it would
|
|
336
|
+
// freeze every chat verb's network labels until the *process* restarts. Serve
|
|
337
|
+
// it for THIS call (the verb still returns, just with "unknown" labels) but
|
|
338
|
+
// leave the cache empty so the very next call re-reads and self-heals the
|
|
339
|
+
// moment the backend populates. Log it so a recurrence is diagnosable, not silent.
|
|
340
|
+
if (list.length === 0) {
|
|
341
|
+
process.stderr.write('[beeperbox-mcp] /v1/accounts returned 0 accounts — Beeper may still be syncing; not caching, will re-read next call\n');
|
|
342
|
+
accountCache = null;
|
|
343
|
+
return map;
|
|
305
344
|
}
|
|
306
|
-
|
|
345
|
+
accountCache = { map, at: now };
|
|
346
|
+
return map;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Map one raw /v1/accounts entry into the list_accounts shape. `status` is the
|
|
350
|
+
// backend's per-account connection state ("connected" / "connecting" / …) —
|
|
351
|
+
// surfaced (not dropped) so a caller can tell a still-syncing bridge from a real
|
|
352
|
+
// one instead of reading a transient as "gone".
|
|
353
|
+
function normalizeAccount(a) {
|
|
354
|
+
return {
|
|
355
|
+
account_id: a.accountID,
|
|
356
|
+
network: networkSlug(a.network),
|
|
357
|
+
network_label: a.network,
|
|
358
|
+
status: a.status || null,
|
|
359
|
+
user: {
|
|
360
|
+
id: a.user?.id || null,
|
|
361
|
+
display_name: a.user?.fullName || a.user?.displayText || a.user?.username || null,
|
|
362
|
+
},
|
|
363
|
+
};
|
|
307
364
|
}
|
|
308
365
|
|
|
309
366
|
// ─── chat normalizer ──────────────────────────────────────────────
|
|
@@ -673,7 +730,7 @@ function applyEchoTags(messages, now) {
|
|
|
673
730
|
const TOOLS = [
|
|
674
731
|
{
|
|
675
732
|
name: 'list_accounts',
|
|
676
|
-
description: 'List all messaging accounts (networks) connected to this Beeper account. Each account corresponds to one platform — WhatsApp, Telegram, Discord, etc. Use this to see which platforms are reachable before calling other tools, or to discover what kinds of chats exist. Returns network slug (machine-readable, e.g. "whatsapp"), network label (human, e.g. "WhatsApp"), the underlying account ID, and the user\'s display name on that platform.',
|
|
733
|
+
description: 'List all messaging accounts (networks) connected to this Beeper account. Each account corresponds to one platform — WhatsApp, Telegram, Discord, etc. Use this to see which platforms are reachable before calling other tools, or to discover what kinds of chats exist. Returns network slug (machine-readable, e.g. "whatsapp"), network label (human, e.g. "WhatsApp"), the underlying account ID, the bridge connection `status` ("connected", "connecting", etc. — lets you tell a still-syncing account from a real one rather than reading a transient as "gone"), and the user\'s display name on that platform.',
|
|
677
734
|
inputSchema: {
|
|
678
735
|
type: 'object',
|
|
679
736
|
properties: {},
|
|
@@ -827,17 +884,15 @@ const TOOLS = [
|
|
|
827
884
|
async function callTool(name, args) {
|
|
828
885
|
switch (name) {
|
|
829
886
|
case 'list_accounts': {
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
},
|
|
840
|
-
}));
|
|
887
|
+
const list = accountList(await beeperFetch('/v1/accounts'));
|
|
888
|
+
// A live read, so a 0 here is the backend's current truth — but that is far
|
|
889
|
+
// more often "Beeper still syncing after a restart/account-add" than a real
|
|
890
|
+
// empty account set. Log it so the operator can tell the difference instead
|
|
891
|
+
// of silently relaying an ambiguous empty list.
|
|
892
|
+
if (list.length === 0) {
|
|
893
|
+
process.stderr.write('[beeperbox-mcp] list_accounts: backend /v1/accounts returned 0 accounts — Beeper may still be syncing (retry shortly)\n');
|
|
894
|
+
}
|
|
895
|
+
return list.map(normalizeAccount);
|
|
841
896
|
}
|
|
842
897
|
|
|
843
898
|
case 'get_chat': {
|
|
@@ -1173,8 +1228,7 @@ async function preflight() {
|
|
|
1173
1228
|
return;
|
|
1174
1229
|
}
|
|
1175
1230
|
try {
|
|
1176
|
-
const
|
|
1177
|
-
const list = Array.isArray(accounts) ? accounts : (accounts?.items || []);
|
|
1231
|
+
const list = accountList(await beeperFetch('/v1/accounts', { timeoutMs: PREFLIGHT_TIMEOUT_MS }));
|
|
1178
1232
|
say(`preflight OK: ${BEEPER_API} reachable, token accepted, ${list.length} account(s)`);
|
|
1179
1233
|
} catch (e) {
|
|
1180
1234
|
say(`preflight FAIL: ${BEEPER_API} unreachable or token rejected — ${e.message}`);
|
|
@@ -1310,6 +1364,7 @@ module.exports = {
|
|
|
1310
1364
|
selectDelivery,
|
|
1311
1365
|
textHash,
|
|
1312
1366
|
normalizeAttachments,
|
|
1367
|
+
normalizeAccount,
|
|
1313
1368
|
assertServableSrcUrl,
|
|
1314
1369
|
matchSentMessage,
|
|
1315
1370
|
recordSent,
|
|
@@ -1317,4 +1372,8 @@ module.exports = {
|
|
|
1317
1372
|
loadLedger,
|
|
1318
1373
|
// test hook: drop the in-memory ledger so a test can re-load from a fresh path
|
|
1319
1374
|
_resetLedger: () => { ledger = null; ledgerPersistWarned = false; },
|
|
1375
|
+
// account-map cache surface (resilience): the map + its TTL/empty-cache rules.
|
|
1376
|
+
getAccountMap,
|
|
1377
|
+
_resetAccountCache: () => { accountCache = null; },
|
|
1378
|
+
_setNow: (fn) => { nowFn = fn || (() => Date.now()); },
|
|
1320
1379
|
};
|