token-tracker 0.4.7__tar.gz → 0.4.9__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.7/src/token_tracker.egg-info → token_tracker-0.4.9}/PKG-INFO +41 -6
- {token_tracker-0.4.7 → token_tracker-0.4.9}/README.md +36 -5
- {token_tracker-0.4.7 → token_tracker-0.4.9}/pyproject.toml +12 -1
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/adapters/claude.py +3 -1
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/adapters/codex.py +3 -1
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/adapters/util.py +15 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/analyzer/cost.py +2 -1
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/cli.py +131 -92
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/config.py +6 -2
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/hooks.py +125 -90
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/i18n.py +50 -4
- token_tracker-0.4.9/src/token_tracker/iterm_split.py +233 -0
- token_tracker-0.4.9/src/token_tracker/sidebar.py +699 -0
- token_tracker-0.4.9/src/token_tracker/sidebar_command.py +181 -0
- token_tracker-0.4.9/src/token_tracker/sidebar_events.py +120 -0
- token_tracker-0.4.9/src/token_tracker/sidebar_install.py +336 -0
- token_tracker-0.4.9/src/token_tracker/skills/__init__.py +1 -0
- token_tracker-0.4.9/src/token_tracker/skills/tt_sidebar/SKILL.md +18 -0
- token_tracker-0.4.9/src/token_tracker/skills/tt_sidebar/__init__.py +1 -0
- token_tracker-0.4.9/src/token_tracker/skills/tt_sidebar/agents/openai.yaml +6 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/templates/claude_statusline.py +42 -10
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/templates/codex_statusline.py +79 -9
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/ui/heatmap.py +5 -5
- token_tracker-0.4.9/src/token_tracker/ui/report_stats.py +56 -0
- token_tracker-0.4.9/src/token_tracker/ui/sidebar.py +303 -0
- token_tracker-0.4.9/src/token_tracker/ui/sidebar_app.py +466 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/ui/tables.py +6 -59
- {token_tracker-0.4.7 → token_tracker-0.4.9/src/token_tracker.egg-info}/PKG-INFO +41 -6
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker.egg-info/SOURCES.txt +17 -0
- token_tracker-0.4.9/src/token_tracker.egg-info/requires.txt +16 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/tests/test_cli.py +10 -3
- {token_tracker-0.4.7 → token_tracker-0.4.9}/tests/test_cost.py +7 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/tests/test_heatmap.py +3 -1
- {token_tracker-0.4.7 → token_tracker-0.4.9}/tests/test_hooks.py +268 -76
- token_tracker-0.4.9/tests/test_iterm_split.py +101 -0
- token_tracker-0.4.9/tests/test_sidebar.py +929 -0
- token_tracker-0.4.9/tests/test_sidebar_app.py +380 -0
- token_tracker-0.4.9/tests/test_sidebar_command.py +124 -0
- token_tracker-0.4.9/tests/test_sidebar_install.py +175 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/tests/test_status.py +3 -3
- token_tracker-0.4.9/tests/test_util.py +83 -0
- token_tracker-0.4.7/src/token_tracker.egg-info/requires.txt +0 -10
- token_tracker-0.4.7/tests/test_util.py +0 -34
- {token_tracker-0.4.7 → token_tracker-0.4.9}/LICENSE +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/setup.cfg +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/__init__.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/adapters/__init__.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/adapters/rate_limits.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/adapters/registry.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/adapters/types.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/analyzer/__init__.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/analyzer/aggregator.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/templates/__init__.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/tz.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/ui/__init__.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/ui/console.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/ui/format.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/ui/status.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/ui/theme.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/ui/themes.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker/wizard.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker.egg-info/dependency_links.txt +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker.egg-info/entry_points.txt +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/src/token_tracker.egg-info/top_level.txt +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/tests/test_aggregator.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/tests/test_codex.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/tests/test_tables.py +0 -0
- {token_tracker-0.4.7 → token_tracker-0.4.9}/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.9
|
|
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
|
|
@@ -43,10 +43,14 @@ Description-Content-Type: text/markdown
|
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Requires-Dist: rich>=13.7
|
|
45
45
|
Requires-Dist: questionary<3,>=2.0
|
|
46
|
+
Requires-Dist: textual>=1.0
|
|
47
|
+
Requires-Dist: iterm2>=2.9; sys_platform == "darwin"
|
|
46
48
|
Provides-Extra: test
|
|
47
49
|
Requires-Dist: pytest>=8.0; extra == "test"
|
|
50
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
|
|
48
51
|
Provides-Extra: dev
|
|
49
52
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
53
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
50
54
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
51
55
|
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
52
56
|
Dynamic: license-file
|
|
@@ -67,6 +71,8 @@ Dynamic: license-file
|
|
|
67
71
|
|
|
68
72
|
- **多 Agent 统一追踪** — Claude Code + Codex 统一读取,多 Agent 按来源分组
|
|
69
73
|
- **状态栏集成** — Claude Code 用官方 StatusLine 接口;**Codex 业界首创伪 statusline 方案**(hook 注入两行真彩色状态栏,把官方未开放的能力在 Codex 里做了出来)
|
|
74
|
+
- **实时侧边栏** — `tt sidebar` 窄窗格常驻面板:全部活跃会话一屏总览(状态灯 + 最近提示词 + 「下一步」建议),点击会话直达对应 iTerm2 / tmux 窗格
|
|
75
|
+
- **当前会话自动分屏** — Codex 中显式执行 `$tt-sidebar`,在原会话右侧自动打开 1/3 宽度的独立提示词侧边栏
|
|
70
76
|
- **限额监控** — 实时 5h / 7d 配额百分比 + 重置倒计时
|
|
71
77
|
- **多维成本分析** — 会话 / 日 / 周 / 月多维报表,等效成本统计
|
|
72
78
|
- **定价识别** — litellm 在线定价 + 内置官方价双层兜底,覆盖 Claude / OpenAI / Gemini / Grok 及国产主流(Kimi / GLM / Qwen / 豆包 / DeepSeek / MiniMax / MiMo);新模型自动套用同系列定价、不静默归零
|
|
@@ -124,7 +130,7 @@ Codex 官方暂不支持自定义 StatusLine。Token Tracker 通过 hook 注入
|
|
|
124
130
|
|
|
125
131
|
## 报表速览
|
|
126
132
|
|
|
127
|
-
`tt status` —
|
|
133
|
+
`tt status` — 今日实时面板(今日合并概览 + 5h/7d 额度 + 今日会话)
|
|
128
134
|
|
|
129
135
|

|
|
130
136
|
|
|
@@ -140,6 +146,34 @@ Codex 官方暂不支持自定义 StatusLine。Token Tracker 通过 hook 注入
|
|
|
140
146
|
|
|
141
147
|

|
|
142
148
|
|
|
149
|
+
## 实时侧边栏(`tt sidebar`)
|
|
150
|
+
|
|
151
|
+
在终端分屏 / tmux 窄窗格里常驻,一屏总览本机所有 AI 会话:
|
|
152
|
+
|
|
153
|
+
- **活跃会话列表** — 过去 5h 内有动静的会话(Claude Code + Codex),按最近活动排序取前 10;头行显示状态灯、`项目名(分支)`、agent、模型与距上次活动时间
|
|
154
|
+
- **三地时钟** — 北京、洛杉矶、伦敦合并为一行,只显示城市与 `HH:MM`
|
|
155
|
+
- **提示词历史** — 运行中、待确认、等输入会话保留最近 5 条,历史最多 1 行、最新最多 2 行;空闲会话只显示最新 1 条,不显示「下一步」
|
|
156
|
+
- **状态灯** — 运行中(星形动画)/ 需要关注(工具调用无结果,大概率在等授权)/ 等输入 / 空闲
|
|
157
|
+
- **「下一步」建议** — 从 AI 最后一条回复的收尾段提取征询 / 待办(纯规则,不走模型);AI 正在用 AskUserQuestion 提问时直接显示问题与选项
|
|
158
|
+
- **点击跳转** — 点会话头行直接切换到它所在的 iTerm2 / tmux 窗格(需启用对应 Agent 的状态栏组件提供映射;Codex 在下一次回答完成后建立映射;iTerm2 首次点击会弹 macOS 自动化授权,属预期)
|
|
159
|
+
|
|
160
|
+
操作:鼠标拖拽选择文字,松开后自动复制;滚轮 / 方向键 / PgUp/PgDn 滚动,`q` / `Esc` / `Ctrl+C` 退出;`tt sidebar --once` 打印一帧快照即退。数据每 5s 刷新,只读本地会话记录、不写任何产物;加 `--claude` / `--codex` 可只看单个 agent。
|
|
161
|
+
|
|
162
|
+
### Codex 当前会话自动 1/3 分屏(`$tt-sidebar`)
|
|
163
|
+
|
|
164
|
+
`tt setup` 会把 `$tt-sidebar` 安装为用户级 Codex Skill。它与普通 `tt sidebar` 相互独立:普通命令继续显示全部活跃会话;Skill 只显示发起命令的当前 Codex 会话,右侧自动占 1/3 宽度,完整提示词按时间倒序展示,最新永远置顶。
|
|
165
|
+
|
|
166
|
+
在 Codex 输入:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
$tt-sidebar
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- 支持 iTerm2(需在 iTerm2 设置中启用 Python API)与 tmux;原会话窗格保持焦点。
|
|
173
|
+
- `tt setup` 把 Codex 的伪 statusline `Stop` 与 sidebar `UserPromptSubmit` 统一安装到用户级 `hooks.json`;后者用本地 FIFO 把新提示词推给已打开的分屏,无 sidebar 时立即返回,不轮询 transcript、不上传或持久化提示词。
|
|
174
|
+
- Codex 会要求审查非托管 Hook:安装后运行 `/hooks`,信任 Token Tracker 对应项。Skill 未立即出现时重启 Codex。
|
|
175
|
+
- `tt unsetup` 会一并移除 Token Tracker 管理的 Skill 与 Hook;若 `~/.agents/skills/tt-sidebar` 已是用户自己的同名 Skill,安装与卸载都不会覆盖它。
|
|
176
|
+
|
|
143
177
|
## 安装
|
|
144
178
|
|
|
145
179
|
```bash
|
|
@@ -148,7 +182,7 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
|
|
|
148
182
|
|
|
149
183
|
脚本自动选最优安装方式(uv / pipx / 私有 venv),绕开 PEP 668、不污染系统 Python。
|
|
150
184
|
|
|
151
|
-
>
|
|
185
|
+
> **升级**:重跑上面的命令即可(脚本幂等、自动升到最新)。包含新 Agent 集成的版本升级后,再运行一次 `tt setup`;例如 `$tt-sidebar` 需要由 setup 安装到 Codex 用户级 Skill 目录。
|
|
152
186
|
> **卸载**:`tt unsetup`
|
|
153
187
|
|
|
154
188
|
**升级后 `tt --version` 还是旧版?** 多半是旧版装在别的 Python 环境里遮蔽了新版(常见于 Windows、或早期用 `pip install` 装过)。卸载旧版后重装一次即可:
|
|
@@ -161,13 +195,14 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
|
|
|
161
195
|
## 使用
|
|
162
196
|
|
|
163
197
|
```bash
|
|
164
|
-
tt setup #
|
|
198
|
+
tt setup # 配置状态栏,并安装 Codex $tt-sidebar Skill / 提示词 Hook
|
|
165
199
|
tt # 过去一年 token 热力图 + 顶部三段概览(= tt daily)
|
|
166
200
|
tt daily # 同上(tt 无参即进 daily)
|
|
167
|
-
tt status #
|
|
201
|
+
tt status # 今日消耗、5h/7d 额度与今日会话
|
|
168
202
|
tt weekly # 周报
|
|
169
203
|
tt monthly # 月报
|
|
170
|
-
tt sessions # 最近 20 条会话明细(tt sessions
|
|
204
|
+
tt sessions # 最近 20 条会话明细(tt sessions <正整数> 改条数、--sort 改排序)
|
|
205
|
+
tt sidebar # 常驻侧边栏:活跃会话总览 + 提示词历史 + 状态灯 + 点击跳转(--once 一帧快照)
|
|
171
206
|
tt theme # 查看 / 切换配色主题(show / list / set / preview)
|
|
172
207
|
tt unsetup # 卸载并恢复安装前的配置
|
|
173
208
|
tt --version # 查看版本(-v / -V 同义)
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
- **多 Agent 统一追踪** — Claude Code + Codex 统一读取,多 Agent 按来源分组
|
|
16
16
|
- **状态栏集成** — Claude Code 用官方 StatusLine 接口;**Codex 业界首创伪 statusline 方案**(hook 注入两行真彩色状态栏,把官方未开放的能力在 Codex 里做了出来)
|
|
17
|
+
- **实时侧边栏** — `tt sidebar` 窄窗格常驻面板:全部活跃会话一屏总览(状态灯 + 最近提示词 + 「下一步」建议),点击会话直达对应 iTerm2 / tmux 窗格
|
|
18
|
+
- **当前会话自动分屏** — Codex 中显式执行 `$tt-sidebar`,在原会话右侧自动打开 1/3 宽度的独立提示词侧边栏
|
|
17
19
|
- **限额监控** — 实时 5h / 7d 配额百分比 + 重置倒计时
|
|
18
20
|
- **多维成本分析** — 会话 / 日 / 周 / 月多维报表,等效成本统计
|
|
19
21
|
- **定价识别** — litellm 在线定价 + 内置官方价双层兜底,覆盖 Claude / OpenAI / Gemini / Grok 及国产主流(Kimi / GLM / Qwen / 豆包 / DeepSeek / MiniMax / MiMo);新模型自动套用同系列定价、不静默归零
|
|
@@ -71,7 +73,7 @@ Codex 官方暂不支持自定义 StatusLine。Token Tracker 通过 hook 注入
|
|
|
71
73
|
|
|
72
74
|
## 报表速览
|
|
73
75
|
|
|
74
|
-
`tt status` —
|
|
76
|
+
`tt status` — 今日实时面板(今日合并概览 + 5h/7d 额度 + 今日会话)
|
|
75
77
|
|
|
76
78
|

|
|
77
79
|
|
|
@@ -87,6 +89,34 @@ Codex 官方暂不支持自定义 StatusLine。Token Tracker 通过 hook 注入
|
|
|
87
89
|
|
|
88
90
|

|
|
89
91
|
|
|
92
|
+
## 实时侧边栏(`tt sidebar`)
|
|
93
|
+
|
|
94
|
+
在终端分屏 / tmux 窄窗格里常驻,一屏总览本机所有 AI 会话:
|
|
95
|
+
|
|
96
|
+
- **活跃会话列表** — 过去 5h 内有动静的会话(Claude Code + Codex),按最近活动排序取前 10;头行显示状态灯、`项目名(分支)`、agent、模型与距上次活动时间
|
|
97
|
+
- **三地时钟** — 北京、洛杉矶、伦敦合并为一行,只显示城市与 `HH:MM`
|
|
98
|
+
- **提示词历史** — 运行中、待确认、等输入会话保留最近 5 条,历史最多 1 行、最新最多 2 行;空闲会话只显示最新 1 条,不显示「下一步」
|
|
99
|
+
- **状态灯** — 运行中(星形动画)/ 需要关注(工具调用无结果,大概率在等授权)/ 等输入 / 空闲
|
|
100
|
+
- **「下一步」建议** — 从 AI 最后一条回复的收尾段提取征询 / 待办(纯规则,不走模型);AI 正在用 AskUserQuestion 提问时直接显示问题与选项
|
|
101
|
+
- **点击跳转** — 点会话头行直接切换到它所在的 iTerm2 / tmux 窗格(需启用对应 Agent 的状态栏组件提供映射;Codex 在下一次回答完成后建立映射;iTerm2 首次点击会弹 macOS 自动化授权,属预期)
|
|
102
|
+
|
|
103
|
+
操作:鼠标拖拽选择文字,松开后自动复制;滚轮 / 方向键 / PgUp/PgDn 滚动,`q` / `Esc` / `Ctrl+C` 退出;`tt sidebar --once` 打印一帧快照即退。数据每 5s 刷新,只读本地会话记录、不写任何产物;加 `--claude` / `--codex` 可只看单个 agent。
|
|
104
|
+
|
|
105
|
+
### Codex 当前会话自动 1/3 分屏(`$tt-sidebar`)
|
|
106
|
+
|
|
107
|
+
`tt setup` 会把 `$tt-sidebar` 安装为用户级 Codex Skill。它与普通 `tt sidebar` 相互独立:普通命令继续显示全部活跃会话;Skill 只显示发起命令的当前 Codex 会话,右侧自动占 1/3 宽度,完整提示词按时间倒序展示,最新永远置顶。
|
|
108
|
+
|
|
109
|
+
在 Codex 输入:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
$tt-sidebar
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- 支持 iTerm2(需在 iTerm2 设置中启用 Python API)与 tmux;原会话窗格保持焦点。
|
|
116
|
+
- `tt setup` 把 Codex 的伪 statusline `Stop` 与 sidebar `UserPromptSubmit` 统一安装到用户级 `hooks.json`;后者用本地 FIFO 把新提示词推给已打开的分屏,无 sidebar 时立即返回,不轮询 transcript、不上传或持久化提示词。
|
|
117
|
+
- Codex 会要求审查非托管 Hook:安装后运行 `/hooks`,信任 Token Tracker 对应项。Skill 未立即出现时重启 Codex。
|
|
118
|
+
- `tt unsetup` 会一并移除 Token Tracker 管理的 Skill 与 Hook;若 `~/.agents/skills/tt-sidebar` 已是用户自己的同名 Skill,安装与卸载都不会覆盖它。
|
|
119
|
+
|
|
90
120
|
## 安装
|
|
91
121
|
|
|
92
122
|
```bash
|
|
@@ -95,7 +125,7 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
|
|
|
95
125
|
|
|
96
126
|
脚本自动选最优安装方式(uv / pipx / 私有 venv),绕开 PEP 668、不污染系统 Python。
|
|
97
127
|
|
|
98
|
-
>
|
|
128
|
+
> **升级**:重跑上面的命令即可(脚本幂等、自动升到最新)。包含新 Agent 集成的版本升级后,再运行一次 `tt setup`;例如 `$tt-sidebar` 需要由 setup 安装到 Codex 用户级 Skill 目录。
|
|
99
129
|
> **卸载**:`tt unsetup`
|
|
100
130
|
|
|
101
131
|
**升级后 `tt --version` 还是旧版?** 多半是旧版装在别的 Python 环境里遮蔽了新版(常见于 Windows、或早期用 `pip install` 装过)。卸载旧版后重装一次即可:
|
|
@@ -108,13 +138,14 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
|
|
|
108
138
|
## 使用
|
|
109
139
|
|
|
110
140
|
```bash
|
|
111
|
-
tt setup #
|
|
141
|
+
tt setup # 配置状态栏,并安装 Codex $tt-sidebar Skill / 提示词 Hook
|
|
112
142
|
tt # 过去一年 token 热力图 + 顶部三段概览(= tt daily)
|
|
113
143
|
tt daily # 同上(tt 无参即进 daily)
|
|
114
|
-
tt status #
|
|
144
|
+
tt status # 今日消耗、5h/7d 额度与今日会话
|
|
115
145
|
tt weekly # 周报
|
|
116
146
|
tt monthly # 月报
|
|
117
|
-
tt sessions # 最近 20 条会话明细(tt sessions
|
|
147
|
+
tt sessions # 最近 20 条会话明细(tt sessions <正整数> 改条数、--sort 改排序)
|
|
148
|
+
tt sidebar # 常驻侧边栏:活跃会话总览 + 提示词历史 + 状态灯 + 点击跳转(--once 一帧快照)
|
|
118
149
|
tt theme # 查看 / 切换配色主题(show / list / set / preview)
|
|
119
150
|
tt unsetup # 卸载并恢复安装前的配置
|
|
120
151
|
tt --version # 查看版本(-v / -V 同义)
|
|
@@ -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.9"
|
|
8
8
|
description = "Track token usage across local AI agents (Claude Code, Codex)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -28,14 +28,20 @@ dependencies = [
|
|
|
28
28
|
"rich>=13.7",
|
|
29
29
|
# wizard 补丁挂在 questionary 私有内部(InquirerControl._get_choice_tokens 等),锁大版本防破坏
|
|
30
30
|
"questionary>=2.0,<3",
|
|
31
|
+
# 仅 tt sidebar live 模式用(cli 延迟 import,日常 tt 启动不加载);与 rich 同门,渲染对象直接复用
|
|
32
|
+
"textual>=1.0",
|
|
33
|
+
# 仅 macOS 的 $tt-sidebar 自动 iTerm2 分屏 worker 加载;其它平台走 tmux、不安装
|
|
34
|
+
"iterm2>=2.9; sys_platform == 'darwin'",
|
|
31
35
|
]
|
|
32
36
|
|
|
33
37
|
[project.optional-dependencies]
|
|
34
38
|
test = [
|
|
35
39
|
"pytest>=8.0",
|
|
40
|
+
"pytest-asyncio>=0.24",
|
|
36
41
|
]
|
|
37
42
|
dev = [
|
|
38
43
|
"pytest>=8.0",
|
|
44
|
+
"pytest-asyncio>=0.24",
|
|
39
45
|
"ruff>=0.6",
|
|
40
46
|
"mypy>=1.8",
|
|
41
47
|
]
|
|
@@ -51,6 +57,9 @@ tt = "token_tracker.cli:main"
|
|
|
51
57
|
[tool.setuptools.packages.find]
|
|
52
58
|
where = ["src"]
|
|
53
59
|
|
|
60
|
+
[tool.setuptools.package-data]
|
|
61
|
+
"token_tracker.skills.tt_sidebar" = ["SKILL.md", "agents/*.yaml"]
|
|
62
|
+
|
|
54
63
|
[tool.ruff]
|
|
55
64
|
line-length = 120
|
|
56
65
|
target-version = "py311"
|
|
@@ -68,6 +77,8 @@ select = ["E4", "E7", "E9", "F", "W", "I", "UP", "B"]
|
|
|
68
77
|
[tool.pytest.ini_options]
|
|
69
78
|
testpaths = ["tests"]
|
|
70
79
|
addopts = "-q"
|
|
80
|
+
# sidebar 的 Textual app 用 pilot 异步测试;auto 模式让 async def 用例免装饰器直接跑
|
|
81
|
+
asyncio_mode = "auto"
|
|
71
82
|
|
|
72
83
|
[tool.mypy]
|
|
73
84
|
python_version = "3.11"
|
|
@@ -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)
|
|
@@ -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 前缀后的最后一段。
|
|
@@ -152,7 +152,8 @@ def _load_pricing() -> dict:
|
|
|
152
152
|
def _read_cache() -> dict | None:
|
|
153
153
|
try:
|
|
154
154
|
with open(CACHE_PATH, encoding="utf-8") as f:
|
|
155
|
-
|
|
155
|
+
data = json.load(f)
|
|
156
|
+
return data if isinstance(data, dict) else None
|
|
156
157
|
except (OSError, json.JSONDecodeError):
|
|
157
158
|
return None
|
|
158
159
|
|
|
@@ -20,10 +20,12 @@ from .analyzer.aggregator import (
|
|
|
20
20
|
from .analyzer.cost import calculate_cost
|
|
21
21
|
from .hooks import is_setup, needs_update, setup, unsetup, update_hook
|
|
22
22
|
from .i18n import t
|
|
23
|
+
from .sidebar import registry_update_hint, scan_sessions
|
|
23
24
|
from .tz import system_tz
|
|
24
25
|
from .ui import theme, themes
|
|
25
26
|
from .ui.console import forced_color_console, get_console
|
|
26
27
|
from .ui.heatmap import render_daily_heatmap
|
|
28
|
+
from .ui.sidebar import render_sidebar
|
|
27
29
|
from .ui.status import render_sessions_view, render_status
|
|
28
30
|
from .ui.tables import (
|
|
29
31
|
render_monthly,
|
|
@@ -59,12 +61,15 @@ _REPORT_COMMANDS: dict[str, tuple[Callable, Callable | None, str, str, bool]] =
|
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
def _parse_limit(args: list[str], default: int) -> int:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
if not args:
|
|
65
|
+
return default
|
|
66
|
+
try:
|
|
67
|
+
value = int(args[0])
|
|
68
|
+
except ValueError as exc:
|
|
69
|
+
raise ValueError(args[0]) from exc
|
|
70
|
+
if value <= 0:
|
|
71
|
+
raise ValueError(args[0])
|
|
72
|
+
return value
|
|
68
73
|
|
|
69
74
|
|
|
70
75
|
def _extract_agent_arg(args: list[str]) -> tuple[list[str], str | None]:
|
|
@@ -216,6 +221,22 @@ def _summary_from_sessions(sessions) -> StatusSummary:
|
|
|
216
221
|
return sm
|
|
217
222
|
|
|
218
223
|
|
|
224
|
+
def _cmd_sidebar(agents, args: list[str]) -> None:
|
|
225
|
+
"""常驻侧边栏(窄窗格用):活跃会话 + 各自最近提示词。live 模式跑 Textual app
|
|
226
|
+
(备用屏 + 滚动 + 5s 定时刷新,q / Ctrl+C 退出);`--once` 或非 tty 打一帧
|
|
227
|
+
Rich 快照即退(脚本 / `!tt sidebar` / 测试用)。只读 transcript 与心跳文件,
|
|
228
|
+
不写任何产物;不跟随会话收窄 agent——侧边栏本职是「总览所有会话」,
|
|
229
|
+
显式 --claude / --codex 才过滤。"""
|
|
230
|
+
agent_ids = {a.id for a in agents}
|
|
231
|
+
if "--once" in args or not sys.stdout.isatty():
|
|
232
|
+
sessions = scan_sessions(agent_ids=agent_ids)
|
|
233
|
+
with forced_color_console():
|
|
234
|
+
get_console().print(render_sidebar(sessions, update_hint=registry_update_hint(sessions)))
|
|
235
|
+
return
|
|
236
|
+
from .ui.sidebar_app import SidebarApp # 延迟 import:textual 仅 live 模式加载
|
|
237
|
+
SidebarApp(agent_ids=agent_ids).run()
|
|
238
|
+
|
|
239
|
+
|
|
219
240
|
def _current_session_agent() -> str | None:
|
|
220
241
|
"""识别当前所在的 agent 会话(靠**会话内才有**的环境变量):Codex / Claude Code;独立终端返回 None。
|
|
221
242
|
不能用 CLAUDE_CONFIG_DIR 判断——那是用户级配置变量(可长期 export 在 shell profile 里挪配置目录,
|
|
@@ -385,132 +406,150 @@ def cmd_theme(args: list[str]) -> None:
|
|
|
385
406
|
get_console().print(f"[dim]{t('theme_usage')}[/dim]")
|
|
386
407
|
|
|
387
408
|
|
|
388
|
-
def
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
409
|
+
def _apply_theme_override(name: str | None) -> None:
|
|
410
|
+
if name is None:
|
|
411
|
+
return
|
|
412
|
+
if name not in themes.THEMES:
|
|
413
|
+
get_console().print(f"[red]{t('theme_unknown', name=name)}[/red]")
|
|
414
|
+
get_console().print(f"[dim]{t('theme_options', names=', '.join(themes.THEME_NAMES))}[/dim]")
|
|
415
|
+
sys.exit(1)
|
|
416
|
+
theme.set_active_theme(name)
|
|
393
417
|
|
|
394
|
-
args = sys.argv[1:]
|
|
395
|
-
# --claude / --codex:多 agent 环境按需只看一个 agent 的报表(issue #19),互斥;不给则走默认
|
|
396
|
-
args, filter_agent = _extract_agent_arg(args)
|
|
397
|
-
# --theme NAME:临时覆盖主题(仅本次进程、不落配置/不重烘焙状态栏),对所有报表 + status 生效
|
|
398
|
-
args, theme_override = _extract_theme_arg(args)
|
|
399
|
-
if theme_override is not None:
|
|
400
|
-
if theme_override not in themes.THEMES:
|
|
401
|
-
get_console().print(f"[red]{t('theme_unknown', name=theme_override)}[/red]")
|
|
402
|
-
get_console().print(f"[dim]{t('theme_options', names=', '.join(themes.THEME_NAMES))}[/dim]")
|
|
403
|
-
sys.exit(1)
|
|
404
|
-
theme.set_active_theme(theme_override)
|
|
405
|
-
command = args[0] if args else "daily"
|
|
406
418
|
|
|
407
|
-
|
|
419
|
+
def _handle_non_data_command(command: str, args: list[str]) -> bool:
|
|
420
|
+
"""处理不需要 Agent 数据的命令;已处理返回 True。"""
|
|
408
421
|
if command in ("--version", "-v", "-V"):
|
|
409
422
|
print(f"token-tracker {_get_version()}")
|
|
410
423
|
print("by stormzhang · https://github.com/stormzhang/token-tracker")
|
|
411
|
-
return
|
|
412
|
-
|
|
424
|
+
return True
|
|
413
425
|
if command == "theme":
|
|
414
|
-
cmd_theme(args
|
|
415
|
-
return
|
|
416
|
-
|
|
417
|
-
# 已配置过的情况下,任意命令都顺带同步状态栏脚本(setup/unsetup 自行处理)
|
|
418
|
-
# 避免升级 pip 包后忘了 tt setup,导致 ~/.claude/tt-statusline.py 停在旧版本
|
|
419
|
-
if command not in ("setup", "unsetup") and is_setup() and needs_update():
|
|
420
|
-
update_hook()
|
|
421
|
-
|
|
422
|
-
# 升级感知:新版若新增了值得重配的选项(SETUP_VERSION bump),老用户跑任意命令时
|
|
423
|
-
# 自动走一遍 setup——_run_setup_flow 内部分流:真终端弹 wizard、会话内 / 非 tty 静默
|
|
424
|
-
# _auto_setup 用默认值全装(语言跟随系统 / mocha / 组件全开)。两者最终都 save_setup_version(),
|
|
425
|
-
# 下次启动 setup_version 已是最新、不再触发。
|
|
426
|
-
if (
|
|
427
|
-
command not in ("setup", "unsetup")
|
|
428
|
-
and is_setup()
|
|
429
|
-
and config.setup_version() < config.SETUP_VERSION
|
|
430
|
-
):
|
|
431
|
-
_run_setup_flow()
|
|
432
|
-
|
|
426
|
+
cmd_theme(args)
|
|
427
|
+
return True
|
|
433
428
|
if command == "setup":
|
|
434
429
|
_run_setup_flow()
|
|
435
|
-
return
|
|
430
|
+
return True
|
|
436
431
|
if command == "unsetup":
|
|
437
432
|
unsetup()
|
|
438
|
-
return
|
|
433
|
+
return True
|
|
434
|
+
return False
|
|
435
|
+
|
|
439
436
|
|
|
440
|
-
|
|
441
|
-
|
|
437
|
+
def _ensure_data_ready() -> None:
|
|
438
|
+
"""数据命令运行前同步脚本、处理升级引导,并确保至少完成过一次 setup。"""
|
|
439
|
+
configured = is_setup()
|
|
440
|
+
if configured and needs_update():
|
|
441
|
+
update_hook()
|
|
442
|
+
if configured and config.setup_version() < config.SETUP_VERSION:
|
|
443
|
+
_run_setup_flow()
|
|
442
444
|
if not is_setup():
|
|
443
445
|
_run_setup_flow()
|
|
444
446
|
if not is_setup():
|
|
445
447
|
sys.exit(1)
|
|
446
448
|
|
|
447
|
-
agents = detect_agents()
|
|
448
|
-
if filter_agent is not None:
|
|
449
|
-
# 显式指定 agent:收窄 agents 列表,所有报表命令自动跟随;未装 / 未检测到 → 报错退出
|
|
450
|
-
matched = [a for a in agents if a.id == filter_agent]
|
|
451
|
-
if not matched:
|
|
452
|
-
flag = "--claude" if filter_agent == "claude-code" else "--codex"
|
|
453
|
-
get_console().print(f"[red]{t('agent_not_detected', flag=flag)}[/red]")
|
|
454
|
-
sys.exit(1)
|
|
455
|
-
agents = matched
|
|
456
|
-
agent_ids = {a.id for a in agents}
|
|
457
|
-
|
|
458
|
-
if command in ("status", "dashboard"):
|
|
459
|
-
data = _build_status_data(agents)
|
|
460
|
-
if not data:
|
|
461
|
-
get_console().print(f"[yellow]{t('no_token_data')}[/yellow]")
|
|
462
|
-
return
|
|
463
|
-
render_status(**data)
|
|
464
|
-
return
|
|
465
|
-
|
|
466
|
-
rest_args, sort_key, sort_desc = _parse_sort_args(args[1:])
|
|
467
449
|
|
|
450
|
+
def _select_agents(filter_agent: str | None):
|
|
451
|
+
agents = detect_agents()
|
|
452
|
+
if filter_agent is None:
|
|
453
|
+
return agents
|
|
454
|
+
matched = [agent for agent in agents if agent.id == filter_agent]
|
|
455
|
+
if matched:
|
|
456
|
+
return matched
|
|
457
|
+
flag = "--claude" if filter_agent == "claude-code" else "--codex"
|
|
458
|
+
get_console().print(f"[red]{t('agent_not_detected', flag=flag)}[/red]")
|
|
459
|
+
sys.exit(1)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _report_agents(agents, command: str, filter_agent: str | None):
|
|
463
|
+
"""daily/weekly 在 Agent 会话内自动收窄;显式 filter 优先。"""
|
|
464
|
+
if filter_agent is not None or command not in ("daily", "weekly"):
|
|
465
|
+
return agents
|
|
466
|
+
session_agent = _current_session_agent()
|
|
467
|
+
if not session_agent or session_agent not in {agent.id for agent in agents}:
|
|
468
|
+
return agents
|
|
469
|
+
return [agent for agent in agents if agent.id == session_agent]
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _run_report_command(command: str, args: list[str], agents, filter_agent: str | None) -> None:
|
|
473
|
+
rest_args, sort_key, sort_desc = _parse_sort_args(args)
|
|
468
474
|
if command not in _REPORT_COMMANDS:
|
|
469
475
|
get_console().print(f"[red]{t('unknown_cmd', cmd=command)}[/red]")
|
|
470
476
|
get_console().print(f"[dim]{t('available_cmds')}[/dim]")
|
|
471
477
|
sys.exit(1)
|
|
472
478
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
report_agents = agents
|
|
476
|
-
if filter_agent is None and command in ("daily", "weekly"):
|
|
477
|
-
session_agent = _current_session_agent()
|
|
478
|
-
if session_agent and session_agent in agent_ids:
|
|
479
|
-
report_agents = [a for a in agents if a.id == session_agent]
|
|
480
|
-
agent_names = [a.name for a in report_agents]
|
|
481
|
-
|
|
479
|
+
selected = _report_agents(agents, command, filter_agent)
|
|
480
|
+
agent_names = [agent.name for agent in selected]
|
|
482
481
|
agg_fn, render_fn, time_attr, no_sort_attr, default_reverse = _REPORT_COMMANDS[command]
|
|
483
|
-
loaded = _load_per_agent(
|
|
482
|
+
loaded = _load_per_agent(selected)
|
|
484
483
|
stats = _aggregate_per_agent(loaded, agg_fn)
|
|
485
484
|
default_attr = time_attr if sort_key == "time" else no_sort_attr
|
|
486
485
|
|
|
487
486
|
if command == "sessions":
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
# 恒久霸榜、新会话和低成本 agent 永远进不了榜),这 N 条再按 cost(或 --sort)展示
|
|
491
|
-
kept = [s for s in stats if s.active_minutes >= 5]
|
|
492
|
-
kept.sort(key=lambda s: s.start_time, reverse=True)
|
|
493
|
-
shown = kept[:_parse_limit(rest_args, default=20)]
|
|
494
|
-
_apply_sort(shown, sort_key, sort_desc, default_attr, default_reverse)
|
|
495
|
-
render_sessions_view(_summary_from_sessions(shown), shown, agent_names)
|
|
487
|
+
_render_session_report(stats, rest_args, sort_key, sort_desc,
|
|
488
|
+
default_attr, default_reverse, agent_names)
|
|
496
489
|
return
|
|
497
490
|
|
|
498
491
|
assert render_fn is not None # sessions(render_fn=None)已在上面 return,其余命令都有渲染函数
|
|
499
492
|
_apply_sort(stats, sort_key, sort_desc, default_attr, default_reverse)
|
|
500
493
|
if command == "daily":
|
|
501
|
-
# daily 顶部三卡片:Last 12 months + This Month + This Week,跟 weekly/monthly 同款样式、
|
|
502
|
-
# 复用 _render_month_summary / _render_week_summary。weekly/monthly 聚合复用同一份 entries。
|
|
503
494
|
render_fn(stats, agents=agent_names,
|
|
504
495
|
weekly=_aggregate_per_agent(loaded, aggregate_weekly),
|
|
505
496
|
monthly=_aggregate_per_agent(loaded, aggregate_monthly))
|
|
506
497
|
elif command == "weekly":
|
|
507
498
|
render_weekly(stats, agents=agent_names, daily=_aggregate_per_agent(loaded, aggregate_daily))
|
|
508
|
-
|
|
499
|
+
else: # monthly
|
|
509
500
|
render_monthly(stats, agents=agent_names,
|
|
510
501
|
daily=_aggregate_per_agent(loaded, aggregate_daily),
|
|
511
502
|
weekly=_aggregate_per_agent(loaded, aggregate_weekly))
|
|
512
|
-
|
|
513
|
-
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _render_session_report(stats, rest_args: list[str], sort_key: str | None,
|
|
506
|
+
sort_desc: bool | None, default_attr: str,
|
|
507
|
+
default_reverse: bool, agent_names: list[str]) -> None:
|
|
508
|
+
# 先按时间取最近 N 条,再按用户指定字段展示;避免历史高 cost 会话长期霸榜。
|
|
509
|
+
kept = [session for session in stats if session.active_minutes >= 5]
|
|
510
|
+
kept.sort(key=lambda session: session.start_time, reverse=True)
|
|
511
|
+
try:
|
|
512
|
+
limit = _parse_limit(rest_args, default=20)
|
|
513
|
+
except ValueError as exc:
|
|
514
|
+
get_console().print(f"[red]{t('sessions_limit_invalid', value=exc.args[0])}[/red]")
|
|
515
|
+
sys.exit(1)
|
|
516
|
+
shown = kept[:limit]
|
|
517
|
+
_apply_sort(shown, sort_key, sort_desc, default_attr, default_reverse)
|
|
518
|
+
render_sessions_view(_summary_from_sessions(shown), shown, agent_names)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def main():
|
|
522
|
+
# --mock:本地开发演示,加载 mock/ 假数据再走正常报表流程(mock/ 在 .gitignore)
|
|
523
|
+
if "--mock" in sys.argv:
|
|
524
|
+
sys.argv = [a for a in sys.argv if a != "--mock"]
|
|
525
|
+
_load_local_mock()
|
|
526
|
+
|
|
527
|
+
args = sys.argv[1:]
|
|
528
|
+
# --claude / --codex:多 agent 环境按需只看一个 agent 的报表(issue #19),互斥;不给则走默认
|
|
529
|
+
args, filter_agent = _extract_agent_arg(args)
|
|
530
|
+
# --theme NAME:临时覆盖主题(仅本次进程、不落配置/不重烘焙状态栏),对所有报表 + status 生效
|
|
531
|
+
args, theme_override = _extract_theme_arg(args)
|
|
532
|
+
_apply_theme_override(theme_override)
|
|
533
|
+
command = args[0] if args else "daily"
|
|
534
|
+
|
|
535
|
+
if _handle_non_data_command(command, args[1:]):
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
_ensure_data_ready()
|
|
539
|
+
agents = _select_agents(filter_agent)
|
|
540
|
+
|
|
541
|
+
if command in ("status", "dashboard"):
|
|
542
|
+
data = _build_status_data(agents)
|
|
543
|
+
if not data:
|
|
544
|
+
get_console().print(f"[yellow]{t('no_token_data')}[/yellow]")
|
|
545
|
+
return
|
|
546
|
+
render_status(**data)
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
if command == "sidebar":
|
|
550
|
+
_cmd_sidebar(agents, args[1:])
|
|
551
|
+
return
|
|
552
|
+
_run_report_command(command, args[1:], agents, filter_agent)
|
|
514
553
|
|
|
515
554
|
|
|
516
555
|
if __name__ == "__main__":
|
|
@@ -18,17 +18,21 @@ from .ui import themes
|
|
|
18
18
|
CONFIG_DIR = os.path.expanduser("~/.config/token-tracker")
|
|
19
19
|
CONFIG_PATH = os.path.join(CONFIG_DIR, "config.json")
|
|
20
20
|
# CC statusline 缓存的单一权威路径(statusline 脚本写、tt status / adapters.rate_limits 读)。
|
|
21
|
-
#
|
|
21
|
+
# 注意两个 statusline 模板因脚本独立运行硬编码了对应缓存路径——改这里必须同步改模板。
|
|
22
22
|
STATUS_FILE = os.path.join(CONFIG_DIR, "tt-status.json")
|
|
23
|
+
# Codex Stop hook 采集的会话→终端窗格映射。与 CC 心跳/status 缓存分文件,避免两个
|
|
24
|
+
# Agent 并发 read-modify-write 同一 JSON 时互相覆盖;sidebar 读取时再合并。
|
|
25
|
+
TERMINAL_MAP_FILE = os.path.join(CONFIG_DIR, "tt-terminal-map.json")
|
|
23
26
|
SCHEMA_VERSION = 1
|
|
24
27
|
|
|
25
28
|
# 引导版本:每次新增"值得让老用户重新走一遍 setup"的配置项时手动 +1(只能整数、一次 +1)。
|
|
26
29
|
# 老用户 config 里没这字段 / 旧版本号 < 当前 → 触发重新引导(真终端弹 wizard、非 tty 静默 _auto_setup)。
|
|
27
30
|
# 跟 SCHEMA_VERSION 解耦:那是数据格式版本,这是用户引导版本,bump 节奏完全不同。
|
|
28
31
|
# 2(0.4.2):强制所有现存用户(0.3.8/0.4.0=无字段=0、0.4.1=1,全 < 2)升级后重走一遍 setup。
|
|
32
|
+
# 3:安装随包分发的用户级 Codex $tt-sidebar Skill + UserPromptSubmit hook。
|
|
29
33
|
# 注:CC statusLine 变可选组件(issue #16/#17)时决定**不 bump**——小众需求不打断存量用户,
|
|
30
34
|
# 存量 tt 用户 intent 缺失时由 is_setup 按「statusLine 已是 tt 的」推断为已配,想改的手动 tt setup。
|
|
31
|
-
SETUP_VERSION =
|
|
35
|
+
SETUP_VERSION = 3
|
|
32
36
|
|
|
33
37
|
# 旧位置(独立 theme.json / lang.json),老用户首次读 config.json 不存在时自动合并迁移
|
|
34
38
|
_LEGACY_THEME_PATH = os.path.join(CONFIG_DIR, "theme.json")
|