token-tracker 0.4.6__tar.gz → 0.4.7__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.
Files changed (54) hide show
  1. {token_tracker-0.4.6/src/token_tracker.egg-info → token_tracker-0.4.7}/PKG-INFO +10 -5
  2. {token_tracker-0.4.6 → token_tracker-0.4.7}/README.md +8 -3
  3. {token_tracker-0.4.6 → token_tracker-0.4.7}/pyproject.toml +14 -2
  4. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/rate_limits.py +1 -3
  5. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/types.py +0 -23
  6. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/analyzer/aggregator.py +20 -7
  7. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/analyzer/cost.py +23 -1
  8. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/cli.py +77 -32
  9. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/config.py +18 -1
  10. token_tracker-0.4.7/src/token_tracker/hooks.py +634 -0
  11. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/i18n.py +20 -98
  12. token_tracker-0.4.7/src/token_tracker/templates/claude_statusline.py +370 -0
  13. token_tracker-0.4.7/src/token_tracker/templates/codex_statusline.py +233 -0
  14. token_tracker-0.4.7/src/token_tracker/tz.py +22 -0
  15. token_tracker-0.4.7/src/token_tracker/ui/__init__.py +0 -0
  16. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/format.py +1 -23
  17. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/heatmap.py +8 -6
  18. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/status.py +11 -3
  19. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/tables.py +10 -73
  20. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/theme.py +1 -9
  21. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/wizard.py +21 -10
  22. {token_tracker-0.4.6 → token_tracker-0.4.7/src/token_tracker.egg-info}/PKG-INFO +10 -5
  23. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker.egg-info/SOURCES.txt +4 -3
  24. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker.egg-info/requires.txt +1 -1
  25. {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_aggregator.py +41 -2
  26. token_tracker-0.4.7/tests/test_cli.py +132 -0
  27. {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_cost.py +25 -0
  28. {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_hooks.py +342 -7
  29. {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_status.py +31 -5
  30. token_tracker-0.4.6/src/token_tracker/analyzer/blocks.py +0 -68
  31. token_tracker-0.4.6/src/token_tracker/hooks.py +0 -1100
  32. token_tracker-0.4.6/src/token_tracker/ui/panels.py +0 -65
  33. token_tracker-0.4.6/tests/test_blocks.py +0 -79
  34. token_tracker-0.4.6/tests/test_cli.py +0 -38
  35. {token_tracker-0.4.6 → token_tracker-0.4.7}/LICENSE +0 -0
  36. {token_tracker-0.4.6 → token_tracker-0.4.7}/setup.cfg +0 -0
  37. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/__init__.py +0 -0
  38. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/__init__.py +0 -0
  39. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/claude.py +0 -0
  40. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/codex.py +0 -0
  41. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/registry.py +0 -0
  42. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/util.py +0 -0
  43. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/analyzer/__init__.py +0 -0
  44. {token_tracker-0.4.6/src/token_tracker/ui → token_tracker-0.4.7/src/token_tracker/templates}/__init__.py +0 -0
  45. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/console.py +0 -0
  46. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/themes.py +0 -0
  47. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker.egg-info/dependency_links.txt +0 -0
  48. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker.egg-info/entry_points.txt +0 -0
  49. {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker.egg-info/top_level.txt +0 -0
  50. {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_codex.py +0 -0
  51. {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_heatmap.py +0 -0
  52. {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_tables.py +0 -0
  53. {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_theme.py +0 -0
  54. {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: token-tracker
3
- Version: 0.4.6
3
+ Version: 0.4.7
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,7 +42,7 @@ 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>=2.0
45
+ Requires-Dist: questionary<3,>=2.0
46
46
  Provides-Extra: test
47
47
  Requires-Dist: pytest>=8.0; extra == "test"
48
48
  Provides-Extra: dev
@@ -83,6 +83,8 @@ Dynamic: license-file
83
83
 
84
84
  基于 Claude Code 官方自定义 StatusLine 接口,**数据完全来自本地 Claude,无任何推测**。
85
85
 
86
+ > 状态栏接管是**可选**的:已有自定义 statusLine 时默认保留你的配置(向导里也可随时选 No),报表命令完全不受影响。注意:不接管时 `tt status` 的 CC 订阅额度段将没有数据来源(CC 配额只经状态栏脚本落盘)。
87
+
86
88
  ![Claude Code StatusLine](assets/screenshot-statusline-cc.png)
87
89
 
88
90
  <details>
@@ -159,7 +161,7 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
159
161
  ## 使用
160
162
 
161
163
  ```bash
162
- tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty 环境自动全装
164
+ tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty 环境按推荐默认自动配置
163
165
  tt # 过去一年 token 热力图 + 顶部三段概览(= tt daily)
164
166
  tt daily # 同上(tt 无参即进 daily)
165
167
  tt status # 过去 5h 实时面板
@@ -171,6 +173,8 @@ tt unsetup # 卸载并恢复安装前的配置
171
173
  tt --version # 查看版本(-v / -V 同义)
172
174
  ```
173
175
 
176
+ > 多 agent 环境下想只看某一个 agent 的报表,加 `--claude` 或 `--codex` 即可(互斥),对 `status` / `daily` / `weekly` / `monthly` / `sessions` 均生效。例如 `tt daily --codex` 只显示 Codex 的热力图。会话内的 `daily` / `weekly` 默认已自动跟随当前会话的 agent,显式 flag 会覆盖该行为。
177
+
174
178
  > 💡 `tt daily` 是 GitHub 风格的 token 贡献热力图(深浅绿方格)。在 Claude Code 会话里输入 `!tt daily` 即可看到彩色热力图 —— 用户主动用 `!` 执行的命令,Claude Code 会渲染其 24-bit 真彩色输出。
175
179
 
176
180
  ## 配色主题
@@ -204,9 +208,10 @@ tt monthly --theme nord # 任意报表临时换主题渲染(不持久化、
204
208
 
205
209
  1. **选语言** — 中文 / English(落 `~/.config/token-tracker/config.json`)
206
210
  2. **选配色主题** — 6 套主题上下键选择,每个选项右侧内联色板预览
207
- 3. **启用 Codex statusline** — Yes/No(仅检测到 Codex 时)
211
+ 3. **接管 Claude Code 状态栏** — Yes/No(仅检测到 Claude Code 时;已有自定义 statusLine 会先备份、选 No 完全不碰)
212
+ 4. **启用 Codex 伪 statusline** — Yes/No(仅检测到 Codex 时)
208
213
 
209
- CI / 非 tty 环境(Docker / 脚本 / `curl|bash`)自动按默认全装:**语言跟随系统设置**、主题 mocha、组件全开。装好后想改任何一项,再跑一次 `tt setup` 即可。
214
+ CI / 非 tty 环境(Docker / 脚本 / `curl|bash`)自动按**推荐默认**配置:语言跟随系统设置、主题 mocha、组件默认开启但**不替换已有自定义 statusLine**。装好后想改任何一项,再跑一次 `tt setup` 即可。
210
215
 
211
216
  ### 报告排序
212
217
 
@@ -30,6 +30,8 @@
30
30
 
31
31
  基于 Claude Code 官方自定义 StatusLine 接口,**数据完全来自本地 Claude,无任何推测**。
32
32
 
33
+ > 状态栏接管是**可选**的:已有自定义 statusLine 时默认保留你的配置(向导里也可随时选 No),报表命令完全不受影响。注意:不接管时 `tt status` 的 CC 订阅额度段将没有数据来源(CC 配额只经状态栏脚本落盘)。
34
+
33
35
  ![Claude Code StatusLine](assets/screenshot-statusline-cc.png)
34
36
 
35
37
  <details>
@@ -106,7 +108,7 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
106
108
  ## 使用
107
109
 
108
110
  ```bash
109
- tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty 环境自动全装
111
+ tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty 环境按推荐默认自动配置
110
112
  tt # 过去一年 token 热力图 + 顶部三段概览(= tt daily)
111
113
  tt daily # 同上(tt 无参即进 daily)
112
114
  tt status # 过去 5h 实时面板
@@ -118,6 +120,8 @@ tt unsetup # 卸载并恢复安装前的配置
118
120
  tt --version # 查看版本(-v / -V 同义)
119
121
  ```
120
122
 
123
+ > 多 agent 环境下想只看某一个 agent 的报表,加 `--claude` 或 `--codex` 即可(互斥),对 `status` / `daily` / `weekly` / `monthly` / `sessions` 均生效。例如 `tt daily --codex` 只显示 Codex 的热力图。会话内的 `daily` / `weekly` 默认已自动跟随当前会话的 agent,显式 flag 会覆盖该行为。
124
+
121
125
  > 💡 `tt daily` 是 GitHub 风格的 token 贡献热力图(深浅绿方格)。在 Claude Code 会话里输入 `!tt daily` 即可看到彩色热力图 —— 用户主动用 `!` 执行的命令,Claude Code 会渲染其 24-bit 真彩色输出。
122
126
 
123
127
  ## 配色主题
@@ -151,9 +155,10 @@ tt monthly --theme nord # 任意报表临时换主题渲染(不持久化、
151
155
 
152
156
  1. **选语言** — 中文 / English(落 `~/.config/token-tracker/config.json`)
153
157
  2. **选配色主题** — 6 套主题上下键选择,每个选项右侧内联色板预览
154
- 3. **启用 Codex statusline** — Yes/No(仅检测到 Codex 时)
158
+ 3. **接管 Claude Code 状态栏** — Yes/No(仅检测到 Claude Code 时;已有自定义 statusLine 会先备份、选 No 完全不碰)
159
+ 4. **启用 Codex 伪 statusline** — Yes/No(仅检测到 Codex 时)
155
160
 
156
- CI / 非 tty 环境(Docker / 脚本 / `curl|bash`)自动按默认全装:**语言跟随系统设置**、主题 mocha、组件全开。装好后想改任何一项,再跑一次 `tt setup` 即可。
161
+ CI / 非 tty 环境(Docker / 脚本 / `curl|bash`)自动按**推荐默认**配置:语言跟随系统设置、主题 mocha、组件默认开启但**不替换已有自定义 statusLine**。装好后想改任何一项,再跑一次 `tt setup` 即可。
157
162
 
158
163
  ### 报告排序
159
164
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "token-tracker"
7
- version = "0.4.6"
7
+ version = "0.4.7"
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,7 +26,8 @@ classifiers = [
26
26
  ]
27
27
  dependencies = [
28
28
  "rich>=13.7",
29
- "questionary>=2.0",
29
+ # wizard 补丁挂在 questionary 私有内部(InquirerControl._get_choice_tokens 等),锁大版本防破坏
30
+ "questionary>=2.0,<3",
30
31
  ]
31
32
 
32
33
  [project.optional-dependencies]
@@ -58,6 +59,12 @@ target-version = "py311"
58
59
  # E501(行长)故意不开:状态栏 ANSI 色码串本就长,启用只产生噪音
59
60
  select = ["E4", "E7", "E9", "F", "W", "I", "UP", "B"]
60
61
 
62
+ [tool.ruff.lint.per-file-ignores]
63
+ # 状态栏脚本模板:__STATUSLINE_*__ 占位符是烘焙时注入的(F821 未定义名是模板本性);
64
+ # E701/E401/I001/UP015/UP017 是脚本既有风格(紧凑 import、timezone.utc 兼容手改老 python)——
65
+ # 豁免以保持渲染产物与历史版本逐字节一致(模板内容 = 落盘脚本内容)
66
+ "src/token_tracker/templates/*" = ["F821", "E701", "E401", "I001", "UP015", "UP017"]
67
+
61
68
  [tool.pytest.ini_options]
62
69
  testpaths = ["tests"]
63
70
  addopts = "-q"
@@ -67,3 +74,8 @@ python_version = "3.11"
67
74
  ignore_missing_imports = true
68
75
  # 宽松起步,后续逐步收紧
69
76
  check_untyped_defs = true
77
+
78
+ [[tool.mypy.overrides]]
79
+ # 状态栏脚本模板含烘焙占位符(__STATUSLINE_*__),不是可独立类型检查的模块
80
+ module = "token_tracker.templates.*"
81
+ ignore_errors = true
@@ -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 累加用)。"""
@@ -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 / SessionBlock 上。"""
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
- monday = e.timestamp.date() - timedelta(days=e.timestamp.weekday())
71
- return monday.isoformat()
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 = e.timestamp.date() - timedelta(days=e.timestamp.weekday())
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-4-6"),
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
 
@@ -243,6 +257,14 @@ def _fallback_pricing() -> dict:
243
257
  "claude-opus-4-7": _OPUS_PRICING,
244
258
  "claude-opus-4-6": _OPUS_PRICING,
245
259
  "claude-opus-4-5": _OPUS_PRICING,
260
+ # Sonnet 5 当前为导入价($2 / $10 / $2.50 cache-write / $0.20 cache-read),截止 2026-08-31;
261
+ # 9-1 起标准价 $3 / $15(与 Sonnet 4.6 一致)。到期后需切价(注意:litellm 若已收录、以在线价为准)。
262
+ "claude-sonnet-5": {
263
+ "input_cost_per_token": 2e-6,
264
+ "output_cost_per_token": 10e-6,
265
+ "cache_creation_input_token_cost": 2.5e-6,
266
+ "cache_read_input_token_cost": 0.2e-6,
267
+ },
246
268
  "claude-sonnet-4-6": {
247
269
  "input_cost_per_token": 3e-6,
248
270
  "output_cost_per_token": 15e-6,
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import sys
3
+ from collections.abc import Callable
3
4
  from datetime import datetime
4
5
 
5
6
  from rich.text import Text
@@ -19,9 +20,9 @@ from .analyzer.aggregator import (
19
20
  from .analyzer.cost import calculate_cost
20
21
  from .hooks import is_setup, needs_update, setup, unsetup, update_hook
21
22
  from .i18n import t
23
+ from .tz import system_tz
22
24
  from .ui import theme, themes
23
25
  from .ui.console import forced_color_console, get_console
24
- from .ui.format import system_tz
25
26
  from .ui.heatmap import render_daily_heatmap
26
27
  from .ui.status import render_sessions_view, render_status
27
28
  from .ui.tables import (
@@ -47,13 +48,13 @@ VALID_SORT_KEYS = (*SORT_ATTRS.keys(), "time")
47
48
 
48
49
  # 数据报表命令分发表:命令 → (聚合函数, 渲染函数, time 排序的属性, 无 --sort 时的默认属性, 默认降序)
49
50
  # time_attr 与 no_sort_attr 仅 daily 不同(默认按 token 排,--sort time 才按日期)
50
- _REPORT_COMMANDS = {
51
+ _REPORT_COMMANDS: dict[str, tuple[Callable, Callable | None, str, str, bool]] = {
51
52
  "daily": (aggregate_daily, render_daily_heatmap, "date", "total_tokens", True),
52
53
  "weekly": (aggregate_weekly, render_weekly, "week", "week", True),
53
54
  "monthly": (aggregate_monthly, render_monthly, "month", "month", False),
54
- # sessions 走专门分支调 render_sessions_view(顶部+底部仿 status、无额度段);render_fn 槽
55
- # render_monthly 占位(仅为保持表项 callable、不经通用 render_fn 调用);默认按 cost 倒序
56
- "sessions": (aggregate_sessions, render_monthly, "start_time", "cost_usd", True),
55
+ # sessions 走专门分支调 render_sessions_view(顶部+底部仿 status、无额度段),
56
+ # 不经通用 render_fn(None 占位);默认按 cost 倒序
57
+ "sessions": (aggregate_sessions, None, "start_time", "cost_usd", True),
57
58
  }
58
59
 
59
60
 
@@ -66,6 +67,24 @@ def _parse_limit(args: list[str], default: int) -> int:
66
67
  return default
67
68
 
68
69
 
70
+ def _extract_agent_arg(args: list[str]) -> tuple[list[str], str | None]:
71
+ """提取 `--claude` / `--codex`,返回 (剩余 args, agent_id | None)。互斥(同时给报错退出);
72
+ 未给返回 None(走默认合并 + 会话自动识别)。用于多 agent 环境按需只看一个 agent(issue #19)。"""
73
+ remaining: list[str] = []
74
+ agent_id: str | None = None
75
+ _FLAG_TO_ID = {"--claude": "claude-code", "--codex": "codex"}
76
+ for a in args:
77
+ target = _FLAG_TO_ID.get(a)
78
+ if target is None:
79
+ remaining.append(a)
80
+ continue
81
+ if agent_id is not None and agent_id != target:
82
+ get_console().print(f"[red]{t('agent_filter_conflict')}[/red]")
83
+ sys.exit(1)
84
+ agent_id = target
85
+ return remaining, agent_id
86
+
87
+
69
88
  def _extract_theme_arg(args: list[str]) -> tuple[list[str], str | None]:
70
89
  """提取 --theme NAME,返回 (剩余 args, theme_name);未给则 name=None。用于报表临时切主题、不落配置。"""
71
90
  remaining: list[str] = []
@@ -81,11 +100,12 @@ def _extract_theme_arg(args: list[str]) -> tuple[list[str], str | None]:
81
100
  return remaining, name
82
101
 
83
102
 
84
- def _parse_sort_args(args: list[str]) -> tuple[list[str], str | None, bool]:
85
- """Extract --sort KEY and --asc from args, return (remaining, sort_key, descending)."""
103
+ def _parse_sort_args(args: list[str]) -> tuple[list[str], str | None, bool | None]:
104
+ """Extract --sort KEY and --asc/--desc from args, return (remaining, sort_key, descending).
105
+ descending=None 表示用户没显式给方向(让 _apply_sort 落各命令默认方向)。"""
86
106
  remaining = []
87
107
  sort_key = None
88
- descending = True
108
+ descending: bool | None = None
89
109
  i = 0
90
110
  while i < len(args):
91
111
  if args[i] == "--sort" and i + 1 < len(args):
@@ -103,9 +123,13 @@ def _parse_sort_args(args: list[str]) -> tuple[list[str], str | None, bool]:
103
123
  return remaining, sort_key, descending
104
124
 
105
125
 
106
- def _apply_sort(stats, sort_key: str | None, descending: bool, default_attr: str, default_reverse: bool):
126
+ def _apply_sort(stats, sort_key: str | None, descending: bool | None,
127
+ default_attr: str, default_reverse: bool):
128
+ # 用户显式给了 --asc/--desc 就尊重(即便没配 --sort,如 `tt sessions --asc`);
129
+ # 没给方向时:带 --sort 默认降序、无 --sort 落各命令默认方向
107
130
  if sort_key is None:
108
- stats.sort(key=lambda s: getattr(s, default_attr), reverse=default_reverse)
131
+ reverse = default_reverse if descending is None else descending
132
+ stats.sort(key=lambda s: getattr(s, default_attr), reverse=reverse)
109
133
  return
110
134
  if sort_key not in VALID_SORT_KEYS:
111
135
  valid = ", ".join(VALID_SORT_KEYS)
@@ -114,7 +138,7 @@ def _apply_sort(stats, sort_key: str | None, descending: bool, default_attr: str
114
138
  return
115
139
  # "time" 不在 SORT_ATTRS → 退回 default_attr(各命令的时间字段)
116
140
  attr = SORT_ATTRS.get(sort_key, default_attr)
117
- stats.sort(key=lambda s: getattr(s, attr), reverse=descending)
141
+ stats.sort(key=lambda s: getattr(s, attr), reverse=True if descending is None else descending)
118
142
 
119
143
 
120
144
  def _load_entries(agent_id: str, hours_back: int = 0):
@@ -122,10 +146,15 @@ def _load_entries(agent_id: str, hours_back: int = 0):
122
146
  return loader.load_entries(hours_back=hours_back) if loader else []
123
147
 
124
148
 
125
- def _aggregate_per_agent(agents, agg_fn):
149
+ def _load_per_agent(agents) -> list[tuple]:
150
+ """(agent, 全量 entries) 列表:每个 agent 只从磁盘解析一次 JSONL(全量扫描很重),
151
+ daily/weekly/monthly 多种聚合复用同一份——避免 tt daily 这类命令重复扫 2~3 遍。"""
152
+ return [(a, _load_entries(a.id)) for a in agents]
153
+
154
+
155
+ def _aggregate_per_agent(loaded, agg_fn):
126
156
  stats = []
127
- for a in agents:
128
- entries = _load_entries(a.id)
157
+ for a, entries in loaded:
129
158
  for s in agg_fn(entries):
130
159
  s.agent_id = a.id
131
160
  stats.append(s)
@@ -188,10 +217,12 @@ def _summary_from_sessions(sessions) -> StatusSummary:
188
217
 
189
218
 
190
219
  def _current_session_agent() -> str | None:
191
- """识别当前所在的 agent 会话(靠环境变量):Codex / Claude Code;独立终端返回 None。"""
220
+ """识别当前所在的 agent 会话(靠**会话内才有**的环境变量):Codex / Claude Code;独立终端返回 None。
221
+ 不能用 CLAUDE_CONFIG_DIR 判断——那是用户级配置变量(可长期 export 在 shell profile 里挪配置目录,
222
+ tt 自己也支持它),拿它当会话信号会让独立终端被误判成会话内(报表被过滤、首次运行进不了 wizard)。"""
192
223
  if os.environ.get("CODEX_THREAD_ID") or os.environ.get("CODEX_SANDBOX"):
193
224
  return "codex"
194
- if os.environ.get("CLAUDE_CONFIG_DIR") or os.environ.get("CLAUDECODE"):
225
+ if os.environ.get("CLAUDECODE"):
195
226
  return "claude-code"
196
227
  return None
197
228
 
@@ -241,8 +272,9 @@ def _run_setup_flow() -> None:
241
272
 
242
273
 
243
274
  def _auto_setup() -> None:
244
- """非交互环境(非 tty / CI / 会话内):默认全装——语言跟随**系统设置**(绕过 CLI LANG)、
245
- 主题 mocha、组件全开。仅当用户从未配置过语言/主题时落默认(不覆盖已有选择)。
275
+ """非交互环境(非 tty / CI / 会话内):语言跟随**系统设置**(绕过 CLI LANG)、主题 mocha、
276
+ 组件按推荐默认(hooks.recommended_components:已有意图优先、不替换已有自定义 statusLine)。
277
+ 仅当用户从未配置过语言/主题时落默认(不覆盖已有选择)。
246
278
  agent 守卫在 _run_setup_flow 已做,这里假设至少有一个 agent。"""
247
279
  if config.resolve_lang() is None:
248
280
  sys_lang = i18n._detect_system_lang()
@@ -250,7 +282,7 @@ def _auto_setup() -> None:
250
282
  i18n.set_lang(sys_lang)
251
283
  if not config.load_config().get("theme"):
252
284
  config.save_theme("mocha")
253
- setup(auto=True) # 组件默认全开
285
+ setup(auto=True) # 组件走推荐默认(setup 内部 components=None → recommended_components)
254
286
  get_console().print(f"[dim]{t('auto_setup_hint')}[/dim]")
255
287
 
256
288
 
@@ -360,6 +392,8 @@ def main():
360
392
  _load_local_mock()
361
393
 
362
394
  args = sys.argv[1:]
395
+ # --claude / --codex:多 agent 环境按需只看一个 agent 的报表(issue #19),互斥;不给则走默认
396
+ args, filter_agent = _extract_agent_arg(args)
363
397
  # --theme NAME:临时覆盖主题(仅本次进程、不落配置/不重烘焙状态栏),对所有报表 + status 生效
364
398
  args, theme_override = _extract_theme_arg(args)
365
399
  if theme_override is not None:
@@ -411,6 +445,14 @@ def main():
411
445
  sys.exit(1)
412
446
 
413
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
414
456
  agent_ids = {a.id for a in agents}
415
457
 
416
458
  if command in ("status", "dashboard"):
@@ -429,41 +471,44 @@ def main():
429
471
  sys.exit(1)
430
472
 
431
473
  # daily / weekly 跟随当前会话:CC 会话只看 CC、Codex 会话只看 Codex;
432
- # 独立终端(识别不到会话)保持合并所有 agent
474
+ # 独立终端(识别不到会话)保持合并所有 agent。显式 --claude / --codex 已在上面收窄 agents、优先级最高。
433
475
  report_agents = agents
434
- if command in ("daily", "weekly"):
476
+ if filter_agent is None and command in ("daily", "weekly"):
435
477
  session_agent = _current_session_agent()
436
478
  if session_agent and session_agent in agent_ids:
437
479
  report_agents = [a for a in agents if a.id == session_agent]
438
480
  agent_names = [a.name for a in report_agents]
439
481
 
440
482
  agg_fn, render_fn, time_attr, no_sort_attr, default_reverse = _REPORT_COMMANDS[command]
441
- stats = _aggregate_per_agent(report_agents, agg_fn)
483
+ loaded = _load_per_agent(report_agents)
484
+ stats = _aggregate_per_agent(loaded, agg_fn)
442
485
  default_attr = time_attr if sort_key == "time" else no_sort_attr
443
486
 
444
487
  if command == "sessions":
445
- # sessions 看「最近的会话」:先过滤掉跨度 <5min 的碎片会话,再按时间取最近 N
446
- #(否则史上高 cost 会话恒久霸榜、新会话和低成本 agent 永远进不了榜),这 N 条再按 cost(或 --sort)展示
447
- kept = [s for s in stats if s.duration_minutes >= 5]
488
+ # sessions 看「最近的会话」:先过滤掉活跃 <5min 的碎片会话(与 tt status 同口径,
489
+ # types.SessionStats.active_minutes),再按时间取最近 N 条(否则史上高 cost 会话
490
+ # 恒久霸榜、新会话和低成本 agent 永远进不了榜),这 N 条再按 cost(或 --sort)展示
491
+ kept = [s for s in stats if s.active_minutes >= 5]
448
492
  kept.sort(key=lambda s: s.start_time, reverse=True)
449
493
  shown = kept[:_parse_limit(rest_args, default=20)]
450
494
  _apply_sort(shown, sort_key, sort_desc, default_attr, default_reverse)
451
495
  render_sessions_view(_summary_from_sessions(shown), shown, agent_names)
452
496
  return
453
497
 
498
+ assert render_fn is not None # sessions(render_fn=None)已在上面 return,其余命令都有渲染函数
454
499
  _apply_sort(stats, sort_key, sort_desc, default_attr, default_reverse)
455
500
  if command == "daily":
456
501
  # daily 顶部三卡片:Last 12 months + This Month + This Week,跟 weekly/monthly 同款样式、
457
- # 复用 _render_month_summary / _render_week_summary。多算两份 weekly/monthly 聚合传进去。
458
- render_fn(stats, agents=agent_names, # type: ignore[operator]
459
- weekly=_aggregate_per_agent(report_agents, aggregate_weekly),
460
- monthly=_aggregate_per_agent(report_agents, aggregate_monthly))
502
+ # 复用 _render_month_summary / _render_week_summaryweekly/monthly 聚合复用同一份 entries。
503
+ render_fn(stats, agents=agent_names,
504
+ weekly=_aggregate_per_agent(loaded, aggregate_weekly),
505
+ monthly=_aggregate_per_agent(loaded, aggregate_monthly))
461
506
  elif command == "weekly":
462
- render_weekly(stats, agents=agent_names, daily=_aggregate_per_agent(report_agents, aggregate_daily))
507
+ render_weekly(stats, agents=agent_names, daily=_aggregate_per_agent(loaded, aggregate_daily))
463
508
  elif command == "monthly":
464
509
  render_monthly(stats, agents=agent_names,
465
- daily=_aggregate_per_agent(report_agents, aggregate_daily),
466
- weekly=_aggregate_per_agent(report_agents, aggregate_weekly))
510
+ daily=_aggregate_per_agent(loaded, aggregate_daily),
511
+ weekly=_aggregate_per_agent(loaded, aggregate_weekly))
467
512
  else:
468
513
  render_fn(stats, agents=agent_names)
469
514
 
@@ -1,4 +1,5 @@
1
- """统一配置存储:~/.config/token-tracker/config.json(XDG,不绑 Claude Code)。
1
+ """统一配置存储:~/.config/token-tracker/config.json(固定路径、不读 XDG_CONFIG_HOME——
2
+ 跨平台行为一致 + statusline 模板内硬编码同路径;不绑 Claude Code)。
2
3
 
3
4
  包含所有 tt 自身偏好——主题、语言、组件意图。schema_version 用于未来格式迁移。
4
5
 
@@ -16,12 +17,17 @@ from .ui import themes
16
17
 
17
18
  CONFIG_DIR = os.path.expanduser("~/.config/token-tracker")
18
19
  CONFIG_PATH = os.path.join(CONFIG_DIR, "config.json")
20
+ # CC statusline 缓存的单一权威路径(statusline 脚本写、tt status / adapters.rate_limits 读)。
21
+ # 注意 templates/claude_statusline.py 因脚本独立运行硬编码了同一路径——改这里必须同步改模板。
22
+ STATUS_FILE = os.path.join(CONFIG_DIR, "tt-status.json")
19
23
  SCHEMA_VERSION = 1
20
24
 
21
25
  # 引导版本:每次新增"值得让老用户重新走一遍 setup"的配置项时手动 +1(只能整数、一次 +1)。
22
26
  # 老用户 config 里没这字段 / 旧版本号 < 当前 → 触发重新引导(真终端弹 wizard、非 tty 静默 _auto_setup)。
23
27
  # 跟 SCHEMA_VERSION 解耦:那是数据格式版本,这是用户引导版本,bump 节奏完全不同。
24
28
  # 2(0.4.2):强制所有现存用户(0.3.8/0.4.0=无字段=0、0.4.1=1,全 < 2)升级后重走一遍 setup。
29
+ # 注:CC statusLine 变可选组件(issue #16/#17)时决定**不 bump**——小众需求不打断存量用户,
30
+ # 存量 tt 用户 intent 缺失时由 is_setup 按「statusLine 已是 tt 的」推断为已配,想改的手动 tt setup。
25
31
  SETUP_VERSION = 2
26
32
 
27
33
  # 旧位置(独立 theme.json / lang.json),老用户首次读 config.json 不存在时自动合并迁移
@@ -157,6 +163,17 @@ def codex_faux_statusline_intent() -> bool | None:
157
163
  return val if isinstance(val, bool) else None
158
164
 
159
165
 
166
+ def save_cc_statusline(enabled: bool) -> None:
167
+ """wizard 选完后写入意图(CC statusLine 是否由 tt 接管)。"""
168
+ _save_field("cc_statusline", bool(enabled))
169
+
170
+
171
+ def cc_statusline_intent() -> bool | None:
172
+ """读用户对 CC statusLine 接管的意图。严格 bool;非 bool / 缺字段 → None(视为没表达)。"""
173
+ val = load_config().get("cc_statusline")
174
+ return val if isinstance(val, bool) else None
175
+
176
+
160
177
  # --- setup 引导版本(老用户升级后重新引导一次的判定依据) ---
161
178
 
162
179