pi-agent-push
Pi extension: push agent idle / interrupted / need-input events to Bark, Feishu, WeCom, DingTalk, ntfy, or a generic webhook.
Package details
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:
- CLI (recommended)
/push set ntfy topic=your-topic enabled=true /push test ntfy - Config file
Createconfig.jsonin the extension directory (see packageconfig.example.json).
Or point envPI_AGENT_PUSH_CONFIGat a custom path.
The repo / npm package does not ship real keys; keep your local
config.jsonprivate.
Quick start
- Open
config.json, fill in keys for the channels you want, set that channel's"enabled"totrue - Restart pi, or run
/reloadin the session /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 testshows each channel's HTTP status or error reason directly- Failures are written to
push.log(same directory asconfig.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。
配置
任选其一:
- 命令行(推荐)
/push set ntfy topic=your-topic enabled=true /push test ntfy - 配置文件
在扩展目录创建config.json(可参考包内config.example.json)。
也可用环境变量PI_AGENT_PUSH_CONFIG指向自定义路径。
仓库 / npm 包不包含真实 key;本地
config.json请自行保管。
快速开始
- 打开
config.json,把要用的渠道填上 key,并把该渠道的"enabled"改成true - 重启 pi,或在会话里执行
/reload /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_question、plan_mode_question) |
开 | pi 需要确认 · pi 正在等你回答问题 |
exit |
会话退出(Ctrl+C / Ctrl+D / /quit) |
关 | pi 已退出 · 会话结束 |
idle 和 interrupted 都在 pi 的 agent_settled 事件里判定,所以自动重试不会重复推送——一次用户可感知的"pi 停下来了"只对应一条消息。
渠道配置
config.json 的 channels 是数组,可以配多个(含同类型多个)。每个渠道都支持
name(日志显示名)、enabled、timeoutMs、events(覆盖全局开关)。
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-5 或 min|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 转义、渠道级开关覆盖、命令行为等。