@arkkwang/pi-cron
v0.1.1
Published
Lightweight local scheduler for the Pi CLI
Readme
English | 简体中文
pi-cron
面向官方 Pi CLI 的本机轻量调度器:配置热更新、任务去重、不重叠执行与超时控制。
独立 Node 轮询进程,不依赖 Pi 的 scheduler 插件;源码附带 Bash 入口;每次到期调用 PATH 中的 pi,参数原样传递,不再定义模型、provider 等包装字段。
启动
Windows 使用 Git Bash;macOS 使用 Bash。两端均需 Node.js 22.19+、npm,以及 PATH 中可用的 pi。
推荐通过 npm 安装(不会安装 Pi、启动服务或创建任务):
npm install -g @arkkwang/pi-cron
pi-cron ls -f /path/to/tasks.json
pi-cron start -f /path/to/tasks.json精简 Linux 镜像还需安装 procps(例如 Debian/Ubuntu 的 apt-get install procps),为进程树终止提供 ps。只有 Node 和 Pi 的容器不足以支持超时处理。
主要用于自动执行 Agent 任务,任务参数仍是原生 Pi CLI 参数。源码运行:
cd pi-cron
npm ci
bash pi-cron ls -f /path/to/tasks.json # 只读列出任务和最近记录
bash pi-cron start -f /path/to/tasks.json # 前台常驻必须用 -f 显式指定配置,没有默认路径;相对路径以调用者当前工作目录为基准。包不附带私人任务或 runtime 文件。可用 bash /absolute/path/to/pi-cron/pi-cron ... 从任意目录调用,保留当前工作目录。
只在前台运行,不自动设置开机启动或后台服务。切换前请停用旧调度器,避免重复执行。依赖 PATH 中的官方 Pi CLI(@earendil-works/pi-coding-agent),无需修改 Pi 本身。
只有 ls 和 start 两个命令。子命令与 -f 顺序不限,例如 ./pi-cron -f ../../tasks.json ls 与 ./pi-cron ls -f ../../tasks.json 等价。ls 按每个任务一段纯文本展示,不输出表格;不获取单例锁,不创建或修改 runtime;调度器运行时也可查询。列表的 lastStatus 是磁盘中保存的最近状态,不代表进程仍然存活;nextRun 是按当前配置算出的下一次计划时间,nextRun/lastRun 均按本机时区展示,不保证一定执行。start 启动前也会校验配置。
两份数据文件
-f 指定的 tasks.json 是任务数组,唯一的配置来源。示例(自行保存到包目录之外):
[
{
"id": "daily-check",
"cron": "0 9 * * *",
"enabled": true,
"timeoutSeconds": 420,
"args": ["--model", "provider/model", "--thinking", "medium", "--print", "--", "执行每日检查"]
}
]id、五段cron、args必填;id会出现在日志与 runtime 记录里,用人类可读的短标识(如daily-check),不要用 UUID;cron 时间按本机时区解释。enabled默认true;timeoutSeconds默认 1800 秒。所有对外超时配置均以秒为单位。cwd可选,相对路径以tasks.json所在目录为基准;省略则用该目录。支持绝对路径,但共享配置应避免机器专属盘符路径。不展开~或环境变量。args就是 Pi CLI 参数数组。非交互任务请包含--print;prompt 前可用--结束选项解析。无需配置command或 Node/Pi 安装路径。- 配置在启动时读取并校验;之后每次轮询比对文件的修改时间与大小,变了就重新读取解析:保存即生效,增删任务、启用停用、改时间都不需要重启。重载成功日志只在解析后的配置指纹变化时输出;原样重存、touch 或仅修改缩进不记录,字段顺序变化仍视为变化。运行中读到非法或读不到的文件时保留上一份可用配置,在 stderr 报一次错(同一份坏文件不会每秒刷屏),不退出;修好保存后自动生效。
ls每次读取磁盘上的配置。
tasks.runtime.json 在配置文件旁自动创建、仅由调度器写入,不随包分发:按任务 ID 保存最后触发时间、状态、起止时间、退出码及最多 4000 字符的输出尾部。时间戳为本机时区的 ISO 8601,带偏移(如 2026-09-30T11:30:00.489+08:00);读取兼容 Z 结尾的历史 UTC 记录。不保存完整历史,也不是通知渠道。写入使用临时文件加 rename,避免半份 JSON;损坏的 runtime 会阻止启动,不会偷偷清空去重记录。不要手动修改运行中的 runtime。
固定规则
- 每秒轮询,分钟精度;同一任务同一分钟最多触发一次,包括同分钟重启。
- 热更新只替换任务定义:已在运行的任务不打断,跑到结束;删掉或停用的任务在本次运行结束后不再触发;改
args/timeoutSeconds下次运行生效。重载不重置去重记录,所以本分钟已触发过的任务不会因重载再触发一次;新增任务若 cron 命中当前分钟且本分钟未跑过,会立即执行。 - 停机、休眠错过的分钟跳过,不补跑。若在计划分钟内启动,仍会执行该分钟的任务。
- 同任务不重叠,重叠的触发直接跳过;不同任务可并行。
- 先持久化触发记录,再启动 Pi;启动失败或进程中断不自动重试。不承诺 exactly-once。
- 到期执行前立即输出
starting日志,包含实际时间、任务 ID 和计划触发时间;结束日志包含实际时间、结果和退出码。日志与 runtime 中的时间戳均为本机时区 ISO 8601(带偏移)。starting表示开始尝试启动 Pi,不代表模型已开始处理。 - 超时尝试终止任务进程树并记录
timeout。终止失败会报错并停止新调度,等待任务结束或人工处理,不假报成功。 - SIGINT/SIGTERM 停止新调度,等待当前任务结束(其超时仍有效)。终端关闭/强制结束不保证走完优雅退出。
- 重启时遗留的
running标为unknown:无法证明旧进程已退出。异常终止后,人工确认旧任务已结束再启动,避免遗留进程重复执行。 - 用本机
127.0.0.1:17391的监听占位保证单实例,不提供网络 API,也无需额外锁文件;端口被占用会明确失败。 - 暂不支持一次性任务、重试队列和原会话投递。
依赖与验证
只复用 croner 的时间计算,不使用其调度引擎;cross-spawn 处理跨平台 PATH/命令启动;tree-kill 处理 Windows/macOS/Linux 的进程树终止差异。本项目不维护 Windows 专用分支。进程库内部会使用各系统的原生命令,不代表依赖一个仅支持 Windows 的方案。
npm test测试将调度时钟固定在同一分钟,避免跨分钟误报;通过测试 preload 将单例监听映射到 OS 分配的临时端口,同一用例的后续进程复用该端口验证互斥与重启,不占用生产端口,也不要求停止常驻调度器。等待执行结果前先确认启动成功,提前退出时直接报告进程输出。日志专用用例禁用任务,覆盖原样重存、格式变化、实际字段变化、损坏配置及恢复,并观察多个轮询周期确认没有重复日志;真实子进程和定时器在综合用例中照常运行。测试仅运行 pi --version / pi --help,不调用模型或发送通知;覆盖到期计算、去重、非法配置、超时单位、实际启动与失败、配置热更新(非法配置保留旧值且只报一次)、单例、输出截断、重启去重及损坏状态文件。
已在 Windows Git Bash + Node 22 实测;macOS/Linux 尚未实测。每秒轮询先比对配置文件的时间戳与大小,只有变化时才重新读取解析;其余时间只检查内存中的任务是否到期。
边界与安全
每台机器只允许一个实例(固定本地端口 17391),同目录配置共用 tasks.runtime.json。不同配置不是独立实例隔离单元。
任务以当前用户权限运行 Pi,可能修改文件、调用工具并产生模型费用。仅运行可信任务配置;runtime 保存的输出尾部可能包含敏感内容,不应提交或公开。不提供沙箱、自动重试、补跑、完整历史或 exactly-once 保证。
相关项目
- pi-snap:帮助人和 Agent 快速定位会话问题。
- pi-subagent:派发、观察和继续指挥子 Agent。
