beeperbox 0.8.0 → 0.9.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 +43 -10
- package/package.json +2 -2
- package/server.js +82 -23
package/README.md
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
```
|
|
2
|
+
╭──────────────────────────────────╮
|
|
3
|
+
│ ╔╗ ╔═╗╔═╗╔═╗╔═╗╦═╗╔╗ ╔═╗ ╦ ╦ │
|
|
4
|
+
│ ╠╩╗╠╣ ╠╣ ╠═╝╠╣ ╠╦╝╠╩╗║ ║ ╚╦╝ │
|
|
5
|
+
│ ╚═╝╚═╝╚═╝╩ ╚═╝╩╚═╚═╝╚═╝ ╩ ╩ │
|
|
6
|
+
│ one agent ──→ 50+ messengers │
|
|
7
|
+
╰──────────────────────────────────╯
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://www.npmjs.com/package/beeperbox"><img src="https://img.shields.io/npm/v/beeperbox?label=npm&color=2a4f8c" alt="npm version"></a>
|
|
12
|
+
<a href="https://github.com/hamr0/beeperbox"><img src="https://img.shields.io/badge/source-github-2a4f8c" alt="source on GitHub"></a>
|
|
13
|
+
<img src="https://img.shields.io/badge/license-Apache%202.0-2a4f8c" alt="license: Apache 2.0">
|
|
14
|
+
</p>
|
|
2
15
|
|
|
3
16
|
**Run beeperbox's MCP verb server against a Beeper Desktop you already have open — no Docker, no Electron, no Xvfb.**
|
|
4
17
|
|
|
5
18
|
This is the *lite* half of [beeperbox](https://github.com/hamr0/beeperbox). The full project ships a Docker image with a headless Beeper Desktop inside; lite mode is the same single-file, zero-dependency MCP server pointed at a Beeper Desktop **you** run on your laptop. Identical verb surface, identical version — you just supply Beeper.
|
|
6
19
|
|
|
7
|
-
- **Always-on / VPS / no local Beeper?** Use the [Docker image](https://github.com/hamr0/beeperbox#quick-start).
|
|
20
|
+
- **Always-on / VPS / no local Beeper?** Use the [Docker image](https://github.com/hamr0/beeperbox#quick-start-container).
|
|
8
21
|
- **Beeper already open on your machine?** Use this.
|
|
9
22
|
|
|
10
|
-
##
|
|
23
|
+
## Quick start
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
2. **Developer API enabled:** Beeper → **Settings → Developers** → enable the API and create an access token (the same token the container uses).
|
|
14
|
-
|
|
15
|
-
## Run
|
|
25
|
+
**Prereqs:** Beeper Desktop running locally, with the Developer API enabled — Beeper → **Settings → Developers** → enable the API and create an access token (the same token the container uses).
|
|
16
26
|
|
|
17
27
|
```sh
|
|
18
28
|
BEEPER_TOKEN=your-token-here npx beeperbox
|
|
@@ -20,12 +30,22 @@ BEEPER_TOKEN=your-token-here npx beeperbox
|
|
|
20
30
|
|
|
21
31
|
That starts the MCP HTTP server on `http://127.0.0.1:23375`, pointed at the local Beeper Desktop API on `http://127.0.0.1:23373`. On boot it logs a one-line reachability verdict (`preflight OK: … N account(s)` or `preflight FAIL: …`) so a misconfigured token or API is obvious immediately.
|
|
22
32
|
|
|
23
|
-
For stdio transport (Claude Code, Cursor, Cline, Continue, bareagent):
|
|
33
|
+
For stdio transport (Claude Code, Cursor, Cline, Continue, [bareagent](https://npmjs.com/package/bare-agent)):
|
|
24
34
|
|
|
25
35
|
```sh
|
|
26
36
|
BEEPER_TOKEN=your-token-here npx beeperbox --stdio
|
|
27
37
|
```
|
|
28
38
|
|
|
39
|
+
## The 12 tools
|
|
40
|
+
|
|
41
|
+
One opinionated MCP verb layer over Beeper — every tool returns a normalized `Chat` / `Message` schema, propagates `chat_id` + `network` onto every message, and is documented in-schema for the model. Reach across all 50+ networks without knowing which bridge you're talking to.
|
|
42
|
+
|
|
43
|
+
- **Read / triage** — `list_accounts` · `list_inbox` · `list_unread` · `get_chat` · `read_chat` · `search_messages`
|
|
44
|
+
- **Write / act** — `send_message` · `note_to_self` · `react_to_message` · `archive_chat`
|
|
45
|
+
- **Watch / reach** — `poll_messages` (read-only watch primitive, restart-safe cursor, `source` echo-guard) · `download_asset` (attachment bytes; every message carries `attachments[]`)
|
|
46
|
+
|
|
47
|
+
Full schemas and usage in the [main README](https://github.com/hamr0/beeperbox#the-mcp).
|
|
48
|
+
|
|
29
49
|
## Config
|
|
30
50
|
|
|
31
51
|
| Env | Meaning | Default |
|
|
@@ -45,6 +65,19 @@ The server binds **loopback only** (`127.0.0.1`) by default, so it's safe with n
|
|
|
45
65
|
|
|
46
66
|
There's no Docker restart policy in lite mode. For an always-on setup, run it under `systemd` or `pm2`.
|
|
47
67
|
|
|
48
|
-
See the [full README](https://github.com/hamr0/beeperbox#
|
|
68
|
+
See the [full README](https://github.com/hamr0/beeperbox#the-mcp) and [docs/GUIDE.md](https://github.com/hamr0/beeperbox/blob/master/docs/GUIDE.md) for the complete tool reference and the container build.
|
|
69
|
+
|
|
70
|
+
## The bare ecosystem
|
|
71
|
+
|
|
72
|
+
Local-first, composable agent infrastructure. Same API patterns throughout — mix and match, each module works standalone.
|
|
73
|
+
|
|
74
|
+
- **[bareagent](https://npmjs.com/package/bare-agent)** — the think→act→observe loop. *Goal in → coordinated actions out.*
|
|
75
|
+
- **[bareguard](https://npmjs.com/package/bareguard)** — the single gate every action passes through. *Action in → allow / deny / ask-a-human out.*
|
|
76
|
+
- **[litectx](https://npmjs.com/package/litectx)** — code + memory graph with activation decay. *Query in → ranked context out.*
|
|
77
|
+
- **[barebrowse](https://npmjs.com/package/barebrowse)** — a real browser for agents. *URL in → pruned snapshot out.*
|
|
78
|
+
- **[baremobile](https://npmjs.com/package/baremobile)** — Android + iOS device control. *Screen in → pruned snapshot out.*
|
|
79
|
+
- **beeperbox** *(this)* — 50+ messaging networks via one MCP server. *Chat in → unified message stream out.*
|
|
80
|
+
|
|
81
|
+
## License
|
|
49
82
|
|
|
50
|
-
[Apache-2.0](https://github.com/hamr0/beeperbox/blob/master/LICENSE)
|
|
83
|
+
[Apache-2.0](https://github.com/hamr0/beeperbox/blob/master/LICENSE). Independent wrapper around Beeper Desktop, no affiliation with Beeper / Automattic.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeperbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"url": "git+https://github.com/hamr0/beeperbox.git",
|
|
35
35
|
"directory": "mcp"
|
|
36
36
|
},
|
|
37
|
-
"homepage": "https://github.com/hamr0/beeperbox#lite-mode",
|
|
37
|
+
"homepage": "https://github.com/hamr0/beeperbox#lite-mode-npx",
|
|
38
38
|
"bugs": "https://github.com/hamr0/beeperbox/issues",
|
|
39
39
|
"author": "hamr0",
|
|
40
40
|
"license": "Apache-2.0"
|
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
|
};
|