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

Packages

Package details

extension

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

  1. 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.json is imported into zalo.json on the next start and then removed automatically.

  2. Install this extension at ~/.pi/agent/extensions/pi-zalo-plus/ (already in place). It loads automatically with pi.

  3. 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 625967
    
  4. Send /pair <code> to your bot from your Zalo account (or /start <code>). The first account to do so is remembered in ~/.pi/agent/zalo.json and becomes the only user allowed to talk to the bot.

    No pairing? Run /zalo open (or set "open_access": true in 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). /stop aborts the current turn.
  • Dialogs without keyboards: the Zalo Bot API has no inline keyboards/callback queries, so pi's select / confirm / input / editor dialogs are rendered as text (reply with a number / yes-no / free text; /cancel or cancel aborts). 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=hidden or thinking=hidden in /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 (Zalo sendPhoto accepts URLs only).

Files

  • ~/.pi/agent/zalo.json — the single config/state file (mode 0600); see Configuration reference for every key. The legacy token-only zalo-bot.json is migrated here on startup and removed.
  • ~/.pi/agent/logs/pi-zalo-plus-YYYY-MM-DD.log — JSON-lines log (level via PI_ZALO_PLUS_LOG_LEVEL=debug|info|warn|error)

Troubleshooting

  • 401 Unauthorized in logs → the bot_token in zalo.json was 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-*.lock directory (safe when no other instance runs).

  • /pair or 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 (outbound sendMessage keeps working, getUpdates keeps 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.com serves getMe/sendMessage but its getUpdates stays empty, which makes the bot look deaf while outbound still works. The extension defaults to zapps.me (override with PI_ZALO_API_ROOT).

Thanks


©️ 2026 @trongthanh (thanh.im)