pi-zalo-plus
Full Zalo control of pi coding agent — commands, interactive UI, model/session management, file transfer, and real-time streaming output, all from Zalo
Package details
Install pi-zalo-plus from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-zalo-plus- Package
pi-zalo-plus- Version
1.0.1- Published
- Sep 7, 2026
- Downloads
- 194/mo · 10/wk
- Author
- trongthanh
- License
- MIT
- Types
- extension
- Size
- 159 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
],
"image": "https://raw.githubusercontent.com/trongthanh/pi-zalo-plus/main/docs/img/pi-zalo-plus.png"
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-zalo-plus
Overview
Full Zalo control of pi coding agent — commands, interactive UI, model/session management, file transfer, and real-time streaming output, all from Zalo.
pi-zalo-plus is a pi extension that turns Zalo into a full-featured remote control surface for the pi coding agent. It mirrors the core pi TUI experience into Zalo chat, with interactive menus, file attachments, live agent output rendering, and safe single-user pairing. Built on the Zalo Bot Platform long-polling API.
| Tool Rendering On | Tool Rendering Off |
Setup
Create a bot at bot.zaloplatforms.com and save its token to
~/.pi/agent/zalo.json(this single file also holds the extension state):{ "bot_token": "<YOUR_BOT_TOKEN>" }Migrating from an older install? A legacy
~/.pi/agent/zalo-bot.jsonis imported intozalo.jsonon the next start and then removed automatically.Install this extension at
~/.pi/agent/extensions/pi-zalo-plus/(already in place). It loads automatically with pi.Start pi. On first run the extension resolves the bot name via
getMe, generates a one-time pairing code and shows it:Zalo pairing required. Send this message to the bot from your Zalo account: /pair 625967Send
/pair <code>to your bot from your Zalo account (or/start <code>). The first account to do so is remembered in~/.pi/agent/zalo.jsonand becomes the only user allowed to talk to the bot.No pairing? Run
/zalo open(or set"open_access": truein zalo.json) to let any Zalo user talk to the bot — no pairing ritual needed. Messages are still answered with the current pi session's context.
Configuration reference (zalo.json)
Everything lives in one file, ~/.pi/agent/zalo.json (permissions 0600). It
mixes settings you may edit with state the extension maintains — keys are
snake_case; legacy camelCase keys from older installs are still read as a
fallback. For the token only, the aliases token / zalo_token / zaloToken
are also accepted on read.
Full example with every key (copy only what you need):
{
"bot_token": "<YOUR_BOT_TOKEN>",
"bot_name": "Bot AI Thien An",
"zalo_enabled": true,
"open_access": false,
"allowed_user_id": "ab19c1b361fa88a4d1eb",
"message_mode": "queue",
"tool": "brief",
"thinking": "brief",
"download_dir": "~/.pi/agent/attachments",
"pairing_code": "625967",
"active_chat_id": "ab19c1b361fa88a4d1eb",
"last_update_id": 1234567890
}
Settings you may edit
| Key | Type | Default | Description |
|---|---|---|---|
bot_token |
string | — (required) | Bot token from bot.zaloplatforms.com. Without it the extension stays disabled. |
zalo_enabled |
boolean | true (when a token is set) |
Master switch. Written by /zalo on and /zalo off. |
open_access |
boolean | false |
true = serve any Zalo user without pairing (/zalo open); false = only the paired user is served (/zalo locked). |
allowed_user_id |
string | — | Zalo user id allowed to talk to the bot. Set automatically by a successful /pair; can also be pasted by hand (get your id by messaging the bot once and checking the log). |
message_mode |
"steer" | "queue" |
"steer" |
Messages arriving while π is working: "steer" injects them into the running turn; "queue" holds them and runs them as the next turn. |
tool |
"hidden" | "brief" | "full" |
"brief" |
Detail level for tool-execution lines in chat ("hidden" suppresses them). |
thinking |
"hidden" | "brief" | "full" |
"brief" |
Detail level for thinking blocks in chat ("hidden" suppresses them). |
download_dir |
string | working directory | Directory incoming attachments are downloaded into. Supports ~ expansion; relative paths resolve against ~/.pi/agent. Useful to keep large/temporary attachments out of synced folders. |
bot_name |
string | auto | Bot display name, resolved once via getMe on first start. Informational only — shown in /status. |
State keys (managed by the extension)
| Key | Type | Description |
|---|---|---|
pairing_code |
string | One-time 6-digit code shown while the bot is unpaired; generated automatically, cleared after a successful /pair, regenerated by /zalo pair. |
active_chat_id |
string | Last chat that messaged the bot; /status replies here when triggered from the TUI. |
last_update_id |
number | Long-polling offset. Deleting the key (or /zalo reset) makes the next poll replay undelivered updates. |
Environment variables
| Variable | Default | Description |
|---|---|---|
PI_ZALO_API_ROOT |
https://bot-api.zapps.me/bot |
Zalo Bot API root (see the endpoint note in Troubleshooting). |
PI_ZALO_PLUS_LOG_LEVEL |
info |
Log verbosity for ~/.pi/agent/logs/pi-zalo-plus-YYYY-MM-DD.log: debug | info | warn | error. |
Hand edits are picked up live: the poller re-reads zalo.json before every
long-poll cycle, and every /zalo … command persists atomically (temp file +
rename) with mode 0600.
Zalo chat commands
| Command | Effect |
|---|---|
/status |
Bot + session status |
/help |
Command help |
/stop |
Interrupt the running turn / cancel a pending dialog |
/cancel |
Cancel a pending dialog (e.g. a select) |
| any other text | Sent to π as a prompt |
Slash commands from pi and other extensions (/new, /model, /compact, /zalo, …)
also work from chat: unknown commands are forwarded to the session's command registry.
TUI command
/zalo [status|on|off|pair|unpair|open|locked|reset] — manage the
bot from the terminal: toggle polling, (re)generate the pairing code, open = skip
pairing and accept messages from any Zalo account, locked = require pairing again,
unpair, or reset the update offset so undelivered updates are replayed.
Behavior notes
- Pairing / security: by default only the paired Zalo user id is served;
everyone else is silently ignored. In open-access mode (
/zalo open) any user who messages the bot is served and answered with the current session's context — use only when you control who can find the bot, since anyone may then run prompts/commands with the permissions of the pi session. - Steer / queue: incoming messages while π is working are steered into the
running turn by default (
message_mode: "steer"; set"queue"in zalo.json to chain them instead)./stopaborts the current turn. - Dialogs without keyboards: the Zalo Bot API has no inline keyboards/callback
queries, so pi's
select/confirm/input/editordialogs are rendered as text (reply with a number / yes-no / free text;/cancelorcancelaborts). Custom TUI dialogs are not supported over chat and resolve as cancelled. - Tool and thinking rendering: by default, tool calls and thinking blocks render
as brief inline lines. Set
tool=hiddenorthinking=hiddenin/zalo-config(or"tool": "hidden"/"thinking": "hidden"in zalo.json) to suppress them. - Output rendering: assistant markdown is converted to the Zalo-supported HTML
subset (bold/italic/strike/lists/headings); messages longer than 2000 chars are
split at line boundaries; thinking and tool calls render at their configured
render levels (
hidden|brief|full). - Typing indicator is sent while a turn is active.
- Single poller: a cross-process lock (
~/.pi/agent/zalo-poll-*.lock) ensures only one pi instance polls the bot; the 408 "Request timeout" response is treated as a normal empty poll. - Files: image/file attachments sent to the bot are downloaded into the
directory configured by
download_dir(default: the working directory) and their paths are appended to the prompt. Base64 image outputs from π are saved under.pi-zalo-images/in the working directory (ZalosendPhotoaccepts URLs only).
Files
~/.pi/agent/zalo.json— the single config/state file (mode 0600); see Configuration reference for every key. The legacy token-onlyzalo-bot.jsonis migrated here on startup and removed.~/.pi/agent/logs/pi-zalo-plus-YYYY-MM-DD.log— JSON-lines log (level viaPI_ZALO_PLUS_LOG_LEVEL=debug|info|warn|error)
Troubleshooting
401 Unauthorized in logs → the
bot_tokeninzalo.jsonwas regenerated in the bot console; paste the new token and restart pi."Zalo polling skipped: another local pi instance is already polling" → a second pi session is open with the same token. Close it, or remove the stale
~/.pi/agent/zalo-poll-*.lockdirectory (safe when no other instance runs)./pairor messages never arrive while polling is healthy → known Zalo platform issue observed with brand-new BASIC bots: after the very first delivered event, inbound delivery can stop entirely (outboundsendMessagekeeps working,getUpdateskeeps returning 408-empty). Resetting the token fixed routing once; the durable fix was recreating the bot and pasting the new token. Verify direction with:TOKEN=$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.pi/agent/zalo.json')))['bot_token'])") # outbound (ask the recipient to confirm it lands): curl -s -X POST "https://bot-api.zapps.me/bot${TOKEN}/sendMessage" \ -H 'Content-Type: application/json' \ -d '{"chat_id":"<user_chat_id>","text":"ping"}' # inbound (send the bot a message from Zalo, expect a result array): curl -s -X POST "https://bot-api.zapps.me/bot${TOKEN}/getUpdates" \ -H 'Content-Type: application/json' -d '{"timeout":5}'Endpoint note: the official node-zalo-bot SDK defaults to
https://bot-api.zapps.me— that host carries the inbound update queue.bot-api.zaloplatforms.comserves getMe/sendMessage but its getUpdates stays empty, which makes the bot look deaf while outbound still works. The extension defaults tozapps.me(override withPI_ZALO_API_ROOT).
Thanks
- @badlogic for the great minimal pi-coding-agent
- This plugin get much inspiration from @jalyfeng's pi-telegram-plus
©️ 2026 @trongthanh (thanh.im)
