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.
- {token_tracker-0.4.6/src/token_tracker.egg-info → token_tracker-0.4.7}/PKG-INFO +10 -5
- {token_tracker-0.4.6 → token_tracker-0.4.7}/README.md +8 -3
- {token_tracker-0.4.6 → token_tracker-0.4.7}/pyproject.toml +14 -2
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/rate_limits.py +1 -3
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/types.py +0 -23
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/analyzer/aggregator.py +20 -7
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/analyzer/cost.py +23 -1
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/cli.py +77 -32
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/config.py +18 -1
- token_tracker-0.4.7/src/token_tracker/hooks.py +634 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/i18n.py +20 -98
- token_tracker-0.4.7/src/token_tracker/templates/claude_statusline.py +370 -0
- token_tracker-0.4.7/src/token_tracker/templates/codex_statusline.py +233 -0
- token_tracker-0.4.7/src/token_tracker/tz.py +22 -0
- token_tracker-0.4.7/src/token_tracker/ui/__init__.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/format.py +1 -23
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/heatmap.py +8 -6
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/status.py +11 -3
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/tables.py +10 -73
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/theme.py +1 -9
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/wizard.py +21 -10
- {token_tracker-0.4.6 → token_tracker-0.4.7/src/token_tracker.egg-info}/PKG-INFO +10 -5
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker.egg-info/SOURCES.txt +4 -3
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker.egg-info/requires.txt +1 -1
- {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_aggregator.py +41 -2
- token_tracker-0.4.7/tests/test_cli.py +132 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_cost.py +25 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_hooks.py +342 -7
- {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_status.py +31 -5
- 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/tests/test_blocks.py +0 -79
- token_tracker-0.4.6/tests/test_cli.py +0 -38
- {token_tracker-0.4.6 → token_tracker-0.4.7}/LICENSE +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/setup.cfg +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/__init__.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/__init__.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/claude.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/codex.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/registry.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/adapters/util.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/analyzer/__init__.py +0 -0
- {token_tracker-0.4.6/src/token_tracker/ui → token_tracker-0.4.7/src/token_tracker/templates}/__init__.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/console.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker/ui/themes.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker.egg-info/dependency_links.txt +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker.egg-info/entry_points.txt +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/src/token_tracker.egg-info/top_level.txt +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_codex.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_heatmap.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_tables.py +0 -0
- {token_tracker-0.4.6 → token_tracker-0.4.7}/tests/test_theme.py +0 -0
- {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.
|
|
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
|
|
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
|

|
|
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.
|
|
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
|
|
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
|

|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 /
|
|
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
|
|
|
@@ -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
|
|
55
|
-
#
|
|
56
|
-
"sessions": (aggregate_sessions,
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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 /
|
|
245
|
-
|
|
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
|
-
|
|
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
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
458
|
-
render_fn(stats, agents=agent_names,
|
|
459
|
-
weekly=_aggregate_per_agent(
|
|
460
|
-
monthly=_aggregate_per_agent(
|
|
502
|
+
# 复用 _render_month_summary / _render_week_summary。weekly/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(
|
|
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(
|
|
466
|
-
weekly=_aggregate_per_agent(
|
|
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
|
|
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
|
|