Asi beeper
Unified messaging via three access tiers — MCP (live API), beeper-cli (authenticated CLI), and direct SQLite→DuckDB (full archive). Search, analyze, and act across all networks. Subsumes beeper-mcp, messaging-world, and signal-messaging.
git clone https://github.com/plurigrid/asi
T=$(mktemp -d) && git clone --depth=1 https://github.com/plurigrid/asi "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/asi/skills/beeper" ~/.claude/skills/plurigrid-asi-beeper && rm -rf "$T"
plugins/asi/skills/beeper/SKILL.mdCRITICAL: TOKENS PAY RENT
Every output token must produce actionable value.
- NO PASSIVE SUMMARIES — extract ACTION ITEMS, DECISIONS, BLOCKERS → create artifacts
- NO AGREEMENT WITHOUT IMPLEMENTATION — "I agree" must be followed by code/file/commit
- NO RHETORICAL QUESTIONS — ask only when you cannot proceed without the answer
- Enforcement: If output contains summary without artifact, STOP and create the artifact first.
Beeper Unified Messaging
Access all messaging networks through three access tiers with increasing depth.
Three Access Tiers
Tier 1: Desktop API (MCP + HTTP) — real-time chat; send text + attachments Tier 2: beeper-cli (Auth) — paginated history, chat type metadata, contacts Tier 3: SQLite→DuckDB (Archive) — full offline archive, cross-platform analytics
Tier Selection Decision Tree
Need to SEND something? └─ Text only → Tier 1 (MCP send_message) └─ File/attachment → Tier 1 (Desktop API HTTP: upload + send) Need chat type (DM vs group)? └─ Yes → Tier 2 (beeper-cli has type: "single"|"group") Need full history or cross-platform JOIN? └─ Yes → Tier 3 (SQLite→DuckDB) Need real-time / recent? └─ Yes → Tier 1 (MCP) or Tier 2 (beeper-cli) Need contact name resolution? └─ MCP has senderName in messages └─ beeper-cli has title field on chats └─ SQLite has m.room.member displayname (most complete)
Tier 1: Desktop API (MCP + HTTP)
MCP Tools
| Tool | Purpose |
|---|---|
| Search chats by title/network or participants |
| Get chat metadata (participants, last activity) |
| List messages in a chat (paged) |
| Search messages (literal word match, limit ≤ 20) |
| Send a text message |
| Open Beeper Desktop, prefill draft text/attachment |
| Archive/unarchive a chat |
| Set reminder for a chat |
mcp__beeper__search_chats query="contact name" mcp__beeper__list_messages chatID="..." mcp__beeper__send_message chatID="..." text="Hello!" mcp__beeper__focus_app chatID="..." draftText="..." draftAttachmentPath="/path/to/file"
Search is LITERAL WORD MATCHING, not semantic. Use single keywords.
Send Attachments (Programmatic)
MCP
send_message is text-only. For files:
(multipart) → returnsPOST /v1/assets/uploaduploadID
withPOST /v1/chats/{chatID}/messagesattachment.uploadID
skills/beeper/scripts/beeper_send_file.sh '<chat_id>' /path/to/file 'optional text'
User Identity
Beeper/Matrix has TWO identifiers per user:
- Matrix userID:
(permanent)@username:beeper.com - Display name: User-chosen (can differ)
Cross-reference
list_messages to map senderID ↔ senderName.
Tier 2: beeper-cli (Authenticated CLI)
# Auth pattern — secret never exposed to context BEEPER_ACCESS_TOKEN=$(fnox get BEEPER_ACCESS_TOKEN --age-key-file ~/.age/key.txt) \ beeper-cli <command> -o json # List chats (has type: "single" vs "group") beeper-cli chats list -o json # List messages from a chat beeper-cli messages list -o json --chat-id "..." # List connected accounts beeper-cli accounts list -o json # Filter by account (e.g. WhatsApp) beeper-cli chats list -o json --account-ids whatsapp
Connected Accounts
| Account | Network | Identity |
|---|---|---|
| hungryserv | Matrix | @zigger:beeper.com |
| local-telegram | Telegram | @physetermacrocephalus |
| +14153141554 | ||
| (system) | iMessage | via macOS bridge |
Caveats
- Pagination bug: cursor can cycle — always deduplicate on chat ID
- iMessage coverage: ~22 recent chats; chat.db has 354 DMs
- WhatsApp: must use
--account-ids whatsapp
Tier 3: SQLite→DuckDB (Full Archive)
Database Locations
| Database | Path | Contents |
|---|---|---|
| Beeper account.db | | Signal + Telegram via Matrix |
| Beeper index.db | | Full-text search index |
| iMessage chat.db | | All iMessage/SMS history |
DuckDB Inline Attach
INSTALL sqlite; LOAD sqlite; ATTACH '~/Library/Application Support/BeeperTexts/account.db' AS beeper (TYPE sqlite, READ_ONLY); ATTACH '~/Library/Messages/chat.db' AS imessage (TYPE sqlite, READ_ONLY);
Unified DM Landscape Query
WITH beeper_dms AS ( SELECT le.room_id, COUNT(*) FILTER (WHERE le.sender = '@zigger:beeper.com') AS my_msg_count, COUNT(DISTINCT le.sender) AS sender_count, CASE WHEN le.room_id LIKE '%.local-signal.%' THEN 'Signal' WHEN le.room_id LIKE '%.local-telegram.%' THEN 'Telegram' ELSE 'Other' END AS network FROM beeper.local_events le WHERE le.type = 'm.room.message' GROUP BY le.room_id HAVING COUNT(*) FILTER (WHERE le.sender = '@zigger:beeper.com') >= 3 AND COUNT(DISTINCT le.sender) <= 2 ), beeper_names AS ( SELECT DISTINCT ON (le.room_id) le.room_id, regexp_extract(CAST(le.content AS VARCHAR), 'displayname\\x22:\\x22([^\\]+)', 1) AS display_name FROM beeper.local_events le WHERE le.type = 'm.room.member' AND le.sender <> '@zigger:beeper.com' AND le.state_key <> '@zigger:beeper.com' AND CAST(le.content AS VARCHAR) NOT LIKE '%bridge bot%' ORDER BY le.room_id, le.event_ts DESC ), beeper_final AS ( SELECT bd.room_id AS id, COALESCE(NULLIF(bn.display_name, ''), 'unnamed') AS contact_name, bd.my_msg_count, bd.network, 'beeper_db' AS source FROM beeper_dms bd LEFT JOIN beeper_names bn ON bd.room_id = bn.room_id ), imessage_dms AS ( SELECT c.chat_identifier AS id, COALESCE(NULLIF(c.display_name, ''), c.chat_identifier) AS contact_name, COUNT(*) AS my_msg_count, 'iMessage' AS network, 'imessage_db' AS source FROM imessage.chat c JOIN imessage.chat_message_join cmj ON c.ROWID = cmj.chat_id JOIN imessage.message m ON cmj.message_id = m.ROWID WHERE m.is_from_me = 1 AND c.style <> 43 GROUP BY c.ROWID, c.chat_identifier, c.display_name HAVING COUNT(*) >= 3 ) SELECT * FROM beeper_final UNION ALL SELECT * FROM imessage_dms ORDER BY my_msg_count DESC;
Persisted Table
Results materialized in
~/i.duckdb as dm_landscape:
- Signal: 232 threads (8,569 msgs)
- iMessage: 119 threads (9,192 msgs)
- Telegram: 9 threads (156 msgs)
- Total: 360 threads, 17,917 messages
Schema Reference
beeper.local_events:
room_id, event_id, sender, type, state_key, content BLOB, event_ts INT
- Content BLOB uses
instead of\x22
— use"
notregexp_extractjson_extract_string
imessage.chat:
ROWID, chat_identifier, display_name, style (43=group)
imessage.message: ROWID, text, is_from_me, date, handle_id
Join via imessage.chat_message_join(chat_id, message_id).
ACSet Social Graph Schema
@present SchMessagingWorld(FreeSchema) begin Identity::Ob; Channel::Ob; Account::Ob; Contact::Ob; Conversation::Ob; Network::Ob identity_account::Hom(Account, Identity) account_channel::Hom(Account, Channel) contact_account::Hom(Contact, Account) conv_account::Hom(Conversation, Account) contact_network::Hom(Contact, Network) conv_network::Hom(Conversation, Network) Email::AttrType; Name::AttrType; Trit::AttrType; Count::AttrType contact_email::Attr(Contact, Email) contact_name::Attr(Contact, Name) channel_trit::Attr(Channel, Trit) contact_count::Attr(Contact, Count) end
Channel Trit Assignment
OUTLOOK (−1) → y.shkel@utoronto.ca (academic, validator) GMAIL ( 0) → greenteatree01@gmail (personal, coordinator) BEEPER (+1) → @greenteatree01:beeper (multi-protocol, generator) Σ = 0 ✓
Network Topology (50 Active Chats)
- Signal (18): ies, Meta-Org, Cognition Cosmos, Gay.jl, sloppies, Avalon, NYCies + DMs
- Telegram (15): Frontier Tower community channels
- WhatsApp (6): Stanford BJJ, family, salon
- Discord (5): string-parsing-mines, 7-calendar-years + DMs
- Matrix (3): Beeper Dev Community, Vivarium Public
Conversation Branch Tracking
CREATE TABLE IF NOT EXISTS beeper_conversation_branches ( branch_id VARCHAR PRIMARY KEY, chat_id VARCHAR NOT NULL, parent_branch_id VARCHAR, topic VARCHAR NOT NULL, first_message_id VARCHAR, last_message_id VARCHAR, status VARCHAR DEFAULT 'open', -- 'open', 'resolved', 'merged', 'stale' created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, resolved_at TIMESTAMP ); CREATE TABLE IF NOT EXISTS beeper_branch_transitions ( from_branch VARCHAR, to_branch VARCHAR, transition_type VARCHAR, -- 'fork', 'merge', 'abandon', 'resolve' message_id VARCHAR, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (from_branch, to_branch, message_id) );
Branch rules: Fork (one message → multiple topics), Merge (response addresses multiple), Resolve (explicit closure), Abandon (7 days inactive → stale).
Signal via Rust MCP (Direct Channel)
For Signal-only access without Beeper bridge:
{ "signal": { "command": "cargo", "args": ["run", "--release", "--example", "signal-server-stdio"], "cwd": "/Users/alice/signal-mcp", "env": { "RUST_LOG": "signal_mcp=info" } } }
Capabilities: send/receive messages, list conversations, handle attachments. Use
read_mcp_resource with signal:// URIs.
GF(3) Triadic Access Pattern
| Trit | Role | Tier | Action |
|---|---|---|---|
| MINUS (−1) | Validator | SQLite→DuckDB | Verify data exists locally before fetching |
| ERGODIC (0) | Coordinator | beeper-cli | Metadata, routing, account selection |
| PLUS (+1) | Generator | MCP | Send messages, fetch fresh data |
Resource-Aware Processing
NEVER pull full message history into context.
- Check DuckDB first (Tier 3) — zero network cost
- Use MCP for recent (Tier 1) — bounded by limit param
- Use beeper-cli for metadata (Tier 2) — chat types, accounts
Context budget: 10,000 chars. Always set
limit and dateAfter params.
MCP Server Config
{ "beeper": { "command": "/bin/sh", "args": ["-c", "BEEPER_ACCESS_TOKEN=$(fnox get BEEPER_ACCESS_TOKEN --age-key-file ~/.age/key.txt) exec npx -y @beeper/desktop-mcp"] } }
Requires:
fnox, age key at ~/.age/key.txt, npx in PATH, Beeper Desktop running.
Known Issues
- beeper-cli pagination cycles: cursor can loop — deduplicate on chat ID
- Beeper BLOB encoding:
escapes — use\x22regexp_extract - WhatsApp not in account.db: use beeper-cli for WhatsApp
- iMessage SQL: use
not<>
(shell escaping)!=