PiRelay is a Telegram bridge for Pi sessions.
It pairs a private Telegram chat to the exact Pi session you are using, then lets you:
- watch Pi progress from your phone
- receive completion, failure, and abort notifications
- send prompts and screenshots/photos back into the same live Pi session
- inspect summaries, full output, or latest image artifacts
- answer structured follow-up questions from Telegram
- manage multiple paired Pi sessions through one bot
The npm package is pirelay. The canonical Pi command family is /relay ...; the old /telegram-tunnel ... namespace has been removed.
PiRelay connects Telegram to a live Pi session without replacing the Pi terminal UI.
Typical flow:
- start working in Pi locally
- run
/relay connect telegram - scan the QR code with Telegram
- press Start in the bot chat
- approve the pairing locally if prompted
- receive updates and send prompts from Telegram while the same Pi session stays usable locally
That means Telegram becomes a mobile companion for the current session, not a separate hidden agent.
- pairs a private Telegram or Discord DM with the current Pi session
- uses expiring, single-use pairing payloads
- restores non-secret binding metadata when the session resumes
- supports multiple concurrently registered Pi sessions through a local broker
- exposes
/sessions//use <session>on Telegram andrelay sessions/relay use <session>on Discord when more than one session is paired to the same chat
- plain Telegram or Discord DM text becomes a Pi prompt when the session is idle
- Telegram photos and supported image documents become Pi image prompts when the current model supports image input
- while Pi is busy, Telegram or Discord text/image prompts are queued as a follow-up by default
/steer <text>and/followup <text>on Telegram, orrelay steer <text>andrelay followup <text>on Discord, provide explicit delivery control- Telegram supports slash-style remote commands such as
/status; Discord's reliable DM command form isrelay status,relay full,relay sessions, etc. Bare Discord/status-style aliases are best-effort because Discord may route slash commands to another app.
- accepted remote prompts trigger Telegram
typing... - activity refreshes while Pi is still working
- if chat actions fail, PiRelay falls back to safe textual acknowledgements
- queued busy-session messages still say clearly that the message was queued
- preserves important trailing decision blocks instead of only sending a head-only preview
- detects structured choices and question sets in the latest completed assistant output
- shows recognized choices as Telegram inline buttons when available
- supports direct option replies when choices are recognized
- supports a Custom answer button that captures your next Telegram message
- adds one-click Show in chat and Download .md buttons when the latest assistant output is longer than the inline preview, without duplicating them across summary and decision messages
- reformats Markdown tables into mobile-friendly code-style blocks for Telegram chat
- supports an explicit Telegram answer draft via
answer - supports
cancelto leave the active answer flow
- accepts Telegram photos and image documents (
image/jpeg,image/png,image/webpby default) after chat/user authorization - uses the Telegram caption as the prompt text, or a safe image-inspection fallback for image-only messages
- rejects image prompts when the selected Pi model does not advertise image input support instead of silently dropping the image
- exposes latest tool-result image outputs and safe latest-turn workspace image file references with
/imagesor inline image buttons - supports
/send-image <relative-path>for explicit delivery of a validated workspace PNG/JPEG/WebP file - sends outbound images as Telegram documents to avoid recompression
- does not automatically echo local/remote input images or browse arbitrary workspace files
- bot token is loaded from environment or local config, never from session history
- pairing nonces are short-lived and single-use
- only non-secret binding metadata is persisted in Pi session history
- unauthorized users are rejected before any prompt reaches Pi
- Telegram Bot API limitations are handled with chunking, retry/backoff, and offline/busy responses
- Pi installed and working
- Node.js compatible with this package (
>=20.6.0) - a Telegram bot token from BotFather
- a private Telegram chat with that bot
Install from npm after the package is published:
pi install npm:pirelayFor a one-off run without adding it to settings:
pi -e npm:pirelayInstall from a local checkout during development:
pi install /absolute/path/to/pirelayOr add the package to your Pi package/settings configuration.
Use @BotFather in Telegram. Telegram's official BotFather guide is at https://core.telegram.org/bots/features#botfather.
- run
/newbot - choose a bot name
- choose a unique bot username
- copy the bot token
Recommended:
export TELEGRAM_BOT_TOKEN="<your-bot-token>"Alternative local config file:
{
"botToken": "<your-bot-token>",
"busyDeliveryMode": "followUp",
"allowUserIds": [123456789],
"summaryMode": "deterministic"
}Default config path:
~/.pi/agent/pirelay/config.json
Recommended permissions:
chmod 600 ~/.pi/agent/pirelay/config.jsonIn Pi:
/relay doctor
/relay setup telegram
/relay doctor checks channel readiness and config/state permissions without printing secrets. /relay setup telegram checks the token and caches the bot identity.
In Pi:
/relay connect telegram
/relay connect telegram is the canonical pairing command.
Then:
- scan the QR code or open the Telegram deep link
- press Start in Telegram
- approve the pairing locally if Pi asks for confirmation
After that, the Telegram chat is bound to the current Pi session.
PiRelay adds the following Pi-side commands:
| Command | Purpose |
|---|---|
/relay setup telegram |
validate the bot token and cache the bot username |
/relay connect telegram [name] |
create a QR/deep-link pairing flow for the current session with an optional display label |
/relay disconnect |
revoke the active binding |
/relay status |
show local tunnel status |
/relay setup <telegram|discord|slack> |
show secret-safe channel setup guidance and readiness diagnostics |
/relay connect <telegram|discord|slack> [name] |
create an expiring pairing instruction for the selected channel |
/relay doctor |
diagnose configured relay channels, credentials, allow-lists, and config/state permissions |
/relay trusted / /relay untrust <messenger> <userId> |
inspect or revoke locally trusted relay users |
/relay disconnect / /relay status |
generic aliases for Telegram disconnect/status compatibility |
Discord and Slack foundations are opt-in. Discord now includes a live DM-first bot runtime when discord.enabled and discord.botToken are configured; run /relay setup discord for the interactive setup wizard with credential, intent, invite/QR, and DM troubleshooting guidance. In headless/no-UI contexts, /relay setup <messenger> falls back to secret-safe plain text guidance. Slack remains an adapter foundation until a live Slack runtime is wired. Guild/channel control requires explicit authorization config.
The interactive setup wizard is read-only for secrets: it shows readiness, links, QR/invite helpers, doctor-style findings, and copy-paste snippets that use placeholders or environment variable names. It does not write bot tokens, signing secrets, OAuth secrets, or peer secrets.
Credential starting points:
- Telegram: create a bot with BotFather, then set
TELEGRAM_BOT_TOKEN(https://core.telegram.org/bots/features#botfather). - Discord: create an application/bot in the Discord Developer Portal, copy the bot token to
PI_RELAY_DISCORD_BOT_TOKENordiscord.botToken, enable Message Content Intent for plain DM prompts andrelay <command>text controls, and copy the Application ID toPI_RELAY_DISCORD_APPLICATION_IDordiscord.applicationId(PI_RELAY_DISCORD_CLIENT_ID/discord.clientIdare accepted aliases) so/relay connect discordcan show a QR link to the bot profile/DM (https://discord.com/developers/docs/quick-start/getting-started). Invite with thebotscope andpermissions=0for DM-first operation;applications.commandsis only needed for a future native/relay <subcommand>UX. Pairing uses a short PIN (relay pair 123-456) and asks the local Pi user to allow once, trust the user, or deny unless the Discord user is already allow-listed/trusted. - Slack: create a Slack app, install it to your workspace, set the Bot User OAuth Token as
PI_RELAY_SLACK_BOT_TOKENorslack.botToken, and set the Signing Secret asPI_RELAY_SLACK_SIGNING_SECRETorslack.signingSecret(https://api.slack.com/apps).
Once paired, Telegram and Discord DMs support the same canonical command semantics with platform-specific invocation syntax. Telegram uses slash-style commands. Discord's reliable baseline is ordinary DM text prefixed with relay; bare Discord /status-style aliases are best-effort only because Discord may route slash commands to another app.
| Telegram | Discord reliable form | Purpose |
|---|---|---|
/help |
relay help |
show available PiRelay commands |
/status |
relay status |
show the session dashboard with identity, online/offline state, busy/idle state, model, progress mode, recent activity, and quick-action buttons |
/sessions |
relay sessions |
list paired Pi sessions for this chat with number, alias/label, online/offline state, active marker, and dashboard buttons |
/use <session> |
relay use <session> |
switch the active session by number, label, or session id prefix |
/forget <session> |
relay forget <session> |
remove an offline paired session from the session list |
/to <session> <prompt> |
relay to <session> <prompt> |
send a one-shot prompt to a session without changing the active session |
/progress <quiet|normal|verbose|completion-only> |
relay progress <quiet|normal|verbose|completion-only> |
set per-session progress notification noise |
/alias <name|clear> |
relay alias <name|clear> |
set or clear a chat-friendly session alias |
/recent or /activity |
relay recent or relay activity |
show recent safe progress/lifecycle activity |
/summary |
relay summary |
show the latest concise summary |
/full |
relay full |
show the latest assistant output using the active messenger's chunking/file fallback |
/images |
relay images |
download latest captured image outputs or safe image files referenced by the latest completed turn |
/send-image <path> |
relay send-image <path> |
send a validated workspace PNG/JPEG/WebP file by relative path |
/steer <text> |
relay steer <text> |
queue steering text while Pi is running |
/followup <text> |
relay followup <text> |
queue an explicit follow-up |
/abort |
relay abort |
request cancellation of the current run |
/compact |
relay compact |
trigger Pi context compaction |
/pause |
relay pause |
pause remote delivery |
/resume |
relay resume |
resume remote delivery |
/disconnect |
relay disconnect |
revoke the current chat binding |
A normal Telegram text message is delivered as a standard Pi prompt. A Telegram photo or supported image document is delivered as an image prompt when the selected Pi model supports image input.
Expected Telegram behavior:
- the bot shows
typing...while Pi starts working - no noisy delivery acknowledgement is sent unless Telegram chat actions fail
A normal Telegram text message, photo, or supported image document is delivered using the configured busy mode.
Default:
followUp
Expected Telegram behavior:
- the bot may continue showing
typing...for the active run - PiRelay replies with a clear queue acknowledgement such as:
Pi is busy; your message was queued as followUp.
Use these when you want to override the default:
/steer <text>/followup <text>
When sending a photo or image document, put /steer ... or /followup ... in the Telegram caption to choose the delivery mode explicitly.
PiRelay supports structured answering from Telegram when the latest completed assistant output contains a reliable choice set or question set.
If Pi ends with something like:
Choose:
1. sync specs now
2. archive without syncing
Telegram can:
- tap an inline option button when the Telegram client supports buttons
- reply directly with a short unambiguous option such as
1or2 - reply with an explicit answer phrase such as
option 1,choose B, oranswer 2 - reply with the option text if it matches cleanly and does not look like a new prompt
- tap Custom answer and send a free-form answer as the next message
- send
answerto open a normalized answer draft
Long, question-like, Markdown-like, or instruction-like Telegram messages are treated as normal prompts by default, even if the latest Pi output had answer buttons. If a short reply is ambiguous, PiRelay asks whether to send it as a prompt, answer the previous question, or cancel.
It also supports inline lettered choices such as:
What should we do next? A) test B) commit C) ship
If Pi ends with explicit questions, answer opens a draft like:
Q1: What environment should we target?
A1:
Q2: Do we archive immediately?
A2:
You can then:
- fill in the
A1:/A2:template and send it back - or answer step-by-step as prompted
PiRelay is intentionally conservative.
If the latest output is not reliably structured:
- it will not enter answer mode
- it will tell you to use
/fullor send a normal text reply instead
Send:
cancel
Completion or decision messages include inline full-output actions when the latest assistant message is longer than the inline preview:
- Show in chat sends the latest assistant message as Telegram-sized chunks
- Download .md sends the latest assistant message as a Markdown attachment
PiRelay shows those actions only once per completed turn. If a structured decision/options message follows the completion summary, the decision message owns the buttons and the summary stays lightweight. Short completions that already fit in the preview avoid redundant buttons. Both actions use only the latest assistant message, not tool logs or the full session transcript. /full remains available as a text-command fallback.
When the chat view contains Markdown tables, PiRelay reformats them into aligned code-style blocks because Telegram does not render Markdown tables natively. The Download .md action keeps the original Markdown table formatting, aside from configured secret redaction.
PiRelay uses one local authoritative broker per bot token so multiple active Pi sessions on the same machine can share one Telegram bot safely.
Internally, PiRelay uses channel adapters and interaction middleware so Telegram-specific transport stays separate from reusable session routing, authorization, output retrieval, guided answer, media, redaction, progress, and future accessibility behavior. Telegram remains fully compatible. Discord and Slack adapter foundations are available for DM-first relay integrations with injected/mockable platform clients; Telegram remains the default packaged runtime. See docs/adapters.md for the adapter and middleware boundaries.
Pair sessions with short labels when useful:
/relay connect telegram docs
/relay connect telegram api
When no label is provided, PiRelay uses the Pi session name when available, then the project folder name, then the session file basename, then a short session id fallback.
If one Telegram chat is paired to multiple sessions:
- use
/sessionsto list numbered sessions with stable visual markers, aliases/labels, active marker, online/offline state, idle/busy state, model, last activity, and dashboard buttons - use
/use <number|alias|label>to pick the active target - use
/forget <number|label>to remove an offline paired session from the list - use
/to <session> <prompt>for a one-shot prompt without changing the active session; quote labels that contain spaces, for example/to "docs team" run tests
Duplicate labels are allowed; /sessions adds short identifiers when needed and numeric selection always works. Lightweight markers such as 🔵 or 🟢 are derived from stable session identity and de-duplicated within the current session list when possible, so multi-session notifications are easier to distinguish without storing extra state. Ordinary prompts are not guessed when multiple live sessions exist without a selected active session.
PiRelay runs one local broker per machine. If the same bot/account is configured on multiple machines, configure one ingress owner and broker federation so other machines register their routes instead of polling the bot concurrently.
Canonical config lives at ~/.pi/agent/pirelay/config.json and uses namespaced messenger instances:
{
"relay": { "machineId": "laptop", "stateDir": "~/.pi/agent/pirelay", "brokerGroup": "personal" },
"defaults": { "pairingExpiryMs": 300000, "busyDeliveryMode": "followUp" },
"messengers": {
"telegram": { "default": { "enabled": true, "tokenEnv": "TELEGRAM_BOT_TOKEN" } },
"discord": { "personal": { "enabled": true, "tokenEnv": "PI_RELAY_DISCORD_BOT_TOKEN" } },
"slack": { "work": { "enabled": false, "tokenEnv": "PI_RELAY_SLACK_BOT_TOKEN", "signingSecretEnv": "PI_RELAY_SLACK_SIGNING_SECRET" } }
}
}Legacy Telegram tunnel config/state under ~/.pi/agent/telegram-tunnel and PI_TELEGRAM_TUNNEL_* env vars are migration fallbacks only. Active non-secret bindings migrate to telegram:default; active pairing codes are not copied.
For more detail, see docs/config.md.
Please treat PiRelay as a convenience/control channel, not a secret-safe transport.
Important points:
- only private Telegram chats are supported in the default runtime; Discord/Slack adapter foundations are DM-first and reject guild/channel control unless explicitly enabled by future runtime wiring
- Telegram Bot API traffic is not end-to-end encrypted
- bot tokens are not stored in Pi session history
- exported/shared Pi sessions only contain non-secret tunnel metadata
- pairing links are single-use and expire quickly
allowUserIdscan restrict which Telegram users may control the tunnel- redaction patterns can scrub common secret shapes before Telegram text/document delivery, including progress/recent-activity messages
- image files can contain visual secrets; PiRelay requires explicit
/images, image button, or/send-image <relative-path>action before sending latest image outputs/files back to Telegram /send-imageaccepts only relative workspace PNG/JPEG/WebP paths after containment, symlink, MIME, and size validation; it is not a file browser
- run
/relay setup telegram - verify
TELEGRAM_BOT_TOKEN - confirm the bot exists and the username resolves in Telegram
- run
/relay connect telegramagain - use the newly generated QR/deep link
- resume or reopen the paired Pi session locally
- if multiple sessions exist, use
/sessionsand/use <session>
- on Telegram clients, bot
typing...commonly appears in the top header under the bot name rather than next to the message bubble - if chat actions fail, PiRelay falls back to a textual acknowledgement
- the latest completed assistant output was not reliably recognized as structured
- use
/fullto inspect the full result - send a normal reply manually if needed
PiRelay uses a detached local broker process.
Restart it before retesting:
pkill -f 'extensions/relay/broker/entry.js'Then reload Pi.
PiRelay consists of:
- a Pi extension at
extensions/relay/ - a companion skill at
skills/relay/ - a local broker process for multi-session Telegram polling/routing
- persisted local state under
~/.pi/agent/pirelay/
The extension listens to Pi lifecycle events, tracks task state, publishes route updates to the broker, and injects authorized Telegram input back into the session.
npm install
npm run typecheck
npm testReal Telegram regression checks, manual smoke-test steps, and release notes live in:
- private chats only
- no Telegram group-chat support
- no end-to-end encryption beyond Telegram Bot API transport
- answer workflow depends on conservative structured-output detection
- image prompts require a Pi model that supports image input
- image transfer is bounded by configured size and MIME-type limits
/imagesonly considers captured image outputs and obvious image file paths mentioned in the latest Pi turn- one active OpenSpec implementation task still remains for broader integration-style coexistence/reconnect tests
- configuration reference: docs/config.md
- manual testing checklist: docs/testing.md
- Pi skill entrypoint: skills/relay/SKILL.md
