{"slug": "pi-msg-drive-pi-from-xmpp", "title": "Pi-Msg: Drive Pi from XMPP", "summary": "Pi-Msg, a new open-source bridge, lets users drive the Pi coding agent entirely from an XMPP chat client, relaying prompts and slash commands over 1:1 or group chats. The tool runs Pi in RPC mode, enabling commands like /new, and provides typing indicators, presence status, read receipts, and file upload support. It requires a config file with bot account credentials and an owner JID, and supports multiple accounts and MUC rooms.", "body_md": "Drive the [Pi](https://pi.dev) coding agent **entirely from an XMPP chat client** —\n1:1 or in a group chat (MUC).\n\n`pi-msg`\n\nlaunches `pi --mode rpc`\n\n, then bridges Pi's JSONL event stream to XMPP\n(via [mellium.im/xmpp](https://mellium.im/xmpp)): the assistant's replies are relayed\nto you as chat messages, and your chat messages drive the agent — plain prompts **and**\nslash commands, exactly as if you'd typed them into Pi locally.\n\nBecause it runs Pi in RPC mode, commands like `/new`\n\nwork over chat (an earlier\nin-process-extension version couldn't do this — `sendUserMessage`\n\ncan't invoke Pi's\ncommand layer).\n\n```\nsequenceDiagram\n    participant You as You (XMPP client)\n    participant Bridge as pi-msg\n    participant Pi as pi --mode rpc\n    You->>Bridge: \"fix the build\"\n    Bridge->>Pi: prompt\n    Pi-->>Bridge: message_end event\n    Bridge-->>You: assistant text\n    You->>Bridge: \"/new\"\n    Bridge->>Pi: {type:\"new_session\"}\n    Note over Pi: fresh session\n```\n\n- Each finished\n**assistant message**→ sent to you as chat. - Agent state shows on three independent signals (1:1): a\n**typing indicator** while a reply is actually being written, presence(`<show>`\n\n`dnd`\n\nwhile busy, available when idle), and a presence**status** label of the current activity (`thinking…`\n\n,`running: <cmd>`\n\n,`replying…`\n\n,`retrying…`\n\n,`listening`\n\n). When a run settles with**no** text you get a`✅ done (no reply) — your turn`\n\nnudge. - Messages you send are acknowledged with\n**read receipts**— XEP-0184 delivery receipts and XEP-0333 chat markers (`displayed`\n\n) — when the agent takes them in, if your client requests them. - Your chat messages → routed to Pi:\n\n| You send | Becomes |\n|---|---|\n| plain text | a prompt to the agent |\n`/skill:name …` , `/template …` , any extension command |\na prompt (Pi expands/runs it) |\n`/new` |\n`new_session` (fresh session; connection stays up) |\n`/compact [instructions]` |\n`compact` |\n`/model <provider/id>` or `/model <search>` |\n`set_model` |\n`/think <off|low|medium|high|…>` |\n`set_thinking_level` |\n`/abort` (or `/stop` ) |\n`abort` |\n`/dump` (or `/dump pretty` ) |\nsend the session transcript to the owner — raw JSONL, or `pretty` for indented per-record JSON (no LLM turn) |\n`/quit` (or `/exit` ) |\nshut down the bridge and Pi |\n\nCreate `~/.config/pi-msg/config.json`\n\n(override the path with `PI_MSG_CONFIG`\n\n), then\n`chmod 600`\n\nit:\n\n```\n{\n  \"accounts\": {\n    \"default\": {\n      \"jid\": \"pi@chat.example.com\",\n      \"password\": \"super-secret\",\n      \"owner\": \"you@chat.example.com\",\n      \"model\": \"anthropic/claude-sonnet-latest\",\n      \"workdir\": \"/path/to/your/project\"\n    }\n  }\n}\n```\n\nPer-account fields:\n\n| field | required | default | notes |\n|---|---|---|---|\n`jid` |\nyes | — | bare JID of the bot account |\n`password` |\nyes | — | bot account password |\n`owner` |\nyes | — | the human this account relays to; the canonical (trusted) driver |\n`service` |\nno | `<jid-domain>:5222` |\n`host:port` (a leading `xmpp://` is tolerated) |\n`resource` |\nno | `pi-msg` |\nXMPP resource (client-session label) |\n`model` |\nno | Pi's default | model pattern passed to `pi --model` |\n`workdir` |\nno | current dir | working directory for the agent (also where Pi discovers `AGENTS.md` /`CLAUDE.md` ) |\n`room` |\nno | — | a bare MUC JID (or an array of them) to also join for group chat (see below) |\n`nick` |\nno | JID localpart | occupant nickname used in the room(s) |\n`roomTrigger` |\nno | `nick` |\naddress prefix that makes a room message a prompt (e.g. `pi` → `pi: …` ) |\n`uploadService` |\nno | auto-probed | XEP-0363 upload component JID for file transfer (e.g. `upload.chat.example.com` ) |\n`pingInterval` |\nno | `60s` |\nkeepalive cadence (Go duration): XEP-0199 server ping + XEP-0410 MUC self-ping; `0` disables |\n`reactions` |\nno | `false` |\nXEP-0444 emoji reactions on 1:1 owner messages: lifecycle → 👀 picked up / ✅ done / ⛔ aborted, and enables the agent-driven `send_reaction` tool (see\n|\n`avatar` |\nno | — | path to a local image (PNG/JPEG/GIF) published as the bot's XEP-0153 vCard profile picture on connect |\n\nMultiple accounts: add more keys under `accounts`\n\n; `default`\n\nis used unless you set\n`PI_MSG_ACCOUNT=<name>`\n\n. In 1:1 mode only the `owner`\n\nJID may drive the agent.\n\nSet `room`\n\non an account (a single MUC JID, or an array of them) and pi-msg\n**also** joins each. **The owner's 1:1 stays the primary channel** — joining a\nroom is purely additive and doesn't change 1:1 behaviour (typing indicator,\nlifecycle notices, and unsolicited output all still go to the owner). Each reply\ngoes back to whichever channel the message arrived on, including the specific\nroom when several are joined. Room messages are handled on **two independent\naxes**:\n\n**Trigger**— does the message start/steer a turn?- the\n**owner**→ always - anyone else who\n**addresses the bot by name**(`pi: …`\n\n/`pi, …`\n\n) → always - all other chatter → never (it's buffered as ambient context)\n\n- the\n**Authority**— is the content trusted?- the\n**owner**→ canonical (authoritative) - everyone else, even when addressing the bot → untrusted\n*commentary*; the agent is told to use its judgment and is under no obligation to act on it\n\n- the\n\nUntriggered messages are buffered and, on the next turn, prepended to the prompt as a\nclearly-labeled *\"room commentary — non-canonical\"* block, then the buffer clears.\n\n**Reply routing (explicit from:/to:).** When an account has room access, routing is\nfully explicit — no guessing. Each prompt the agent receives leads with a header naming\nthe message's origin:\n\n```\nfrom: <channel jid>     # the room (group msg) or the owner (DM) — reply here to answer in place\nsender: <person jid>    # room messages only, when the real JID is known — reply here to DM them\n<message body>\n```\n\nAnd **every** agent reply must begin with a `to: <jid>`\n\nline naming its destination:\n\n`to: <room jid>`\n\n→ the group chat (groupchat)`to: <owner or occupant jid>`\n\n→ that person, 1:1\n\nOne reply may contain **several to: blocks** — each\n\n`to:`\n\nline starts a new message, so\nthe agent can fan a single turn out to multiple destinations:\n\n```\nto: team@muc.chat.zachmanson.com\nDeploying now — back in 5.\nto: zach@chat.zachmanson.com\n(privately: the staging creds are stale, heads up)\n```\n\nDestinations are **allowlisted**: the owner, joined room(s), and real JIDs currently seen\nin a room. A reply whose `to:`\n\nis missing or points anywhere else is sent to the owner, so\nnothing is silently lost — the agent can't message arbitrary users. In a pure 1:1 account\n(no room) there are no prefixes; replies just go to the owner.\n\n**File transfer.** The agent sends files with the ** send_file** tool (a structured tool\ncall, not in-band text — see\n\n[Agent tools](#agent-tools)below): pi-msg uploads the file via\n\n**XEP-0363 HTTP Upload** and sends the resulting URL as an\n\n**XEP-0066** out-of-band message, so the recipient's client shows a downloadable file. The destination is allowlisted (owner, joined rooms, known occupants) exactly like a\n\n`to:`\n\nreply. The upload component is discovered\nautomatically (`upload.<domain>`\n\n/ `httpupload.<domain>`\n\n) or set explicitly via the\n`uploadService`\n\nconfig field.**The room must be non-anonymous** (ejabberd: *\"Present real Jabber IDs to → anyone\"*,\noptionally *members-only*). The owner is recognized by real JID; in a semi-anonymous\nroom real JIDs are hidden, so the owner can't be distinguished and every message falls\nthrough to the untrusted/ambient tiers.\n\nBeyond reply text, the agent gets structured **tools** (registered by a small companion\nextension that pi-msg loads into `pi --mode rpc`\n\n, which relays each call back to pi-msg to\nperform the XMPP action):\n\n| Tool | What it does | Enabled when |\n|---|---|---|\n`send_reaction` |\nReact to the human's latest message with an emoji (XEP-0444) | `reactions` is on |\n`send_file` |\nUpload a local file and deliver it (XEP-0363 + XEP-0066); dest defaults to the current conversation, allowlisted | always |\n\nReply **routing** (`to:`\n\n) stays an in-band text convention (above); only these discrete\nside-effect actions are tools.\n\n```\ngo build -o pi-msg . && ./pi-msg     # from the repo\nnix run   github:zachpmanson/pi-msg    # run the bridge\nnix build github:zachpmanson/pi-msg    # build the package (bin: pi-msg)\n```\n\nDev shell (Go + gopls) via `nix develop`\n\n, or automatically with\n[direnv](https://direnv.net/) — the repo ships a `.envrc`\n\n(`use flake`\n\n); run\n`direnv allow`\n\nonce.\n\nSet `PI_MSG_DEBUG=1`\n\nto print connection/status/stderr diagnostics. On startup the bot\nsimply comes **online** in your roster (presence `listening`\n\n); on shutdown or a pi crash it\ngoes **offline** with a `<status>`\n\ndescribing why and when — pi-msg no longer posts chat\nbanners for these lifecycle events.\n\nRequirements: Go ≥ 1.26 (to build), and a `pi`\n\non `PATH`\n\nthat's logged into a provider\n(`pi`\n\n→ `/login`\n\n).\n\n- Pi runs tools autonomously (no built-in approval prompts). If some other extension\nraises a dialog (\n`select`\n\n/`confirm`\n\n/`input`\n\n/`editor`\n\n), pi-msg auto-dismisses it (nobody's at the TUI) and tells you over chat — so approval-gated tools are declined over the bridge. - Auth uses SASL SCRAM-SHA-256 (mellium negotiates it cleanly against ejabberd); STARTTLS is required first.", "url": "https://wpnews.pro/news/pi-msg-drive-pi-from-xmpp", "canonical_source": "https://github.com/zachpmanson/pi-msg", "published_at": "2026-08-05 05:51:51+00:00", "updated_at": "2026-08-05 06:23:21.167411+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["Pi", "pi-msg", "XMPP", "mellium.im/xmpp", "XEP-0184", "XEP-0333", "XEP-0363", "XEP-0444"], "alternates": {"html": "https://wpnews.pro/news/pi-msg-drive-pi-from-xmpp", "markdown": "https://wpnews.pro/news/pi-msg-drive-pi-from-xmpp.md", "text": "https://wpnews.pro/news/pi-msg-drive-pi-from-xmpp.txt", "jsonld": "https://wpnews.pro/news/pi-msg-drive-pi-from-xmpp.jsonld"}}