token-tracker 0.4.6__tar.gz → 0.4.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {token_tracker-0.4.6/src/token_tracker.egg-info → token_tracker-0.4.8}/PKG-INFO +31 -8
- {token_tracker-0.4.6 → token_tracker-0.4.8}/README.md +26 -6
- {token_tracker-0.4.6 → token_tracker-0.4.8}/pyproject.toml +20 -2
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/adapters/claude.py +3 -1
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/adapters/codex.py +3 -1
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/adapters/rate_limits.py +1 -3
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/adapters/types.py +0 -23
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/adapters/util.py +15 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/analyzer/aggregator.py +20 -7
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/analyzer/cost.py +25 -2
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/cli.py +189 -105
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/config.py +21 -1
- token_tracker-0.4.8/src/token_tracker/hooks.py +637 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/i18n.py +56 -102
- token_tracker-0.4.8/src/token_tracker/sidebar.py +699 -0
- token_tracker-0.4.8/src/token_tracker/sidebar_events.py +120 -0
- token_tracker-0.4.8/src/token_tracker/templates/claude_statusline.py +402 -0
- token_tracker-0.4.8/src/token_tracker/templates/codex_statusline.py +303 -0
- token_tracker-0.4.8/src/token_tracker/tz.py +22 -0
- token_tracker-0.4.8/src/token_tracker/ui/__init__.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/ui/format.py +1 -23
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/ui/heatmap.py +13 -11
- token_tracker-0.4.8/src/token_tracker/ui/report_stats.py +56 -0
- token_tracker-0.4.8/src/token_tracker/ui/sidebar.py +303 -0
- token_tracker-0.4.8/src/token_tracker/ui/sidebar_app.py +466 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/ui/status.py +11 -3
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/ui/tables.py +15 -131
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/ui/theme.py +1 -9
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/wizard.py +21 -10
- {token_tracker-0.4.6 → token_tracker-0.4.8/src/token_tracker.egg-info}/PKG-INFO +31 -8
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker.egg-info/SOURCES.txt +11 -3
- token_tracker-0.4.8/src/token_tracker.egg-info/requires.txt +13 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/tests/test_aggregator.py +41 -2
- token_tracker-0.4.8/tests/test_cli.py +139 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/tests/test_cost.py +32 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/tests/test_heatmap.py +3 -1
- {token_tracker-0.4.6 → token_tracker-0.4.8}/tests/test_hooks.py +468 -8
- token_tracker-0.4.8/tests/test_sidebar.py +929 -0
- token_tracker-0.4.8/tests/test_sidebar_app.py +380 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/tests/test_status.py +34 -8
- token_tracker-0.4.8/tests/test_util.py +83 -0
- token_tracker-0.4.6/src/token_tracker/analyzer/blocks.py +0 -68
- token_tracker-0.4.6/src/token_tracker/hooks.py +0 -1100
- token_tracker-0.4.6/src/token_tracker/ui/panels.py +0 -65
- token_tracker-0.4.6/src/token_tracker.egg-info/requires.txt +0 -10
- token_tracker-0.4.6/tests/test_blocks.py +0 -79
- token_tracker-0.4.6/tests/test_cli.py +0 -38
- token_tracker-0.4.6/tests/test_util.py +0 -34
- {token_tracker-0.4.6 → token_tracker-0.4.8}/LICENSE +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/setup.cfg +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/__init__.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/adapters/__init__.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/adapters/registry.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/analyzer/__init__.py +0 -0
- {token_tracker-0.4.6/src/token_tracker/ui → token_tracker-0.4.8/src/token_tracker/templates}/__init__.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/ui/console.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker/ui/themes.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker.egg-info/dependency_links.txt +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker.egg-info/entry_points.txt +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/src/token_tracker.egg-info/top_level.txt +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/tests/test_codex.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/tests/test_tables.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.8}/tests/test_theme.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: token-tracker
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary: Track token usage across local AI agents (Claude Code, Codex)
|
|
5
5
|
Author-email: stormzhang <stormzhang.dev@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -42,11 +42,14 @@ Requires-Python: >=3.11
|
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Requires-Dist: rich>=13.7
|
|
45
|
-
Requires-Dist: questionary
|
|
45
|
+
Requires-Dist: questionary<3,>=2.0
|
|
46
|
+
Requires-Dist: textual>=1.0
|
|
46
47
|
Provides-Extra: test
|
|
47
48
|
Requires-Dist: pytest>=8.0; extra == "test"
|
|
49
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
|
|
48
50
|
Provides-Extra: dev
|
|
49
51
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
52
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
50
53
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
51
54
|
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
52
55
|
Dynamic: license-file
|
|
@@ -67,6 +70,7 @@ Dynamic: license-file
|
|
|
67
70
|
|
|
68
71
|
- **多 Agent 统一追踪** — Claude Code + Codex 统一读取,多 Agent 按来源分组
|
|
69
72
|
- **状态栏集成** — Claude Code 用官方 StatusLine 接口;**Codex 业界首创伪 statusline 方案**(hook 注入两行真彩色状态栏,把官方未开放的能力在 Codex 里做了出来)
|
|
73
|
+
- **实时侧边栏** — `tt sidebar` 窄窗格常驻面板:全部活跃会话一屏总览(状态灯 + 最近提示词 + 「下一步」建议),点击会话直达对应 iTerm2 / tmux 窗格
|
|
70
74
|
- **限额监控** — 实时 5h / 7d 配额百分比 + 重置倒计时
|
|
71
75
|
- **多维成本分析** — 会话 / 日 / 周 / 月多维报表,等效成本统计
|
|
72
76
|
- **定价识别** — litellm 在线定价 + 内置官方价双层兜底,覆盖 Claude / OpenAI / Gemini / Grok 及国产主流(Kimi / GLM / Qwen / 豆包 / DeepSeek / MiniMax / MiMo);新模型自动套用同系列定价、不静默归零
|
|
@@ -83,6 +87,8 @@ Dynamic: license-file
|
|
|
83
87
|
|
|
84
88
|
基于 Claude Code 官方自定义 StatusLine 接口,**数据完全来自本地 Claude,无任何推测**。
|
|
85
89
|
|
|
90
|
+
> 状态栏接管是**可选**的:已有自定义 statusLine 时默认保留你的配置(向导里也可随时选 No),报表命令完全不受影响。注意:不接管时 `tt status` 的 CC 订阅额度段将没有数据来源(CC 配额只经状态栏脚本落盘)。
|
|
91
|
+
|
|
86
92
|

|
|
87
93
|
|
|
88
94
|
<details>
|
|
@@ -122,7 +128,7 @@ Codex 官方暂不支持自定义 StatusLine。Token Tracker 通过 hook 注入
|
|
|
122
128
|
|
|
123
129
|
## 报表速览
|
|
124
130
|
|
|
125
|
-
`tt status` —
|
|
131
|
+
`tt status` — 今日实时面板(今日合并概览 + 5h/7d 额度 + 今日会话)
|
|
126
132
|
|
|
127
133
|

|
|
128
134
|
|
|
@@ -138,6 +144,19 @@ Codex 官方暂不支持自定义 StatusLine。Token Tracker 通过 hook 注入
|
|
|
138
144
|
|
|
139
145
|

|
|
140
146
|
|
|
147
|
+
## 实时侧边栏(`tt sidebar`)
|
|
148
|
+
|
|
149
|
+
在终端分屏 / tmux 窄窗格里常驻,一屏总览本机所有 AI 会话:
|
|
150
|
+
|
|
151
|
+
- **活跃会话列表** — 过去 5h 内有动静的会话(Claude Code + Codex),按最近活动排序取前 10;头行显示状态灯、`项目名(分支)`、agent、模型与距上次活动时间
|
|
152
|
+
- **三地时钟** — 北京、洛杉矶、伦敦合并为一行,只显示城市与 `HH:MM`
|
|
153
|
+
- **提示词历史** — 运行中、待确认、等输入会话保留最近 5 条,历史最多 1 行、最新最多 2 行;空闲会话只显示最新 1 条,不显示「下一步」
|
|
154
|
+
- **状态灯** — 运行中(星形动画)/ 需要关注(工具调用无结果,大概率在等授权)/ 等输入 / 空闲
|
|
155
|
+
- **「下一步」建议** — 从 AI 最后一条回复的收尾段提取征询 / 待办(纯规则,不走模型);AI 正在用 AskUserQuestion 提问时直接显示问题与选项
|
|
156
|
+
- **点击跳转** — 点会话头行直接切换到它所在的 iTerm2 / tmux 窗格(需启用对应 Agent 的状态栏组件提供映射;Codex 在下一次回答完成后建立映射;iTerm2 首次点击会弹 macOS 自动化授权,属预期)
|
|
157
|
+
|
|
158
|
+
操作:鼠标拖拽选择文字,松开后自动复制;滚轮 / 方向键 / PgUp/PgDn 滚动,`q` / `Esc` / `Ctrl+C` 退出;`tt sidebar --once` 打印一帧快照即退。数据每 5s 刷新,只读本地会话记录、不写任何产物;加 `--claude` / `--codex` 可只看单个 agent。
|
|
159
|
+
|
|
141
160
|
## 安装
|
|
142
161
|
|
|
143
162
|
```bash
|
|
@@ -159,18 +178,21 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
|
|
|
159
178
|
## 使用
|
|
160
179
|
|
|
161
180
|
```bash
|
|
162
|
-
tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty
|
|
181
|
+
tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty 环境按推荐默认自动配置
|
|
163
182
|
tt # 过去一年 token 热力图 + 顶部三段概览(= tt daily)
|
|
164
183
|
tt daily # 同上(tt 无参即进 daily)
|
|
165
|
-
tt status #
|
|
184
|
+
tt status # 今日消耗、5h/7d 额度与今日会话
|
|
166
185
|
tt weekly # 周报
|
|
167
186
|
tt monthly # 月报
|
|
168
|
-
tt sessions # 最近 20 条会话明细(tt sessions
|
|
187
|
+
tt sessions # 最近 20 条会话明细(tt sessions <正整数> 改条数、--sort 改排序)
|
|
188
|
+
tt sidebar # 常驻侧边栏:活跃会话总览 + 提示词历史 + 状态灯 + 点击跳转(--once 一帧快照)
|
|
169
189
|
tt theme # 查看 / 切换配色主题(show / list / set / preview)
|
|
170
190
|
tt unsetup # 卸载并恢复安装前的配置
|
|
171
191
|
tt --version # 查看版本(-v / -V 同义)
|
|
172
192
|
```
|
|
173
193
|
|
|
194
|
+
> 多 agent 环境下想只看某一个 agent 的报表,加 `--claude` 或 `--codex` 即可(互斥),对 `status` / `daily` / `weekly` / `monthly` / `sessions` 均生效。例如 `tt daily --codex` 只显示 Codex 的热力图。会话内的 `daily` / `weekly` 默认已自动跟随当前会话的 agent,显式 flag 会覆盖该行为。
|
|
195
|
+
|
|
174
196
|
> 💡 `tt daily` 是 GitHub 风格的 token 贡献热力图(深浅绿方格)。在 Claude Code 会话里输入 `!tt daily` 即可看到彩色热力图 —— 用户主动用 `!` 执行的命令,Claude Code 会渲染其 24-bit 真彩色输出。
|
|
175
197
|
|
|
176
198
|
## 配色主题
|
|
@@ -204,9 +226,10 @@ tt monthly --theme nord # 任意报表临时换主题渲染(不持久化、
|
|
|
204
226
|
|
|
205
227
|
1. **选语言** — 中文 / English(落 `~/.config/token-tracker/config.json`)
|
|
206
228
|
2. **选配色主题** — 6 套主题上下键选择,每个选项右侧内联色板预览
|
|
207
|
-
3.
|
|
229
|
+
3. **接管 Claude Code 状态栏** — Yes/No(仅检测到 Claude Code 时;已有自定义 statusLine 会先备份、选 No 完全不碰)
|
|
230
|
+
4. **启用 Codex 伪 statusline** — Yes/No(仅检测到 Codex 时)
|
|
208
231
|
|
|
209
|
-
CI / 非 tty 环境(Docker / 脚本 / `curl|bash
|
|
232
|
+
CI / 非 tty 环境(Docker / 脚本 / `curl|bash`)自动按**推荐默认**配置:语言跟随系统设置、主题 mocha、组件默认开启但**不替换已有自定义 statusLine**。装好后想改任何一项,再跑一次 `tt setup` 即可。
|
|
210
233
|
|
|
211
234
|
### 报告排序
|
|
212
235
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
- **多 Agent 统一追踪** — Claude Code + Codex 统一读取,多 Agent 按来源分组
|
|
16
16
|
- **状态栏集成** — Claude Code 用官方 StatusLine 接口;**Codex 业界首创伪 statusline 方案**(hook 注入两行真彩色状态栏,把官方未开放的能力在 Codex 里做了出来)
|
|
17
|
+
- **实时侧边栏** — `tt sidebar` 窄窗格常驻面板:全部活跃会话一屏总览(状态灯 + 最近提示词 + 「下一步」建议),点击会话直达对应 iTerm2 / tmux 窗格
|
|
17
18
|
- **限额监控** — 实时 5h / 7d 配额百分比 + 重置倒计时
|
|
18
19
|
- **多维成本分析** — 会话 / 日 / 周 / 月多维报表,等效成本统计
|
|
19
20
|
- **定价识别** — litellm 在线定价 + 内置官方价双层兜底,覆盖 Claude / OpenAI / Gemini / Grok 及国产主流(Kimi / GLM / Qwen / 豆包 / DeepSeek / MiniMax / MiMo);新模型自动套用同系列定价、不静默归零
|
|
@@ -30,6 +31,8 @@
|
|
|
30
31
|
|
|
31
32
|
基于 Claude Code 官方自定义 StatusLine 接口,**数据完全来自本地 Claude,无任何推测**。
|
|
32
33
|
|
|
34
|
+
> 状态栏接管是**可选**的:已有自定义 statusLine 时默认保留你的配置(向导里也可随时选 No),报表命令完全不受影响。注意:不接管时 `tt status` 的 CC 订阅额度段将没有数据来源(CC 配额只经状态栏脚本落盘)。
|
|
35
|
+
|
|
33
36
|

|
|
34
37
|
|
|
35
38
|
<details>
|
|
@@ -69,7 +72,7 @@ Codex 官方暂不支持自定义 StatusLine。Token Tracker 通过 hook 注入
|
|
|
69
72
|
|
|
70
73
|
## 报表速览
|
|
71
74
|
|
|
72
|
-
`tt status` —
|
|
75
|
+
`tt status` — 今日实时面板(今日合并概览 + 5h/7d 额度 + 今日会话)
|
|
73
76
|
|
|
74
77
|

|
|
75
78
|
|
|
@@ -85,6 +88,19 @@ Codex 官方暂不支持自定义 StatusLine。Token Tracker 通过 hook 注入
|
|
|
85
88
|
|
|
86
89
|

|
|
87
90
|
|
|
91
|
+
## 实时侧边栏(`tt sidebar`)
|
|
92
|
+
|
|
93
|
+
在终端分屏 / tmux 窄窗格里常驻,一屏总览本机所有 AI 会话:
|
|
94
|
+
|
|
95
|
+
- **活跃会话列表** — 过去 5h 内有动静的会话(Claude Code + Codex),按最近活动排序取前 10;头行显示状态灯、`项目名(分支)`、agent、模型与距上次活动时间
|
|
96
|
+
- **三地时钟** — 北京、洛杉矶、伦敦合并为一行,只显示城市与 `HH:MM`
|
|
97
|
+
- **提示词历史** — 运行中、待确认、等输入会话保留最近 5 条,历史最多 1 行、最新最多 2 行;空闲会话只显示最新 1 条,不显示「下一步」
|
|
98
|
+
- **状态灯** — 运行中(星形动画)/ 需要关注(工具调用无结果,大概率在等授权)/ 等输入 / 空闲
|
|
99
|
+
- **「下一步」建议** — 从 AI 最后一条回复的收尾段提取征询 / 待办(纯规则,不走模型);AI 正在用 AskUserQuestion 提问时直接显示问题与选项
|
|
100
|
+
- **点击跳转** — 点会话头行直接切换到它所在的 iTerm2 / tmux 窗格(需启用对应 Agent 的状态栏组件提供映射;Codex 在下一次回答完成后建立映射;iTerm2 首次点击会弹 macOS 自动化授权,属预期)
|
|
101
|
+
|
|
102
|
+
操作:鼠标拖拽选择文字,松开后自动复制;滚轮 / 方向键 / PgUp/PgDn 滚动,`q` / `Esc` / `Ctrl+C` 退出;`tt sidebar --once` 打印一帧快照即退。数据每 5s 刷新,只读本地会话记录、不写任何产物;加 `--claude` / `--codex` 可只看单个 agent。
|
|
103
|
+
|
|
88
104
|
## 安装
|
|
89
105
|
|
|
90
106
|
```bash
|
|
@@ -106,18 +122,21 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
|
|
|
106
122
|
## 使用
|
|
107
123
|
|
|
108
124
|
```bash
|
|
109
|
-
tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty
|
|
125
|
+
tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty 环境按推荐默认自动配置
|
|
110
126
|
tt # 过去一年 token 热力图 + 顶部三段概览(= tt daily)
|
|
111
127
|
tt daily # 同上(tt 无参即进 daily)
|
|
112
|
-
tt status #
|
|
128
|
+
tt status # 今日消耗、5h/7d 额度与今日会话
|
|
113
129
|
tt weekly # 周报
|
|
114
130
|
tt monthly # 月报
|
|
115
|
-
tt sessions # 最近 20 条会话明细(tt sessions
|
|
131
|
+
tt sessions # 最近 20 条会话明细(tt sessions <正整数> 改条数、--sort 改排序)
|
|
132
|
+
tt sidebar # 常驻侧边栏:活跃会话总览 + 提示词历史 + 状态灯 + 点击跳转(--once 一帧快照)
|
|
116
133
|
tt theme # 查看 / 切换配色主题(show / list / set / preview)
|
|
117
134
|
tt unsetup # 卸载并恢复安装前的配置
|
|
118
135
|
tt --version # 查看版本(-v / -V 同义)
|
|
119
136
|
```
|
|
120
137
|
|
|
138
|
+
> 多 agent 环境下想只看某一个 agent 的报表,加 `--claude` 或 `--codex` 即可(互斥),对 `status` / `daily` / `weekly` / `monthly` / `sessions` 均生效。例如 `tt daily --codex` 只显示 Codex 的热力图。会话内的 `daily` / `weekly` 默认已自动跟随当前会话的 agent,显式 flag 会覆盖该行为。
|
|
139
|
+
|
|
121
140
|
> 💡 `tt daily` 是 GitHub 风格的 token 贡献热力图(深浅绿方格)。在 Claude Code 会话里输入 `!tt daily` 即可看到彩色热力图 —— 用户主动用 `!` 执行的命令,Claude Code 会渲染其 24-bit 真彩色输出。
|
|
122
141
|
|
|
123
142
|
## 配色主题
|
|
@@ -151,9 +170,10 @@ tt monthly --theme nord # 任意报表临时换主题渲染(不持久化、
|
|
|
151
170
|
|
|
152
171
|
1. **选语言** — 中文 / English(落 `~/.config/token-tracker/config.json`)
|
|
153
172
|
2. **选配色主题** — 6 套主题上下键选择,每个选项右侧内联色板预览
|
|
154
|
-
3.
|
|
173
|
+
3. **接管 Claude Code 状态栏** — Yes/No(仅检测到 Claude Code 时;已有自定义 statusLine 会先备份、选 No 完全不碰)
|
|
174
|
+
4. **启用 Codex 伪 statusline** — Yes/No(仅检测到 Codex 时)
|
|
155
175
|
|
|
156
|
-
CI / 非 tty 环境(Docker / 脚本 / `curl|bash
|
|
176
|
+
CI / 非 tty 环境(Docker / 脚本 / `curl|bash`)自动按**推荐默认**配置:语言跟随系统设置、主题 mocha、组件默认开启但**不替换已有自定义 statusLine**。装好后想改任何一项,再跑一次 `tt setup` 即可。
|
|
157
177
|
|
|
158
178
|
### 报告排序
|
|
159
179
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "token-tracker"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.8"
|
|
8
8
|
description = "Track token usage across local AI agents (Claude Code, Codex)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -26,15 +26,20 @@ classifiers = [
|
|
|
26
26
|
]
|
|
27
27
|
dependencies = [
|
|
28
28
|
"rich>=13.7",
|
|
29
|
-
|
|
29
|
+
# wizard 补丁挂在 questionary 私有内部(InquirerControl._get_choice_tokens 等),锁大版本防破坏
|
|
30
|
+
"questionary>=2.0,<3",
|
|
31
|
+
# 仅 tt sidebar live 模式用(cli 延迟 import,日常 tt 启动不加载);与 rich 同门,渲染对象直接复用
|
|
32
|
+
"textual>=1.0",
|
|
30
33
|
]
|
|
31
34
|
|
|
32
35
|
[project.optional-dependencies]
|
|
33
36
|
test = [
|
|
34
37
|
"pytest>=8.0",
|
|
38
|
+
"pytest-asyncio>=0.24",
|
|
35
39
|
]
|
|
36
40
|
dev = [
|
|
37
41
|
"pytest>=8.0",
|
|
42
|
+
"pytest-asyncio>=0.24",
|
|
38
43
|
"ruff>=0.6",
|
|
39
44
|
"mypy>=1.8",
|
|
40
45
|
]
|
|
@@ -58,12 +63,25 @@ target-version = "py311"
|
|
|
58
63
|
# E501(行长)故意不开:状态栏 ANSI 色码串本就长,启用只产生噪音
|
|
59
64
|
select = ["E4", "E7", "E9", "F", "W", "I", "UP", "B"]
|
|
60
65
|
|
|
66
|
+
[tool.ruff.lint.per-file-ignores]
|
|
67
|
+
# 状态栏脚本模板:__STATUSLINE_*__ 占位符是烘焙时注入的(F821 未定义名是模板本性);
|
|
68
|
+
# E701/E401/I001/UP015/UP017 是脚本既有风格(紧凑 import、timezone.utc 兼容手改老 python)——
|
|
69
|
+
# 豁免以保持渲染产物与历史版本逐字节一致(模板内容 = 落盘脚本内容)
|
|
70
|
+
"src/token_tracker/templates/*" = ["F821", "E701", "E401", "I001", "UP015", "UP017"]
|
|
71
|
+
|
|
61
72
|
[tool.pytest.ini_options]
|
|
62
73
|
testpaths = ["tests"]
|
|
63
74
|
addopts = "-q"
|
|
75
|
+
# sidebar 的 Textual app 用 pilot 异步测试;auto 模式让 async def 用例免装饰器直接跑
|
|
76
|
+
asyncio_mode = "auto"
|
|
64
77
|
|
|
65
78
|
[tool.mypy]
|
|
66
79
|
python_version = "3.11"
|
|
67
80
|
ignore_missing_imports = true
|
|
68
81
|
# 宽松起步,后续逐步收紧
|
|
69
82
|
check_untyped_defs = true
|
|
83
|
+
|
|
84
|
+
[[tool.mypy.overrides]]
|
|
85
|
+
# 状态栏脚本模板含烘焙占位符(__STATUSLINE_*__),不是可独立类型检查的模块
|
|
86
|
+
module = "token_tracker.templates.*"
|
|
87
|
+
ignore_errors = true
|
|
@@ -3,7 +3,7 @@ from datetime import UTC, datetime
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from .types import AgentInfo, UsageEntry
|
|
6
|
-
from .util import claude_home, iter_jsonl_dicts, project_from_cwd
|
|
6
|
+
from .util import claude_home, file_may_have_events_since, iter_jsonl_dicts, project_from_cwd
|
|
7
7
|
|
|
8
8
|
CLAUDE_DIRS = [
|
|
9
9
|
os.path.join(claude_home(), "projects"),
|
|
@@ -31,6 +31,8 @@ def load_entries(hours_back: int = 0) -> list[UsageEntry]:
|
|
|
31
31
|
if not base.is_dir():
|
|
32
32
|
continue
|
|
33
33
|
for jsonl_path in base.rglob("*.jsonl"):
|
|
34
|
+
if not file_may_have_events_since(jsonl_path, cutoff):
|
|
35
|
+
continue
|
|
34
36
|
fallback_project = _extract_project_from_dir(jsonl_path, base)
|
|
35
37
|
_parse_jsonl(jsonl_path, fallback_project, entries, seen, cutoff)
|
|
36
38
|
|
|
@@ -4,7 +4,7 @@ from datetime import UTC, datetime, timedelta
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
from .types import AgentInfo, RateLimits, UsageEntry, normalize_pct
|
|
7
|
-
from .util import codex_home, iter_jsonl_dicts, project_from_cwd
|
|
7
|
+
from .util import codex_home, file_may_have_events_since, iter_jsonl_dicts, project_from_cwd
|
|
8
8
|
|
|
9
9
|
CODEX_DIR = codex_home()
|
|
10
10
|
SESSIONS_DIR = os.path.join(CODEX_DIR, "sessions")
|
|
@@ -45,6 +45,8 @@ def load_entries(hours_back: int = 0) -> list[UsageEntry]:
|
|
|
45
45
|
return entries
|
|
46
46
|
|
|
47
47
|
for jsonl_path in sessions_path.rglob("*.jsonl"):
|
|
48
|
+
if not file_may_have_events_since(jsonl_path, cutoff):
|
|
49
|
+
continue
|
|
48
50
|
_parse_jsonl(jsonl_path, models, entries, seen, cutoff)
|
|
49
51
|
|
|
50
52
|
entries.sort(key=lambda e: e.timestamp)
|
|
@@ -2,11 +2,9 @@ import json
|
|
|
2
2
|
import os
|
|
3
3
|
from datetime import UTC, datetime
|
|
4
4
|
|
|
5
|
+
from ..config import STATUS_FILE
|
|
5
6
|
from .types import RateLimits, normalize_pct
|
|
6
7
|
|
|
7
|
-
# 与 hooks.STATUS_FILE 一致:tt 自己的产物集中放 ~/.config/token-tracker(XDG)
|
|
8
|
-
STATUS_FILE = os.path.join(os.path.expanduser("~/.config/token-tracker"), "tt-status.json")
|
|
9
|
-
|
|
10
8
|
|
|
11
9
|
def load_rate_limits() -> RateLimits | None:
|
|
12
10
|
if not os.path.exists(STATUS_FILE):
|
|
@@ -129,29 +129,6 @@ class RateLimits:
|
|
|
129
129
|
context_window: int | None = None
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
@dataclass
|
|
133
|
-
class P90Limits:
|
|
134
|
-
token_limit: int = 0
|
|
135
|
-
cost_limit: float = 0.0
|
|
136
|
-
message_limit: int = 0
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
@dataclass
|
|
140
|
-
class SessionBlock:
|
|
141
|
-
start_time: datetime
|
|
142
|
-
end_time: datetime
|
|
143
|
-
entries: list[UsageEntry] = field(default_factory=list)
|
|
144
|
-
input_tokens: int = 0
|
|
145
|
-
output_tokens: int = 0
|
|
146
|
-
cache_creation_tokens: int = 0
|
|
147
|
-
cache_read_tokens: int = 0
|
|
148
|
-
total_tokens: int = 0
|
|
149
|
-
cost_usd: float = 0.0
|
|
150
|
-
is_active: bool = False
|
|
151
|
-
burn_rate: float = 0.0
|
|
152
|
-
is_gap: bool = False
|
|
153
|
-
|
|
154
|
-
|
|
155
132
|
@dataclass
|
|
156
133
|
class StatusSummary:
|
|
157
134
|
"""tt status 头图面板:当天多 agent 合并的消耗汇总(add_token_fields 累加用)。"""
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
from collections.abc import Iterator
|
|
6
|
+
from datetime import datetime
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
|
|
@@ -45,6 +46,20 @@ def iter_jsonl_dicts(path: Path | str) -> Iterator[dict]:
|
|
|
45
46
|
return
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
def file_may_have_events_since(path: Path, cutoff: datetime | None) -> bool:
|
|
50
|
+
"""文件是否可能包含 cutoff 之后的事件;无窗口时放行,文件消失时跳过。
|
|
51
|
+
|
|
52
|
+
JSONL 只追加写,事件时间不会晚于文件 mtime,因此 mtime 早于 cutoff 的文件可在逐行解析前
|
|
53
|
+
安全排除。mtime 新只代表“可能有”,仍由解析器按事件时间做最终过滤。
|
|
54
|
+
"""
|
|
55
|
+
if cutoff is None:
|
|
56
|
+
return True
|
|
57
|
+
try:
|
|
58
|
+
return path.stat().st_mtime >= cutoff.timestamp()
|
|
59
|
+
except OSError:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
48
63
|
def project_from_cwd(cwd: str) -> str:
|
|
49
64
|
"""项目名:优先取所属 git 仓库根的目录名(逐级向上找 .git,纯文件系统、不依赖 git 二进制);
|
|
50
65
|
非仓库 / 仓库根也删了 → 回退去 home 前缀后的最后一段。
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
2
|
from collections.abc import Callable
|
|
3
|
-
from datetime import timedelta
|
|
3
|
+
from datetime import date, timedelta
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from ..adapters.types import DailyStats, MonthlyStats, SessionStats, UsageEntry, WeeklyStats
|
|
7
|
+
from ..tz import system_tz
|
|
7
8
|
from .cost import calculate_cost
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def add_token_fields(target: Any, e: UsageEntry, cost: float) -> None:
|
|
11
|
-
"""把一条 UsageEntry 的 6 个通用 token/成本字段累加到任意 *Stats /
|
|
12
|
+
"""把一条 UsageEntry 的 6 个通用 token/成本字段累加到任意 *Stats / StatusSummary 上。"""
|
|
12
13
|
target.input_tokens += e.input_tokens
|
|
13
14
|
target.output_tokens += e.output_tokens
|
|
14
15
|
target.cache_creation_tokens += e.cache_creation_tokens
|
|
@@ -47,31 +48,39 @@ def _aggregate_by_key(
|
|
|
47
48
|
return sorted(by_key.values(), key=lambda s: getattr(s, sort_key))
|
|
48
49
|
|
|
49
50
|
|
|
51
|
+
# daily/weekly/monthly 一律按**系统时区**的日历日分桶(entries 时间戳是 UTC;
|
|
52
|
+
# 不转换会把北京时间 00:00-08:00 的用量记到前一天,且与 tt status 的「今天」口径打架)。
|
|
53
|
+
# tz 在函数入口取一次(system_tz 每次调用要 readlink),闭包复用。
|
|
54
|
+
|
|
50
55
|
def aggregate_daily(entries: list[UsageEntry]) -> list[DailyStats]:
|
|
56
|
+
tz = system_tz()
|
|
51
57
|
return _aggregate_by_key(
|
|
52
58
|
entries,
|
|
53
|
-
lambda e: e.timestamp.strftime("%Y-%m-%d"),
|
|
59
|
+
lambda e: e.timestamp.astimezone(tz).strftime("%Y-%m-%d"),
|
|
54
60
|
lambda k, e: DailyStats(date=k),
|
|
55
61
|
"date",
|
|
56
62
|
)
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
def aggregate_monthly(entries: list[UsageEntry]) -> list[MonthlyStats]:
|
|
66
|
+
tz = system_tz()
|
|
60
67
|
return _aggregate_by_key(
|
|
61
68
|
entries,
|
|
62
|
-
lambda e: e.timestamp.strftime("%Y-%m"),
|
|
69
|
+
lambda e: e.timestamp.astimezone(tz).strftime("%Y-%m"),
|
|
63
70
|
lambda k, e: MonthlyStats(month=k),
|
|
64
71
|
"month",
|
|
65
72
|
)
|
|
66
73
|
|
|
67
74
|
|
|
68
75
|
def aggregate_weekly(entries: list[UsageEntry]) -> list[WeeklyStats]:
|
|
76
|
+
tz = system_tz()
|
|
77
|
+
|
|
69
78
|
def _week_key(e: UsageEntry) -> str:
|
|
70
|
-
|
|
71
|
-
return
|
|
79
|
+
d = e.timestamp.astimezone(tz).date()
|
|
80
|
+
return (d - timedelta(days=d.weekday())).isoformat()
|
|
72
81
|
|
|
73
82
|
def _factory(k: str, e: UsageEntry) -> WeeklyStats:
|
|
74
|
-
monday =
|
|
83
|
+
monday = date.fromisoformat(k)
|
|
75
84
|
sunday = monday + timedelta(days=6)
|
|
76
85
|
return WeeklyStats(week=k, week_start=monday.strftime("%m-%d"), week_end=sunday.strftime("%m-%d"))
|
|
77
86
|
|
|
@@ -102,6 +111,10 @@ def aggregate_sessions(entries: list[UsageEntry]) -> list[SessionStats]:
|
|
|
102
111
|
gap = (b.timestamp - a.timestamp).total_seconds() / 60
|
|
103
112
|
if gap <= ACTIVE_GAP_CAP_MIN:
|
|
104
113
|
active += gap
|
|
114
|
+
# codex 会话是单条 entry(段内无相邻间隔可累计),活跃时长退化为整段跨度——
|
|
115
|
+
# 否则恒为 0,被 status 的 active_minutes>=5 过滤,codex 会话永远进不了当天列表
|
|
116
|
+
if len(session_entries) == 1 and first.session_end:
|
|
117
|
+
active = duration
|
|
105
118
|
|
|
106
119
|
# 代表模型按 output_tokens(真实生成量)选,output 持平时用 total 兜底;
|
|
107
120
|
# 不用 total 直接选,避免后台小模型(如 Haiku)读了大量上下文、cache_read 撑高 total 被误判为主力。
|
|
@@ -24,7 +24,7 @@ _warned_insecure = False
|
|
|
24
24
|
# codex- 兜底覆盖 Codex 内部虚拟 model(如 codex-auto-review,stop-time auto-review gate 用)
|
|
25
25
|
_FAMILY_FALLBACK = (
|
|
26
26
|
("claude-opus", "claude-opus-4-8"),
|
|
27
|
-
("claude-sonnet", "claude-sonnet-
|
|
27
|
+
("claude-sonnet", "claude-sonnet-5"),
|
|
28
28
|
("claude-haiku", "claude-haiku-4-5-20251001"),
|
|
29
29
|
("claude-fable", "claude-fable-5"),
|
|
30
30
|
("codex-", "gpt-5.5"),
|
|
@@ -48,6 +48,10 @@ _FAMILY_FALLBACK = (
|
|
|
48
48
|
# 解析不到定价的模型只提示一次,避免聚合时每条 entry 刷屏
|
|
49
49
|
_warned_unknown_models: set[str] = set()
|
|
50
50
|
|
|
51
|
+
# model → 解析出的定价 key。非精确命中要线性扫全表(litellm 数千 key),逐 entry 调用必须记忆化。
|
|
52
|
+
# 命中后还校验 key 仍在当前 pricing 里(测试会整表替换 _pricing),失效则重算。
|
|
53
|
+
_model_key_cache: dict[str, str | None] = {}
|
|
54
|
+
|
|
51
55
|
|
|
52
56
|
def get_pricing() -> dict:
|
|
53
57
|
global _pricing
|
|
@@ -85,6 +89,16 @@ def calculate_cost(entry: UsageEntry) -> float:
|
|
|
85
89
|
def _resolve_model_key(model: str, pricing: dict) -> str | None:
|
|
86
90
|
if not model:
|
|
87
91
|
return None
|
|
92
|
+
if model in _model_key_cache:
|
|
93
|
+
cached = _model_key_cache[model]
|
|
94
|
+
if cached is None or cached in pricing:
|
|
95
|
+
return cached
|
|
96
|
+
key = _resolve_model_key_uncached(model, pricing)
|
|
97
|
+
_model_key_cache[model] = key
|
|
98
|
+
return key
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _resolve_model_key_uncached(model: str, pricing: dict) -> str | None:
|
|
88
102
|
if model in pricing:
|
|
89
103
|
return model
|
|
90
104
|
|
|
@@ -138,7 +152,8 @@ def _load_pricing() -> dict:
|
|
|
138
152
|
def _read_cache() -> dict | None:
|
|
139
153
|
try:
|
|
140
154
|
with open(CACHE_PATH, encoding="utf-8") as f:
|
|
141
|
-
|
|
155
|
+
data = json.load(f)
|
|
156
|
+
return data if isinstance(data, dict) else None
|
|
142
157
|
except (OSError, json.JSONDecodeError):
|
|
143
158
|
return None
|
|
144
159
|
|
|
@@ -243,6 +258,14 @@ def _fallback_pricing() -> dict:
|
|
|
243
258
|
"claude-opus-4-7": _OPUS_PRICING,
|
|
244
259
|
"claude-opus-4-6": _OPUS_PRICING,
|
|
245
260
|
"claude-opus-4-5": _OPUS_PRICING,
|
|
261
|
+
# Sonnet 5 当前为导入价($2 / $10 / $2.50 cache-write / $0.20 cache-read),截止 2026-08-31;
|
|
262
|
+
# 9-1 起标准价 $3 / $15(与 Sonnet 4.6 一致)。到期后需切价(注意:litellm 若已收录、以在线价为准)。
|
|
263
|
+
"claude-sonnet-5": {
|
|
264
|
+
"input_cost_per_token": 2e-6,
|
|
265
|
+
"output_cost_per_token": 10e-6,
|
|
266
|
+
"cache_creation_input_token_cost": 2.5e-6,
|
|
267
|
+
"cache_read_input_token_cost": 0.2e-6,
|
|
268
|
+
},
|
|
246
269
|
"claude-sonnet-4-6": {
|
|
247
270
|
"input_cost_per_token": 3e-6,
|
|
248
271
|
"output_cost_per_token": 15e-6,
|