pi-agent-push

Pi extension: push agent idle / interrupted / need-input events to Bark, Feishu, WeCom, DingTalk, ntfy, or a generic webhook.

Packages

Package details

extension

Install pi-agent-push from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-agent-push
Package
pi-agent-push
Version
1.0.4
Published
Jul 27, 2026
Downloads
715/mo · 353/wk
Author
yp_wong
License
MIT
Types
extension
Size
83.4 KB
Dependencies
0 dependencies · 1 peer
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-agent-push

Push a message to your phone or group chat when the pi main agent settles: Bark, Feishu, WeCom, DingTalk, ntfy, or a generic Webhook. All payloads are plain text.

Install

pi install npm:pi-agent-push
# or from GitHub:
pi install git:github.com/kiim-wong/pi-agent-push

Restart pi or run /reload after install.

Config

Pick one:

  1. CLI (recommended)
    /push set ntfy topic=your-topic enabled=true
    /push test ntfy
    
  2. Config file
    Create config.json in the extension directory (see package config.example.json).
    Or point env PI_AGENT_PUSH_CONFIG at a custom path.

The repo / npm package does not ship real keys; keep your local config.json private.


Quick start

  1. Open config.json, fill in keys for the channels you want, set that channel's "enabled" to true
  2. Restart pi, or run /reload in the session
  3. /push test — prints HTTP results per channel
/push                         status
/push list                    list channels
/push get <channel>           view config (secrets redacted)
/push enable|disable <channel> toggle channel (writes config.json)
/push set <channel> k=v [k=v…] set topic/token/deviceKey/url…
/push test [channel]          test (optionally one channel)
/push events [k=on|off…]      global event toggles
/push on|off                  session master switch (not persisted)
/push help

Channel selector: ntfy / name / ntfy#1 / index.

/push set ntfy topic=my-topic token=tk_xxx enabled=true
/push enable ntfy
/push test ntfy

When it pushes

Event Trigger Default Message
idle Model finished, auto-retry and auto-compaction done, pi is truly waiting for you on pi ready · output finished, waiting for input
interrupted Turn cancelled with Esc on pi interrupted · turn cancelled
interrupted Turn errored (after retries exhausted) on pi interrupted · runtime error: 401 authentication_error: invalid x-api-key
needInput A question tool (ask_user_question, plan_mode_question) was called on pi needs input · pi is waiting for your answer
exit Session exit (Ctrl+C / Ctrl+D / /quit) off pi exited · session ended

Both idle and interrupted are decided inside pi's agent_settled event, so auto-retries do not spam — one user-visible "pi settled" maps to one message.

Channel config

config.json channels is an array; you can configure many (including multiple of the same type). Every channel supports name (log display name), enabled, timeoutMs, and events (overrides global toggles).

Bark (iOS)

{ "type": "bark", "enabled": true, "deviceKey": "$BARK_KEY",
  "server": "https://api.day.app", "sound": "bell", "group": "pi", "level": "active" }

deviceKey is the key shown in the app. For a self-hosted bark-server, change server (uses POST /:device_key, compatible with V1/V2 servers).

Feishu group bot

{ "type": "feishu", "enabled": true,
  "url": "https://open.feishu.cn/open-apis/bot/v2/hook/xxxx", "secret": "" }

secret is only needed when signature verification is enabled. With custom keywords, put the keyword in template.

WeCom group bot

{ "type": "wecom", "enabled": true,
  "url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx" }

DingTalk group bot

{ "type": "dingtalk", "enabled": true,
  "url": "https://oapi.dingtalk.com/robot/send?access_token=xxxx", "secret": "SECxxxx" }

For sign mode, set secret; for keyword mode, put the keyword in template, otherwise DingTalk returns errcode=310000 (the plugin logs this as a failure).

ntfy

{ "type": "ntfy", "enabled": true, "topic": "$NTFY_TOPIC",
  "server": "https://ntfy.sh", "token": "$NTFY_TOKEN",
  "priority": "default", "tags": "pi,computer" }

On the public server, topic is effectively a password — use a hard-to-guess name. For self-hosted ntfy, change server. Set token when you need an access token (adds Authorization: Bearer automatically). priority accepts 1-5 or min|low|default|high|max.

Generic Webhook

{ "type": "webhook", "name": "my-hook", "enabled": true,
  "url": "https://example.com/push",
  "method": "POST",
  "headers": { "Authorization": "Bearer ${MY_TOKEN}" },
  "body": { "text": "{{text}}", "meta": { "event": "{{event}}", "project": "{{project}}" } },
  "events": { "idle": false, "interrupted": true } }

Prefer writing body as a JSON object: placeholders are substituted into the structure, then the whole thing is serialized, so quotes and newlines in error text never break JSON. String templates are also supported and auto-escaped by contentType. Placeholders in url are URL-encoded.

All options

Key Default Description
enabled true Master switch
modes ["tui"] pi run modes allowed to push; see below
timeoutMs 5000 Per-request timeout
shutdownTimeoutMs 2000 Max block time to send on exit
dedupeMs 3000 Same content only sent once within this window
minDurationSec 0 Skip idle when turn is shorter than this (interrupts still fire)
maxTextChars 500 Body truncation length
titleTemplate pi {{status}} Title template (Bark)
template pi {{status}} · {{reason}} Body template
events see table above Global event toggles
needInputTools ["ask_user_question", "plan_mode_question"] Which tools count as "asking you"
debug false Log successes too

Placeholders: {{status}} {{reason}} {{text}} {{title}} {{event}} {{cwd}} {{project}} {{duration}} {{session}} {{model}} {{host}} {{time}} {{date}}

Values written as "$VAR" are replaced entirely from the environment; "Bearer ${VAR}" does in-string substitution — keys need not live on disk.

The config file is hot-read by mtime; edits do not need /reload (only plugin code changes do).

Why modes defaults to only tui

pi subagents run as pi --mode json -p child processes, and global extensions load there too. If you allow json/print, every finished subagent would push — pure noise. Add print only when you really want batch-job notifications.

Troubleshooting

  • /push test shows each channel's HTTP status or error reason directly
  • Failures are written to push.log (same directory as config.json); with "debug": true, successes are logged too
  • Group bots that return HTTP 200 but business failure (bad signature, missing keyword, rate limit) are treated as failures and logged with the error code — never pretended as success

Known limits

  • SIGKILL / power loss / hard-killed terminal get no notification: pi does not fire session_shutdown, so nothing in-process can send
  • pi itself crashing (uncaughtException) also gets none: pi's crash handler registers first and calls process.exit(1) synchronously, so async requests cannot leave
  • Only tool-shaped questions (needInputTools) are detected; confirm dialogs from other extensions are not

Self-test

cd ~/.pi/agent/extensions/pi-agent-push && node test/run.ts   # needs Node >= 23

Spins up a local HTTP server that pretends to be each channel and runs assertions: signature algorithms (fixed vectors matching official algorithms), per-channel payload shapes, dedupe, timeout, mode isolation, JSON escaping, per-channel event overrides, command behavior, etc.


中文

pi 主 agent 停下来时,把消息推到手机 / 群:Bark、飞书、企业微信、钉钉、ntfy、通用 Webhook。全部纯文本

安装

pi install npm:pi-agent-push
# 或从 GitHub:
pi install git:github.com/kiim-wong/pi-agent-push

安装后重启 pi 或执行 /reload

配置

任选其一:

  1. 命令行(推荐)
    /push set ntfy topic=your-topic enabled=true
    /push test ntfy
    
  2. 配置文件
    在扩展目录创建 config.json(可参考包内 config.example.json)。
    也可用环境变量 PI_AGENT_PUSH_CONFIG 指向自定义路径。

仓库 / npm 包不包含真实 key;本地 config.json 请自行保管。


快速开始

  1. 打开 config.json,把要用的渠道填上 key,并把该渠道的 "enabled" 改成 true
  2. 重启 pi,或在会话里执行 /reload
  3. /push test —— 会逐个渠道打出 HTTP 结果
/push                         状态
/push list                    列出渠道
/push get <渠道>              查看配置(密钥脱敏)
/push enable|disable <渠道>   开关渠道(写 config.json)
/push set <渠道> k=v [k=v…]   设置 topic/token/deviceKey/url…
/push test [渠道]             测试(可只测一个)
/push events [k=on|off…]      全局事件开关
/push on|off                  当前会话总开关(不写盘)
/push help

渠道选择器:ntfy / 名称 / ntfy#1 / 序号。

/push set ntfy topic=my-topic token=tk_xxx enabled=true
/push enable ntfy
/push test ntfy

什么时候会推送

事件 触发时机 默认 文案
idle 模型回答完、自动重试和自动压缩都结束,pi 真正在等你 pi 已就绪 · 输出结束,等待输入
interrupted 本轮被 Esc 取消 pi 已中断 · 本轮被取消
interrupted 本轮报错(重试用尽后) pi 已中断 · 运行出错:401 authentication_error: invalid x-api-key
needInput 调用了提问工具(ask_user_questionplan_mode_question pi 需要确认 · pi 正在等你回答问题
exit 会话退出(Ctrl+C / Ctrl+D / /quit pi 已退出 · 会话结束

idleinterrupted 都在 pi 的 agent_settled 事件里判定,所以自动重试不会重复推送——一次用户可感知的"pi 停下来了"只对应一条消息。

渠道配置

config.jsonchannels 是数组,可以配多个(含同类型多个)。每个渠道都支持 name(日志显示名)、enabledtimeoutMsevents(覆盖全局开关)。

Bark(iOS)

{ "type": "bark", "enabled": true, "deviceKey": "$BARK_KEY",
  "server": "https://api.day.app", "sound": "bell", "group": "pi", "level": "active" }

deviceKey 是 App 里那串 key。自建 bark-server 改 server 即可(用的是 POST /:device_key,V1/V2 服务端都兼容)。

飞书群机器人

{ "type": "feishu", "enabled": true,
  "url": "https://open.feishu.cn/open-apis/bot/v2/hook/xxxx", "secret": "" }

只有开启了「签名校验」才需要 secret。用「自定义关键词」的话,把关键词写进 template 里。

企业微信群机器人

{ "type": "wecom", "enabled": true,
  "url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx" }

钉钉群机器人

{ "type": "dingtalk", "enabled": true,
  "url": "https://oapi.dingtalk.com/robot/send?access_token=xxxx", "secret": "SECxxxx" }

加签模式填 secret;关键词模式请把关键词写进 template,否则钉钉会返回 errcode=310000(插件会把它当作失败记进日志)。

ntfy

{ "type": "ntfy", "enabled": true, "topic": "$NTFY_TOPIC",
  "server": "https://ntfy.sh", "token": "$NTFY_TOKEN",
  "priority": "default", "tags": "pi,computer" }

topic 在公共服务器上相当于密码,请用难猜的名字。自建 ntfy 改 server。 需要 access token 时填 token(自动加 Authorization: Bearer)。 priority 支持 1-5min|low|default|high|max

通用 Webhook

{ "type": "webhook", "name": "my-hook", "enabled": true,
  "url": "https://example.com/push",
  "method": "POST",
  "headers": { "Authorization": "Bearer ${MY_TOKEN}" },
  "body": { "text": "{{text}}", "meta": { "event": "{{event}}", "project": "{{project}}" } },
  "events": { "idle": false, "interrupted": true } }

body 建议写成 JSON 对象:占位符先替换进结构、再整体序列化,错误信息里的引号和 换行永远不会破坏 JSON。写成字符串模板也支持,会按 contentType 自动转义。 url 里的占位符会做 URL 编码。

全部配置项

默认 说明
enabled true 总开关
modes ["tui"] 允许推送的 pi 运行模式,见下方说明
timeoutMs 5000 单次请求超时
shutdownTimeoutMs 2000 退出时最多阻塞多久发消息
dedupeMs 3000 相同内容在此窗口内只发一次
minDurationSec 0 本轮短于该秒数时不发 idle(中断不受影响)
maxTextChars 500 正文截断长度
titleTemplate pi {{status}} 标题模板(Bark 用)
template pi {{status}} · {{reason}} 正文模板
events 见上表 全局事件开关
needInputTools ["ask_user_question", "plan_mode_question"] 哪些工具算"在问你"
debug false 成功也记日志

占位符:{{status}} {{reason}} {{text}} {{title}} {{event}} {{cwd}} {{project}} {{duration}} {{session}} {{model}} {{host}} {{time}} {{date}}

值写成 "$VAR" 会整体取环境变量,写成 "Bearer ${VAR}" 可嵌入替换——key 不必落盘。

配置文件按 mtime 热读,改完不需要 /reload(改插件代码才需要)。

为什么 modes 默认只有 tui

pi 的 subagent 是用 pi --mode json -p 拉子进程跑的,全局扩展在子进程里同样会加载。 如果放开 json/print,每个 subagent 跑完都会推一条,纯噪音。确实想给批处理任务 推送时,再把 print 加进去。

排错

  • /push test 会直接显示每个渠道的 HTTP 状态或错误原因
  • 失败会记到 push.log(和 config.json 同目录);"debug": true 时成功也记
  • 群机器人 HTTP 200 但业务失败(签名错、缺关键词、限流)会被判定为失败并记录 错误码,不会假装成功

已知限制

  • SIGKILL / 断电 / 终端被强杀收不到通知:这类情况 pi 不会触发 session_shutdown, 进程内无从发出请求
  • pi 自身崩溃(uncaughtException)同样收不到:pi 的崩溃处理器先注册、且同步 process.exit(1),异步请求来不及发出
  • 只能感知工具形式的提问(needInputTools);其它扩展直接弹的确认框拦不到

自测

cd ~/.pi/agent/extensions/pi-agent-push && node test/run.ts   # 需要 Node >= 23

会起一个本地 HTTP 服务假扮各渠道,跑自测断言:签名算法(对拍官方算法的固定 向量)、各渠道报文结构、去重、超时、模式隔离、JSON 转义、渠道级开关覆盖、命令行为等。