Skip to content

JJLAAA/cc-heartbeat-cli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

heartbeat-cli

macOS CLI 工具,用于为工作区配置 AI 驱动的定时健康检查。通过 macOS launchd 调度,定期将用户自定义的 prompt 喂给 claude 执行,并将结果持久化到日志。

依赖

  • macOS(使用 launchd / LaunchAgents)
  • Bun 运行时
  • Claude Code CLIclaude 命令需在 PATH 中)

安装

git clone <repo>
cd heartbeat-cli
bun install

注册为全局命令:

bun link

或直接通过 Bun 执行:

bun src/index.ts <command>

快速上手

1. 在工作区初始化 HEARTBEAT.md

heartbeat init /path/to/your/project

生成一个带默认内容的模板文件,按需编辑 prompt 描述 Claude 应该检查什么。

2. 注册到 launchd:

heartbeat add /path/to/your/project

生成 .plist 并加载到 ~/Library/LaunchAgents/,launchd 从此按配置的 schedule 自动触发。

3. 查看已注册的工作区:

heartbeat list

4. 手动触发一次:

heartbeat run /path/to/your/project

5. 查看运行日志:

heartbeat logs                          # 所有工作区
heartbeat logs /path/to/your/project    # 指定工作区

6. 注销工作区:

heartbeat remove /path/to/your/project

HEARTBEAT.md 格式

每个工作区需要有一个 HEARTBEAT.md,由 YAML frontmatter 和 prompt 正文两部分组成,用 --- 分隔:

---
name: "My Project"
schedule: "0 9 * * 1-5"
enabled: true
timeout: 120
---

检查这个工作区是否一切正常。

如果没有问题,回复:HEARTBEAT_OK
如果需要关注,回复:ATTENTION: <简要描述>

Frontmatter 字段

字段 必填 说明
name 显示名称,默认使用目录名
schedule 执行时机,支持间隔简写、cron 表达式或数组
enabled 是否启用,默认 true
timeout Claude 最长运行秒数,默认 120

schedule 格式

间隔简写:

含义
15m 每 15 分钟
1h 每小时
6h 每 6 小时
1d 每天

5 段 cron 表达式:

┌─ 分钟 (0-59)
│  ┌─ 小时 (0-23)
│  │  ┌─ 日期 (1-31)
│  │  │  ┌─ 月份 (1-12)
│  │  │  │  ┌─ 星期 (0-6,0=周日)
│  │  │  │  │
0  9  *  *  1-5    # 工作日早上 9 点

支持 *(通配)、1-5(范围)、1,3,5(列表)、*/15(步进)。

多组 schedule(数组):

当需要在多个时间点触发时,可将 schedule 配置为数组。所有项均须为 cron 表达式(间隔简写不支持多组):

schedule:
  - "0 9 * * 1-5"   # 工作日早上 9 点
  - "0 18 * * 1-5"  # 工作日下午 6 点

多组 cron 会被合并进同一个 StartCalendarInterval 数组,注册为单个 launchd job。


结果分类

Claude 的回复会被分类为三种状态:

状态 触发条件 含义
ok 回复中包含 HEARTBEAT_OK 一切正常
attention 回复中包含 ATTENTION: <描述> 需要关注
error 超时或空响应 执行失败

prompt 中必须明确指示 Claude 用这两个关键词之一作答。


技术设计

整体数据流

HEARTBEAT.md(每个工作区)
       │
       ▼
 parseHeartbeat()       解析 YAML frontmatter + prompt 正文
       │
       ▼
 registerJob()          生成 plist → launchctl load → launchd 接管调度
       │
       │  (launchd 按 schedule 触发)
       ▼
 runHeartbeat()         spawn claude 子进程,stdin 传入 prompt
                        读取 stream-json 输出,提取最终文本
       │
       ▼
 classify()             检测 HEARTBEAT_OK / ATTENTION 关键词
       │
       ▼
 appendLog()            追加到 ~/.heartbeat/heartbeats.jsonl

Task ID 生成

每个工作区的唯一标识取工作区绝对路径的 SHA-256 哈希前 8 位:

createHash('sha256').update(absolutePath).digest('hex').slice(0, 8)

路径不变则 ID 不变,保证注册操作幂等,同时用于 plist 命名、日志过滤和配置索引。

schedule 转换为 launchd XML

launchd 支持两种触发机制,转换逻辑如下:

间隔简写 → StartInterval

<key>StartInterval</key>
<integer>3600</integer>

cron 表达式 → StartCalendarInterval

cron 中的通配符、范围、列表、步进会被完全展开为笛卡尔积,每个时间点生成一个 <dict>

<key>StartCalendarInterval</key>
<array>
    <dict>
        <key>Hour</key><integer>9</integer>
        <key>Minute</key><integer>0</integer>
        <key>Weekday</key><integer>1</integer>
    </dict>
    <!-- ... -->
</array>

多组 cron 各自展开后合并进同一个 <array>,共用一个 plist。

Claude 子进程调用

claude --output-format stream-json --verbose --print --dangerously-skip-permissions -
参数 作用
--print - 非交互模式,从 stdin 读取 prompt
--output-format stream-json --verbose 以换行分隔的 JSON 事件流输出
--dangerously-skip-permissions 允许无人值守时自动批准所有工具调用

stream-json 输出解析

Claude 的输出是逐行的 JSON 事件流:

{"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}
{"type":"tool_use", ...}
{"type":"result","result":"HEARTBEAT_OK"}

extractFinalText() 解析策略:

  1. 优先:从后向前扫描,取第一个 type=resultresult 字段
  2. 回退:拼接所有 type=assistant 事件中的 text block

日志格式(JSONL)

每次运行追加一行到 ~/.heartbeat/heartbeats.jsonl

{
  "ts": "2026-03-01T09:00:00.000Z",
  "taskId": "0422840c",
  "name": "My Project",
  "workspacePath": "/path/to/project",
  "status": "ok",
  "message": "ok",
  "durationMs": 4200,
  "raw": "..."
}

Append-only,无需加锁,易于用 jq 查询。

磁盘结构

~/.heartbeat/
  config.json                       # 工作区注册表
  heartbeats.jsonl                  # 追加式运行日志
  logs/
    <taskId>.stdout.log             # launchd 触发时的 stdout
    <taskId>.stderr.log             # launchd 触发时的 stderr

~/Library/LaunchAgents/
  com.heartbeat.scheduler.<taskId>.plist   # 每个工作区对应一个 launchd job

源文件职责

文件 职责
src/index.ts CLI 入口,命令分发
src/config.ts 工作区注册表的读写;Task ID 生成
src/heartbeat.ts 解析 HEARTBEAT.md,拆分 frontmatter 和 prompt
src/runner.ts spawn claude 子进程,解析 stream-json,分类结果,写日志
src/scheduler.ts 生成 plist XML,转换 schedule,调用 launchctl
src/logs.ts JSONL 日志的追加与读取

技术栈

技术
运行时 Bun — 直接执行 TypeScript,无需构建
语言 TypeScript(ESM 模块)
调度 macOS launchd(launchctl + .plist
AI 引擎 Claude Code CLI(stream-json 模式)
YAML 解析 js-yaml
日志格式 JSONL(换行分隔 JSON)

关键设计决策

决策 原因
使用 launchd 而非自研调度器 无需常驻进程,系统级可靠性,开机自动恢复
Task ID = SHA-256(path)[:8] 路径唯一确定工作区,幂等注册
stream-json 模式 支持工具调用(Task / Skill / Write / Bash),不限于文本响应
prompt 完全由用户定义 不限制检查逻辑,Claude 可运行任意工具
JSONL 日志 Append-only,无锁,便于 grep / jq 查询

Prompt 示例

检查未提交的代码变更:

查看这个 git 仓库,如果没有未提交的改动或长期未合并的分支,回复 HEARTBEAT_OK。
否则回复 ATTENTION: <发现的问题>。

监控日志文件异常:

读取 app.log 最后 50 行,如果没有 ERROR 或 FATAL 记录,回复 HEARTBEAT_OK。
否则回复 ATTENTION: <错误摘要>。

依赖版本审计:

执行 `bun outdated`,检查是否有主版本落后的依赖。
如果一切最新,回复 HEARTBEAT_OK。
如果有主版本更新可用,回复 ATTENTION: <需要更新的包列表>。

每日新闻汇总(多工具协作示例):

schedule:
  - "0 9 * * *"
timeout: 600
用 Task 工具并行启动两个 agent,分别调用 hubtoday-daily 和 linuxdo-news-analyzer 技能。
等两个 agent 完成后,用 Bash 获取今日日期(date +%Y-%m-%d),将汇总内容写入当前目录的 YYYY-MM-DD.md。
文件写入成功后回复 HEARTBEAT_OK,否则回复 ATTENTION: <失败原因>。

About

MacOS下基于launchd 定期调用的Claude Code HeartBeat实现

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors