FIELD GUIDE · TokenMeter · 2026-04-24
Field Guide 00 / TOKENMETER

给 AI 编码 Agent
装一副仪表盘

一个常驻本地的 daemon,把 Claude Code 和 Codex 的每次工具调用、每次 token 消耗、每一次会话都收集进 SQLite,再用 TUI 和 Web 两套面板回放给你看。

  1. 用 Claude Code / Codex 写代码,钱花在哪里、做了什么、花了多长时间——默认是黑盒。TokenMeter 把它打开。
  2. 两条数据源:Claude 走 hook 事件 + JSONL 日志,Codex 只能扫日志。一个 daemon 接住,统一落到 SQLite。
  3. 终端里 tm 开 TUI 看四视图;浏览器里 tm web 开 Dashboard。零配置、单二进制、纯 Go。
读给谁
在用 AI 编码 agent 的开发者;想知道"花了多少、做了什么"的维护者;想理解 TokenMeter 架构的贡献者。
读完能答
TokenMeter 解决什么问题、数据怎么来、怎么落库、TUI/Web 怎么读、为什么两条平台路径不一样。
对照原件
README.md · CLAUDE.md · docs/architecture.svg · internal/* 源码
SCROLL · 往下读 ↓
01
CHAPTER ONE · 先看痛点

AI 编码的黑盒

用 Claude Code 或 Codex 的那一刻,你其实并不知道它在花多少钱、做了什么、上下文塞了多少。

跑完一个任务,唯一能看见的是终端里最后一行 "Done"。至于:

  • 这次会话花了多少 token?多少钱?
  • Claude 真的读了那个文件,还是只搜了关键词?
  • 它 spawn 了几个 subagent?每个跑了多久?
  • 上周这个项目在 AI 编码上的总支出是多少?哪个模型贵?
  • 上次改错的地方,是哪次 Edit 调用?参数是什么?

这些问题,官方 UI 都不直接回答。你只能翻 ~/.claude/projects/ 下那堆 JSONL 文件,或者每次会话结束打开 Codex 的 sessions 目录一行行看。

类比 · 开车不看仪表

想象你开车上高速,没有时速表、没有油表、没有行程里程。车还能开,但你不知道油还剩多少、一个月花了多少油钱、哪段路最费油。TokenMeter 就是给 AI 编码这辆车装上仪表盘。

常见场景

没有 TokenMeter
  • 月底看 API 账单懵住
  • 怀疑某个任务慢,但找不到是哪次 tool call
  • Subagent 出错了,不知道它调了什么
  • 想回忆"上次那个 prompt 是咋写的",找不到
有 TokenMeter
  • 今日/本周/本月费用一眼看清
  • 每个 tool 调用耗时、参数、结果都在
  • Subagent 层级可折叠展开
  • / 搜索历史 prompt
核心承诺

正常使用 Claude Code 或 Codex,TokenMeter 在后台自动采集——不改变你的工作流,只是把一直就发生但你看不见的事变成可见。

02
CHAPTER TWO · 一张图看全

一个 daemon 接住所有

架构可以在一张图里讲完:左边数据源、中间 daemon + SQLite、右边 TUI 与 Web。

① DATA SOURCES · 数据源 Claude Code hooks 8 种 Pre/Post 事件 Claude JSONL ~/.claude/projects/ Codex JSONL ~/.codex/sessions/ ② COLLECTORS · 转换 tm emit ClaudeLogWatcher CodexWatcher ③ DAEMON · 中央调度 TokenMeter daemon Unix socket PID lock SQLite ~/.tokenmeter/data/tokenmeter.db ④ CONSUMERS · 界面 tm TUI bubbletea · 4 tabs tm web HTTP · :8370
Figure 02-A · 完整数据流

五块各自干什么

  1. 数据源:两类 AI 编码工具产出三种信号——Claude 的 hook 事件(实时)、Claude 的 JSONL 日志(token 真相)、Codex 的 JSONL 日志(唯一通道)
  2. Collectors · 转换层tm emit 是 hook 的轻量接收端;ClaudeLogWatcher 扫描 JSONL 补 token;CodexWatcher 轮询 Codex 会话目录
  3. Daemon:Unix socket 接事件 → 写 SQLite → 广播给订阅者。单例,PID lock 保护
  4. SQLite~/.tokenmeter/data/tokenmeter.db,5 张表装下所有历史
  5. Consumers:TUI(bubbletea,4 个 tab)订阅实时事件;Web(嵌入式 SPA + REST API)独立启动
一句话版本

左边抓数据、中间 daemon 存库、右边两种界面展示。抓取与展示解耦——daemon 在后台安静跑着,开不开 TUI 都不影响数据持续采集。

03
CHAPTER THREE · 两条路径

Claude 和 Codex不一样

Claude Code 给了 hook 机制,实时、干净;Codex 没有,只能扫日志。所以 TokenMeter 为它们各自写了一条 collector。

REALTIME
PATH · A
Claude Code
信号 1
8 种 hook 事件(PreToolUse / PostToolUse / SessionStart / Stop / SubagentStart / ...)
信号 2
JSONL 日志补 token 数量
传输
Unix socket(实时)
延迟
毫秒级
POLL
PATH · B
Codex
信号
JSONL 日志(唯一通道)
传输
文件系统轮询
延迟
秒级
去重
内存 lastTokenUsage map

Claude Code 路径:hook + 日志"双通道"

Claude Code 的每个 tool 调用都会 stdin 发一个 JSON 给 TokenMeter 注册的 hook 脚本(PreToolUse 发起前、PostToolUse 完成后)。hook 脚本就是 tm emit——把 stdin 读完,转成事件发到 Unix socket,结束。

但是——hook 本身不携带 token 数量。Claude 只在把会话写 JSONL 时附上 usage。所以 TokenMeter 同时跑一个 ClaudeLogWatcher 扫描 ~/.claude/projects/ 下那些 JSONL 文件,找 type == "assistant" 的行,从 message.usage 把 token 数补进数据库。

类比 · 餐厅点单 + 小票

Hook 像服务员实时报给后台"桌 3 点了宫保鸡丁"——知道点了什么但不知道价格。真要结账,得等厨房打印小票(JSONL 日志)来对账。TokenMeter 两头都接,才能同时知道"做了什么"和"花了多少钱"。

Codex 路径:只能扫日志

Codex 没有 hook 机制。TokenMeter 启一个 CodexWatcher 轮询 ~/.codex/sessions/YYYY/MM/DD/*.jsonl。关注三种 payload:

  • session_meta → 会话开始
  • response_item → 工具调用(function_call 起、function_call_output 完)
  • event_msg with type:"token_count" → token 记账

轮询意味着同一份数据可能被读两次。Codex watcher 在内存里维护 lastTokenUsage map 去重,保证不重复计费。

设计取舍

Codex 路径天然比 Claude 慢、粗、依赖去重。如果以后 Codex 出了 hook,这条路径可以退役;当下只能先扫日志兜底,宁可多做一次工作,也不能遗漏会话。

Claude JSONL 的一个小坑 · cwd 编码

Claude 把会话 JSONL 存在 ~/.claude/projects/<cwd-encoded>/<session-id>.jsonl。cwd 的编码规则是:每个 / 替换成 -(含首字符)。比如:

  • /Users/admin/code/tokenmeter-Users-admin-code-tokenmeter

这个细节写在 internal/collector/claude.go 里——错一个字符,就找不到日志文件。

04
CHAPTER FOUR · 统一

两条路,一种事件

不管数据从 Claude 还是 Codex 来,都先转成同一个 Event struct 再进 daemon。这是架构里最省事的一招。

type Event struct {
    ID        string     // tool_use_id (Claude) 或 call_id (Codex)
    Type      EventType  // ToolCallStart | End | AgentStart | ...
    SessionID string
    AgentID   string
    Platform  Platform   // "claude" | "codex"
    Timestamp time.Time
    Data      EventData
}
Figure 04-A · internal/event/event.go

为什么这样设计

Claude 和 Codex 的原生事件结构完全不一样。Claude 的 hook JSON 有 hook_event_name / session_id / tool_input / tool_use_id;Codex 的 JSONL 里是 timestamp / type / payload 套娃。

如果 daemon、storage、TUI 都分别处理两种原生格式,每加一个平台就要在五个地方改代码。所以 TokenMeter 的第一件事是把它们收敛到统一 Event,后面所有层都只认 Event。

类比 · 海关报关单

不管你是从日本运电器、从法国运红酒、还是从美国运芯片,到了海关都得填同一张报关单。集装箱、船舱、搬运方式各不相同,但报关单格式固定。daemon 对待事件的方式一样——你从哪条路径来的,到我这都是一个 Event

Pre / Post 关联靠 ID 对齐

一次工具调用其实是两个事件:Pre 发起前、Post 完成后。daemon 靠 Event.ID 关联两头,算出 duration_ms

  • Claudetool_use_id(Claude Code 保证全局唯一)
  • Codexcall_id(函数调用 id)

一条调用 Pre 先到、Post 后到——中间可能隔几秒。daemon 维护一张待配对 map,Post 到时 pop 出来更新那行 tool_calls 记录。

统一事件的红利

未来接第三个平台(比如 Cursor、Cline 或自研 agent),只需要写一个新的 collector 把原生信号转成 Event,后面 daemon / storage / TUI / Web 全都不用改。这是"统一模型"为软件换来的弹性。

05
CHAPTER FIVE · 数据长啥样

SQLite 五张表

一个 daemon 进程、一个 .db 文件、五张表。schema 变动遵循"只加不删"的兼容原则。

所有历史都落在 ~/.tokenmeter/data/tokenmeter.db,用 modernc 的纯 Go SQLite 驱动。5 张表,职责分明:

§
sessions
会话

session_id PK · platform · start_time · status · total_input_tokens · total_output_tokens · total_cost_usd

§
agents
Agent 实例

agent_id PK · session_id FK · parent_agent_id · role · status。subagent 靠 parent_agent_id 形成树。

§
tool_calls
工具调用

call_id PK · agent_id / session_id FK · tool_name · params_summary · result_summary · duration_ms · status

§
token_usage
Token 记录

id AUTOINCREMENT · agent_id / session_id FK · input_tokens · output_tokens · model · cost_usd。source_id 唯一索引防重。

§
file_changes
文件变更

id AUTOINCREMENT · session_id FK · file_path · change_type

Schema 变更原则

internal/storage/db.go 里的 migrate() 函数只用两种动作

  • CREATE TABLE IF NOT EXISTS 建新表
  • addColumnIfMissing() 幂等加新列

不允许删列、重命名列。老版本 TokenMeter 的数据库升级到新版本必须能直接打开用。这条纪律让 TokenMeter 敢做"零停机自升级"。

时间字段约定

SQLite 存时间统一用 TEXT + RFC3339 格式,读的时候用 parseTime()。不用 UNIX 时间戳——跨平台调试时人眼可读更重要。

06
CHAPTER SIX · 终端面板

TUI 四视图

Bubbletea 做底、每 2 秒轮询 + 实时事件推送。Tab 键切换,j/k 上下移,/ 搜索。

视图 内容 数据来源
Dashboard 会话列表(费用、上下文占用、状态、标签);顶部汇总栏支持 t 切换今日/本周/本月/全部 db.ListSessions()
Messages 从 Claude / Codex JSONL 提取的用户对话消息;/ 全文搜索 collector.ReadUserMessages()
Tool Calls 当前会话的工具调用流;Enter 展开查看参数与结果 db.ListToolCalls(session, 500)
Stats 7 天费用柱状图、工具调用统计、Agent 分布、文件变更汇总 多张表聚合

刷新机制:轮询 + 推送

TUI 每 2 秒跑一次 tickCmd 主动查库(兜底),同时订阅 daemon 的实时事件(listenEvents)——有新 hook 事件一到就立刻刷屏。两条路互为保底。

类比 · 车站到站显示屏

显示屏既接收调度中心的实时推送(新车进站立刻改),又每 30 秒自己拉一次全表(防推送丢包)。TUI 的 tick + listenEvents 是同一套思路——实时但不依赖实时。

几个贴心细节

  • gitBranch 作显示名:原始 session UUID 太丑;优先用 JSONL 顶层的 gitBranch,其次 filepath.Base(cwd),最后才是 UUID
  • 跨视图切会话[ / ] 在任何 tab 都能切上一个/下一个 session
  • Copy 恢复命令c 键把当前会话的恢复命令拷到剪贴板
  • 展开详情不丢expandedCalls map 按 call_id 持久化,刷新后仍保留哪些调用是展开的

快捷键节选

01
Tab / Shift+Tab
切换视图
02
j / k
上下导航
03
G
跳到底部
04
Enter
选择 / 展开
05
[ / ]
切会话
06
/
过滤当前列表
07
t
切时间范围
08
p
切平台(全部/Claude/Codex)
09
s
切排序(最近/费用)
10
c
复制恢复命令
11
Esc
清过滤
12
q
退出
07
CHAPTER SEVEN · 浏览器版本

当 TUI 不够用

想看图表、想分享给不用终端的同事、想在更大屏幕上翻旧会话——启 tm web 就好。

tm web 启动一个独立的 HTTP 服务(默认 :8370),提供:

  • 费用面积图——Canvas 绘制,hover 显示详情,点击某一天按天筛选会话
  • 模型占比 + 工具排行——同一屏看清钱花哪了
  • 会话列表——搜索、排序、按费用色标
  • 会话详情——对话消息、工具时间线、文件变更、Agent 层级
  • 深色 / 浅色主题——自动跟随系统偏好
  • 键盘导航——j/k 选、Enter 开、/ 搜、/ 切、Esc 返回、? 帮助

架构上:独立的进程

Web 不是 TUI 的扩展,是独立进程。它直接读 SQLite(与 daemon 共用同一个 .db 文件),对外暴露 REST API + 嵌入式 SPA(Go embed 打包,不依赖外部静态目录)。

解耦设计

TUI 和 Web 互不知道对方存在。都读同一个 SQLite,都订阅同一套 daemon 事件。开一个不影响另一个。也可以只开 Web、不开 TUI。

08
CHAPTER EIGHT · 不能数重

Token 只算一次

daemon 重启、日志被重新扫描、Codex 轮询重叠——每个事件都可能被处理多次。去重是默默的地板工程。

三种身份键

§
tool_use_id
Claude 工具调用

Claude Code 自己分配的全局唯一 id。Pre/Post 对齐、重复事件去重都靠它。

§
call_id
Codex 工具调用

Codex JSONL 里的 function call id。作用同 tool_use_id

§
source_id
Token 记录唯一索引

token_usage 表上的唯一索引。INSERT OR IGNORE 保证同一条 JSONL 行扫多遍也只记一次。

三种典型重复场景

  1. daemon 重启——重启后 ClaudeLogWatcher 从头扫 JSONL。没有 source_id 唯一索引,token 会被重新累加一遍。
  2. Codex 轮询覆盖——文件还在写,上次轮询读了 80%,这次读了 100%。内存 lastTokenUsage map 记录"这个 key 上次用量是 X",避免重复计费。
  3. Stop hook 重试——Claude 有时会发两次 Stop。MarkPendingToolCallsInterrupted 幂等,跑几次都一样。
类比 · 会计冲销

会计每月对账,同一笔钱可能在三张账单上出现。要么靠唯一凭证号对齐、要么靠"已入账"标记。TokenMeter 的 source_id 唯一索引是凭证号;lastTokenUsage map 是"已入账"标记。两种都用——保险一点。

僵尸会话的处理

有些会话因为 Claude 进程被 kill、机器休眠等原因没走 SessionEnd,数据库里就挂着 status=running。daemon 每次启动会跑 MarkStaleSessionsEnded(2h)——2 小时没更新的 running 会话,标记为 ended。

09
CHAPTER NINE · 常用命令

命令速查

装好 TokenMeter 之后,90% 的时候只用三个命令。剩下 10% 是报告、清理、标签。

每日三件套

命令 做什么
tm setup一次性:把 hook 注入 ~/.claude/settings.json
tm启 TUI(自动拉起 daemon)
tm web启浏览器 Dashboard,默认 :8370

报告与清理

命令 做什么
tm status快速查看会话摘要
tm cost [today|week]Token 用量与费用统计
tm report [session]单会话详细文本报告
tm report --weekly本周 Markdown 费用报告(按模型、按会话)
tm report --monthly本月 Markdown 费用报告
tm tag <id> "备注"给会话打标签;省略 text 则清除
tm clean [days]清理 N 天前的历史(默认 7)

守护进程与卸载

命令 做什么
tm daemon只启 daemon,不开 TUI
tm uninstall移除 hooks、停 daemon
rm -rf ~/.tokenmeter删除所有数据
tm version显示版本
目录速查

~/.tokenmeter/data/tokenmeter.db — SQLite 数据库
~/.tokenmeter/tokenmeter.sock — Unix socket
~/.tokenmeter/daemon.pid — PID 锁文件

10
CHAPTER TEN · 设计哲学

合上手册时带走的

TokenMeter 的形状是被几个不愿妥协的原则塑造的——这些原则决定了它是什么、以及它不是什么。

I
零配置 · 单二进制
装好就能用。tm setup + tm 两条命令到位。纯 Go,没有外部 runtime、没有数据库服务、没有 Python 环境。
II
统一事件 · 多平台一种形状
Claude / Codex 原生格式完全不同,到 daemon 这里都是 Event。未来加 Cursor、Cline 也不用改 daemon 和 storage。
III
采集与展示解耦
daemon 默默跑着,开不开 TUI 不影响数据采集。TUI 和 Web 是两个独立进程,都读同一个 SQLite。
IV
幂等地板工程
daemon 重启、日志重扫、Codex 轮询重叠——系统默认假设会发生。source_id 唯一索引、INSERT OR IGNORE、内存去重 map 三层保护。
V
Schema 只加不删
老版本的 .db 文件永远能在新版本 TokenMeter 里打开。升级不丢数据,也不要求用户手动迁移。
VI
实时但不依赖实时
TUI 既订阅 daemon 事件(实时),又每 2 秒轮询一次 SQLite(兜底)。推送丢了也不会漏数据。
VII
本地优先 · 数据就在 ~/.tokenmeter
没有云、没有遥测、没有上报。所有数据都在你本地 SQLite 里,想删就 rm -rf 一键清空。
一句话版本

TokenMeter 是给 AI 编码 agent 装的本地仪表盘——一个 Go 写的 daemon 接住 Claude Code 和 Codex 的所有信号,统一成事件落进 SQLite,再用 TUI 和 Web 两套界面回放。零配置、幂等、本地、解耦。