From asi
Access, search, analyze, and send messages across Beeper's unified networks using MCP API, beeper-cli, or SQLite/DuckDB archives. Useful for chat automation and cross-network analysis.
npx claudepluginhub plurigrid/asi --plugin asiThis skill uses the workspace's default tool permissions.
**Every output token must produce actionable value.**
Integrates Beeper Desktop API to send messages, search chats, add reactions, set reminders, handle attachments, and mark as read across WhatsApp, Telegram, Signal, Discord, Slack, Instagram, iMessage, and more.
Interacts with Channel Talk workspaces via CLI: read/send messages in user chats/groups/direct chats, list chats, snapshot data, manage multi-workspace auth with auto-extracted desktop app/browser credentials.
Reads conversations and new messages from macOS Messages SQLite database, sends messages via AppleScript. For messaging automation and integration.
Share bugs, ideas, or general feedback.
Every output token must produce actionable value.
Access all messaging networks through three access tiers with increasing depth.
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
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)
| Tool | Purpose |
|---|---|
search_chats | Search chats by title/network or participants |
get_chat | Get chat metadata (participants, last activity) |
list_messages | List messages in a chat (paged) |
search_messages | Search messages (literal word match, limit ≤ 20) |
send_message | Send a text message |
focus_app | Open Beeper Desktop, prefill draft text/attachment |
archive_chat | Archive/unarchive a chat |
set_chat_reminder | 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.
MCP send_message is text-only. For files:
POST /v1/assets/upload (multipart) → returns uploadIDPOST /v1/chats/{chatID}/messages with attachment.uploadIDskills/beeper/scripts/beeper_send_file.sh '<chat_id>' /path/to/file 'optional text'
Beeper/Matrix has TWO identifiers per user:
@username:beeper.com (permanent)Cross-reference list_messages to map senderID ↔ senderName.
# 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
| Account | Network | Identity |
|---|---|---|
| hungryserv | Matrix | @zigger:beeper.com |
| local-telegram | Telegram | @physetermacrocephalus |
| +14153141554 | ||
| (system) | iMessage | via macOS bridge |
--account-ids whatsapp| Database | Path | Contents |
|---|---|---|
| Beeper account.db | ~/Library/Application Support/BeeperTexts/account.db | Signal + Telegram via Matrix |
| Beeper index.db | ~/Library/Application Support/BeeperTexts/index.db | Full-text search index |
| iMessage chat.db | ~/Library/Messages/chat.db | All iMessage/SMS history |
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);
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;
Results materialized in ~/i.duckdb as dm_landscape:
beeper.local_events: room_id, event_id, sender, type, state_key, content BLOB, event_ts INT
\x22 instead of " — use regexp_extract not json_extract_stringimessage.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).
@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
OUTLOOK (−1) → y.shkel@utoronto.ca (academic, validator)
GMAIL ( 0) → greenteatree01@gmail (personal, coordinator)
BEEPER (+1) → @greenteatree01:beeper (multi-protocol, generator)
Σ = 0 ✓
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).
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.
| 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 |
NEVER pull full message history into context.
Context budget: 10,000 chars. Always set limit and dateAfter params.
{
"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.
\x22 escapes — use regexp_extract<> not != (shell escaping)