token-tracker 0.4.4__tar.gz → 0.4.6__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.4/src/token_tracker.egg-info → token_tracker-0.4.6}/PKG-INFO +8 -1
- {token_tracker-0.4.4 → token_tracker-0.4.6}/README.md +7 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/pyproject.toml +1 -1
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/hooks.py +118 -30
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/ui/themes.py +4 -2
- {token_tracker-0.4.4 → token_tracker-0.4.6/src/token_tracker.egg-info}/PKG-INFO +8 -1
- {token_tracker-0.4.4 → token_tracker-0.4.6}/tests/test_hooks.py +98 -2
- {token_tracker-0.4.4 → token_tracker-0.4.6}/LICENSE +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/setup.cfg +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/__init__.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/adapters/__init__.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/adapters/claude.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/adapters/codex.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/adapters/rate_limits.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/adapters/registry.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/adapters/types.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/adapters/util.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/analyzer/__init__.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/analyzer/aggregator.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/analyzer/blocks.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/analyzer/cost.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/cli.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/config.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/i18n.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/ui/__init__.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/ui/console.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/ui/format.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/ui/heatmap.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/ui/panels.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/ui/status.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/ui/tables.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/ui/theme.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker/wizard.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker.egg-info/SOURCES.txt +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker.egg-info/dependency_links.txt +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker.egg-info/entry_points.txt +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker.egg-info/requires.txt +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/src/token_tracker.egg-info/top_level.txt +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/tests/test_aggregator.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/tests/test_blocks.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/tests/test_cli.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/tests/test_codex.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/tests/test_cost.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/tests/test_heatmap.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/tests/test_status.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/tests/test_tables.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/tests/test_theme.py +0 -0
- {token_tracker-0.4.4 → token_tracker-0.4.6}/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.6
|
|
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
|
|
@@ -149,6 +149,13 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
|
|
|
149
149
|
> **升级**:重跑上面的命令即可(脚本幂等、自动升到最新)。
|
|
150
150
|
> **卸载**:`tt unsetup`
|
|
151
151
|
|
|
152
|
+
**升级后 `tt --version` 还是旧版?** 多半是旧版装在别的 Python 环境里遮蔽了新版(常见于 Windows、或早期用 `pip install` 装过)。卸载旧版后重装一次即可:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip uninstall token-tracker
|
|
156
|
+
curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/install.sh | bash
|
|
157
|
+
```
|
|
158
|
+
|
|
152
159
|
## 使用
|
|
153
160
|
|
|
154
161
|
```bash
|
|
@@ -96,6 +96,13 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
|
|
|
96
96
|
> **升级**:重跑上面的命令即可(脚本幂等、自动升到最新)。
|
|
97
97
|
> **卸载**:`tt unsetup`
|
|
98
98
|
|
|
99
|
+
**升级后 `tt --version` 还是旧版?** 多半是旧版装在别的 Python 环境里遮蔽了新版(常见于 Windows、或早期用 `pip install` 装过)。卸载旧版后重装一次即可:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip uninstall token-tracker
|
|
103
|
+
curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/install.sh | bash
|
|
104
|
+
```
|
|
105
|
+
|
|
99
106
|
## 使用
|
|
100
107
|
|
|
101
108
|
```bash
|
|
@@ -36,8 +36,8 @@ CODEX_DIR = _CODEX
|
|
|
36
36
|
CODEX_CONFIG = os.path.join(CODEX_DIR, "config.toml") # 改 Codex 配置,留 agent 目录
|
|
37
37
|
CODEX_STATUSLINE_HOOK_PATH = os.path.join(_TT, "codex-statusline.py")
|
|
38
38
|
STATUS_FILE = os.path.join(_TT, "tt-status.json") # CC statusline 缓存(脚本写、tt status 读)
|
|
39
|
-
HOOK_VERSION = "1.
|
|
40
|
-
STATUSLINE_HOOK_VERSION = "1.
|
|
39
|
+
HOOK_VERSION = "1.8"
|
|
40
|
+
STATUSLINE_HOOK_VERSION = "1.1"
|
|
41
41
|
|
|
42
42
|
CC_BACKUP_PATH = os.path.join(_TT, "cc-backup.json")
|
|
43
43
|
CODEX_BACKUP_LEGACY = os.path.join(_TT, "codex-backup.json") # 老用户残留,unsetup 时还能恢复
|
|
@@ -150,25 +150,35 @@ def git_branch(cwd):
|
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
def git_diff_stat(cwd):
|
|
153
|
-
"""相对 HEAD
|
|
153
|
+
"""相对 HEAD 的未提交增删行数 + 未跟踪文件数(已跟踪改动按行、未跟踪按文件计数)。
|
|
154
|
+
失败/无 commit 返回 (0, 0, 0)。"""
|
|
155
|
+
added = deleted = 0
|
|
154
156
|
try:
|
|
155
157
|
out = subprocess.check_output(
|
|
156
158
|
["git", "diff", "HEAD", "--numstat"], cwd=cwd,
|
|
157
159
|
stderr=subprocess.DEVNULL, text=True, timeout=2,
|
|
158
160
|
)
|
|
161
|
+
for line in out.splitlines():
|
|
162
|
+
parts = line.split("\t")
|
|
163
|
+
if len(parts) < 2:
|
|
164
|
+
continue
|
|
165
|
+
a, d = parts[0], parts[1]
|
|
166
|
+
if a.isdigit():
|
|
167
|
+
added += int(a)
|
|
168
|
+
if d.isdigit():
|
|
169
|
+
deleted += int(d)
|
|
159
170
|
except Exception:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return added, deleted
|
|
171
|
+
pass
|
|
172
|
+
untracked = 0
|
|
173
|
+
try:
|
|
174
|
+
out = subprocess.check_output(
|
|
175
|
+
["git", "ls-files", "--others", "--exclude-standard"], cwd=cwd,
|
|
176
|
+
stderr=subprocess.DEVNULL, text=True, timeout=2,
|
|
177
|
+
)
|
|
178
|
+
untracked = sum(1 for ln in out.splitlines() if ln.strip())
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
return added, deleted, untracked
|
|
172
182
|
|
|
173
183
|
|
|
174
184
|
def save_data(data, now):
|
|
@@ -267,11 +277,13 @@ def render(data, now, tps=None):
|
|
|
267
277
|
branch = git_branch(project)
|
|
268
278
|
if branch:
|
|
269
279
|
inner = f"{C['branch']}{branch}{C['reset']}"
|
|
270
|
-
added, deleted = git_diff_stat(project)
|
|
280
|
+
added, deleted, untracked = git_diff_stat(project)
|
|
271
281
|
if added:
|
|
272
282
|
inner += f" {C['added']}+{added}{C['reset']}"
|
|
273
283
|
if deleted:
|
|
274
284
|
inner += f" {C['deleted']}-{deleted}{C['reset']}"
|
|
285
|
+
if untracked:
|
|
286
|
+
inner += f" {C['untracked']}?{untracked}{C['reset']}"
|
|
275
287
|
line1.append(f"\033[1m{C['project']}[{name}]{C['reset']}({inner})")
|
|
276
288
|
else:
|
|
277
289
|
line1.append(f"\033[1m{C['project']}[{name}]{C['reset']}")
|
|
@@ -528,9 +540,10 @@ def _ctx_pct(info):
|
|
|
528
540
|
|
|
529
541
|
|
|
530
542
|
def _git_status(cwd):
|
|
531
|
-
"""(branch, +A, -D
|
|
543
|
+
"""(branch, +A, -D, ?U);已跟踪改动按行、未跟踪按文件计数。
|
|
544
|
+
失败/非 git/无 commit 返回 ("", 0, 0, 0)。"""
|
|
532
545
|
if not cwd:
|
|
533
|
-
return "", 0, 0
|
|
546
|
+
return "", 0, 0, 0
|
|
534
547
|
|
|
535
548
|
def run(args):
|
|
536
549
|
return subprocess.check_output(
|
|
@@ -540,9 +553,9 @@ def _git_status(cwd):
|
|
|
540
553
|
try:
|
|
541
554
|
branch = run(["branch", "--show-current"])
|
|
542
555
|
except Exception:
|
|
543
|
-
return "", 0, 0
|
|
556
|
+
return "", 0, 0, 0
|
|
544
557
|
if not branch:
|
|
545
|
-
return "", 0, 0
|
|
558
|
+
return "", 0, 0, 0
|
|
546
559
|
try:
|
|
547
560
|
if run(["status", "--porcelain", "--untracked-files=no"]):
|
|
548
561
|
branch += "*"
|
|
@@ -559,14 +572,19 @@ def _git_status(cwd):
|
|
|
559
572
|
d += int(parts[1])
|
|
560
573
|
except Exception:
|
|
561
574
|
pass
|
|
562
|
-
|
|
575
|
+
u = 0
|
|
576
|
+
try:
|
|
577
|
+
u = sum(1 for ln in run(["ls-files", "--others", "--exclude-standard"]).splitlines() if ln.strip())
|
|
578
|
+
except Exception:
|
|
579
|
+
pass
|
|
580
|
+
return branch, a, d, u
|
|
563
581
|
|
|
564
582
|
|
|
565
583
|
def _render_project(cwd):
|
|
566
584
|
if not cwd:
|
|
567
585
|
return ""
|
|
568
586
|
name = os.path.basename(cwd.rstrip("/"))
|
|
569
|
-
branch, a, d = _git_status(cwd)
|
|
587
|
+
branch, a, d, u = _git_status(cwd)
|
|
570
588
|
if not branch:
|
|
571
589
|
return f"{BOLD}{C['project']}[{name}]{RST}"
|
|
572
590
|
inner = f"{C['branch']}{branch}{RST}"
|
|
@@ -574,6 +592,8 @@ def _render_project(cwd):
|
|
|
574
592
|
inner += f" {C['added']}+{a}{RST}"
|
|
575
593
|
if d:
|
|
576
594
|
inner += f" {C['deleted']}-{d}{RST}"
|
|
595
|
+
if u:
|
|
596
|
+
inner += f" {C['untracked']}?{u}{RST}"
|
|
577
597
|
return f"{BOLD}{C['project']}[{name}]{RST}({inner})"
|
|
578
598
|
|
|
579
599
|
|
|
@@ -614,7 +634,8 @@ def main():
|
|
|
614
634
|
line1.append(f"{C['tokens']}Total: {fmt_tokens(total)}{RST}") # 整体取 tokens 槽(mocha=peach/橙)
|
|
615
635
|
model = model or payload.get("model") or "" # session turn_context 的 model(gpt-5.5)优先
|
|
616
636
|
if model:
|
|
617
|
-
|
|
637
|
+
# effort 缺失时显示 default(Codex 默认 reasoning level),与 TUI 的 Current reasoning level 对齐
|
|
638
|
+
label = f"{model} {effort or 'default'}"
|
|
618
639
|
line1.append(f"{C['total']}Model: {label}{RST}") # 整体取 total 槽(mocha=red/红)
|
|
619
640
|
|
|
620
641
|
# L2: Limit: 5h | 7d | <window> Ctx(仿 CC statusline,带进度条 + reset)
|
|
@@ -702,13 +723,21 @@ def _has_tt_codex_statusline(content: str) -> bool:
|
|
|
702
723
|
|
|
703
724
|
def _install_codex_statusline(content: str, python: str) -> str:
|
|
704
725
|
"""落盘 Codex statusline 脚本 + 在 config.toml 末尾追加 Stop hook 段。
|
|
705
|
-
|
|
726
|
+
- 新名 codex-statusline 段已存在 + python 路径一致 → 幂等返回;
|
|
727
|
+
- 已存在但 python 路径不一致(用户升级 Python / 切换 conda/venv / 卸载某环境后跑 tt setup)→ 删旧装新,
|
|
728
|
+
避免 command 指向已死 python(症状:脚本 import token_tracker 失败被 try/except 吞掉,状态栏只剩项目名);
|
|
729
|
+
- 只存在旧名 tt-statusline → 清掉后追加新路径段(老用户迁移)。"""
|
|
706
730
|
_write_codex_statusline_script()
|
|
707
|
-
if "codex-statusline" in content:
|
|
708
|
-
return content
|
|
709
|
-
if "tt-statusline" in content: # 旧路径残留 → 清掉,下面追加新路径段
|
|
710
|
-
content = _CODEX_STATUSLINE_REGEX.sub("\n", content)
|
|
711
731
|
cmd = f"{python} {CODEX_STATUSLINE_HOOK_PATH}"
|
|
732
|
+
if "codex-statusline" in content or "tt-statusline" in content:
|
|
733
|
+
# 从已有的 command 行提取 python 路径(兼容 basic / literal string 包裹)
|
|
734
|
+
m = re.search(
|
|
735
|
+
r"command = [\"']([^ ]+) [^\"']*(?:codex-statusline|tt-statusline)[^\"']*[\"']",
|
|
736
|
+
content,
|
|
737
|
+
)
|
|
738
|
+
if m and m.group(1) == python and "codex-statusline" in content:
|
|
739
|
+
return content # python 一致 + 已是新名 → 幂等
|
|
740
|
+
content = _CODEX_STATUSLINE_REGEX.sub("\n", content) # 路径不一致或老名残留 → 删旧段
|
|
712
741
|
return content.rstrip() + (
|
|
713
742
|
"\n\n[[hooks.Stop]]\n\n"
|
|
714
743
|
"[[hooks.Stop.hooks]]\n"
|
|
@@ -791,6 +820,30 @@ def _is_tt_cc_command(cmd: str) -> bool:
|
|
|
791
820
|
return "claude-statusline" in cmd or "tt-statusline" in cmd
|
|
792
821
|
|
|
793
822
|
|
|
823
|
+
def _build_cc_command(python: str, script: str) -> str:
|
|
824
|
+
"""拼 statusLine command 字符串。
|
|
825
|
+
Windows: 反斜杠转正斜杠(CC 在 Windows 走 Git Bash/sh 执行 command,反斜杠被吞致 exit 127);
|
|
826
|
+
所有平台: 两段路径都加双引号包裹(防路径含空格断词)。
|
|
827
|
+
issue #13 / #14 根治:旧格式 `f"{python} {script}"` 在 Windows 静默失败、状态栏空白。"""
|
|
828
|
+
if os.name == "nt":
|
|
829
|
+
python = python.replace("\\", "/")
|
|
830
|
+
script = script.replace("\\", "/")
|
|
831
|
+
return f'"{python}" "{script}"'
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _cc_command_outdated(cmd: str) -> bool:
|
|
835
|
+
"""settings.json 里 tt 的 statusLine.command 是否还是旧格式(裸拼接 / 含反斜杠)。
|
|
836
|
+
新格式:两段路径都用 `"` 包裹 + Windows 上路径必须正斜杠。
|
|
837
|
+
仅对 tt 的 command 生效(_is_tt_cc_command 已先过滤),用户原 command 不动。"""
|
|
838
|
+
if not cmd:
|
|
839
|
+
return False
|
|
840
|
+
if not cmd.startswith('"'):
|
|
841
|
+
return True # 没引号 = 旧裸拼接
|
|
842
|
+
if os.name == "nt" and "\\" in cmd:
|
|
843
|
+
return True # Windows 上还含反斜杠 = 没转过来
|
|
844
|
+
return False
|
|
845
|
+
|
|
846
|
+
|
|
794
847
|
def _write_cc_statusline_script() -> None:
|
|
795
848
|
"""渲染并落盘 CC statusline 脚本(mkdir + 执行权限)。"""
|
|
796
849
|
os.makedirs(_TT, exist_ok=True)
|
|
@@ -811,12 +864,45 @@ def _migrate_legacy() -> None:
|
|
|
811
864
|
pass
|
|
812
865
|
|
|
813
866
|
|
|
867
|
+
def _cc_command_needs_sync() -> bool:
|
|
868
|
+
"""检测 settings.json 里 tt 的 statusLine.command 是否需要重写为新格式(issue #13/#14)。
|
|
869
|
+
用户原 command(非 tt)一律不动。"""
|
|
870
|
+
if not os.path.exists(CLAUDE_SETTINGS):
|
|
871
|
+
return False
|
|
872
|
+
try:
|
|
873
|
+
with open(CLAUDE_SETTINGS, encoding="utf-8") as f:
|
|
874
|
+
settings = json.load(f)
|
|
875
|
+
except (OSError, json.JSONDecodeError):
|
|
876
|
+
return False
|
|
877
|
+
cmd = (settings.get("statusLine") or {}).get("command") or ""
|
|
878
|
+
if not _is_tt_cc_command(cmd):
|
|
879
|
+
return False
|
|
880
|
+
return _cc_command_outdated(cmd)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _sync_cc_command() -> None:
|
|
884
|
+
"""重写 settings.json 里 tt 的 statusLine.command 字段(保留其它字段不动)。"""
|
|
885
|
+
try:
|
|
886
|
+
with open(CLAUDE_SETTINGS, encoding="utf-8") as f:
|
|
887
|
+
settings = json.load(f)
|
|
888
|
+
except (OSError, json.JSONDecodeError):
|
|
889
|
+
return
|
|
890
|
+
if not _is_tt_cc_command((settings.get("statusLine") or {}).get("command") or ""):
|
|
891
|
+
return
|
|
892
|
+
python = sys.executable or "python3"
|
|
893
|
+
settings["statusLine"] = {"type": "command", "command": _build_cc_command(python, HOOK_SCRIPT_PATH)}
|
|
894
|
+
with open(CLAUDE_SETTINGS, "w", encoding="utf-8") as f:
|
|
895
|
+
json.dump(settings, f, indent=2, ensure_ascii=False)
|
|
896
|
+
|
|
897
|
+
|
|
814
898
|
def needs_update() -> bool:
|
|
815
899
|
# 只在已安装(新位置脚本文件存在)时纳入版本判断,未装不主动装
|
|
816
900
|
if os.path.exists(HOOK_SCRIPT_PATH) and _installed_hook_version() != HOOK_VERSION:
|
|
817
901
|
return True
|
|
818
902
|
sv = _installed_codex_statusline_version()
|
|
819
|
-
|
|
903
|
+
if sv is not None and sv != STATUSLINE_HOOK_VERSION:
|
|
904
|
+
return True
|
|
905
|
+
return _cc_command_needs_sync() # settings.json 里 command 格式过时也算待更新(issue #13/#14)
|
|
820
906
|
|
|
821
907
|
|
|
822
908
|
def update_hook() -> None:
|
|
@@ -824,6 +910,8 @@ def update_hook() -> None:
|
|
|
824
910
|
_write_cc_statusline_script()
|
|
825
911
|
if _installed_codex_statusline_version() is not None:
|
|
826
912
|
_write_codex_statusline_script()
|
|
913
|
+
if _cc_command_needs_sync():
|
|
914
|
+
_sync_cc_command()
|
|
827
915
|
|
|
828
916
|
|
|
829
917
|
# --- setup ---
|
|
@@ -895,7 +983,7 @@ def _setup_claude(quiet: bool = False) -> None:
|
|
|
895
983
|
json.dump({"statusLine": existing}, f, indent=2)
|
|
896
984
|
|
|
897
985
|
python = sys.executable or "python3"
|
|
898
|
-
settings["statusLine"] = {"type": "command", "command":
|
|
986
|
+
settings["statusLine"] = {"type": "command", "command": _build_cc_command(python, HOOK_SCRIPT_PATH)}
|
|
899
987
|
|
|
900
988
|
with open(CLAUDE_SETTINGS, "w", encoding="utf-8") as f:
|
|
901
989
|
json.dump(settings, f, indent=2, ensure_ascii=False)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
每个主题只定义 **9 个基色**(沿用 Catppuccin 的槽位命名)+ 是否浅色 + 5 档热力渐变;
|
|
4
4
|
- 17 个 CLI 语义槽由 `derive_slots()` 统一派生(`token=sapphire`、`cost=yellow`…);
|
|
5
|
-
- statusline 的
|
|
5
|
+
- statusline 的 13 个 key 由 `_STATUSLINE_SLOTS` 映射到这 9 槽的子集。
|
|
6
6
|
|
|
7
7
|
颜色取值:Catppuccin(mocha/latte/frappe/macchiato)用官方 hex;Nord / Dracula 按色相
|
|
8
8
|
就近映射到这 9 槽。truecolor 终端用精确 hex,不支持 truecolor 的降级到 **256 色近似**
|
|
@@ -77,15 +77,17 @@ THEMES: dict[str, dict] = {
|
|
|
77
77
|
# 展示顺序(tt theme list / 向导)。
|
|
78
78
|
THEME_NAMES = tuple(THEMES)
|
|
79
79
|
|
|
80
|
-
# statusline
|
|
80
|
+
# statusline 13 个着色 key → 9 基色槽(reset 固定为 \033[0m,单列)。
|
|
81
81
|
# 这套角色映射沿用旧 mocha 状态栏观感(分支红/玫红、标签粉、Tokens 桃、Model/Duration 蓝),
|
|
82
82
|
# 与 CLI 报表的 _S 语义槽**不完全同源**(CLI token=sapphire 青)——按主人审美选择保留状态栏旧手感。
|
|
83
83
|
# total=red 红:第 1 行消耗/产出指标(Total/Cost/Code 标签)用它。
|
|
84
|
+
# untracked=mauve 紫:git 段未跟踪文件数 ?N,独占一色与 +A(绿)/-D(红) 区分。
|
|
84
85
|
_STATUSLINE_SLOTS = {
|
|
85
86
|
"project": "green",
|
|
86
87
|
"branch": "red",
|
|
87
88
|
"added": "green",
|
|
88
89
|
"deleted": "red",
|
|
90
|
+
"untracked": "mauve",
|
|
89
91
|
"label": "pink",
|
|
90
92
|
"bar_ok": "green",
|
|
91
93
|
"bar_warn": "yellow",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: token-tracker
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
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
|
|
@@ -149,6 +149,13 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/instal
|
|
|
149
149
|
> **升级**:重跑上面的命令即可(脚本幂等、自动升到最新)。
|
|
150
150
|
> **卸载**:`tt unsetup`
|
|
151
151
|
|
|
152
|
+
**升级后 `tt --version` 还是旧版?** 多半是旧版装在别的 Python 环境里遮蔽了新版(常见于 Windows、或早期用 `pip install` 装过)。卸载旧版后重装一次即可:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip uninstall token-tracker
|
|
156
|
+
curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/install.sh | bash
|
|
157
|
+
```
|
|
158
|
+
|
|
152
159
|
## 使用
|
|
153
160
|
|
|
154
161
|
```bash
|
|
@@ -80,12 +80,28 @@ def test_codex_statusline_install_uninstall_roundtrip(tmp_path, monkeypatch):
|
|
|
80
80
|
assert "tt-statusline" not in removed and 'command = "mine"' in removed
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def test_codex_statusline_install_updates_stale_python(tmp_path, monkeypatch):
|
|
84
|
+
# 回归:用户升级 Python / 切换 conda/venv 后再跑 tt setup,
|
|
85
|
+
# 老的 codex-statusline 段被无脑幂等保留 → command 指向已死 python → 状态栏半残(issue 用户反馈)。
|
|
86
|
+
monkeypatch.setattr(hooks, "CODEX_STATUSLINE_HOOK_PATH", str(tmp_path / "codex-statusline.py"))
|
|
87
|
+
first = hooks._install_codex_statusline("", "/old/python")
|
|
88
|
+
assert "/old/python" in first
|
|
89
|
+
# 同 python 再装 → 幂等保持
|
|
90
|
+
assert hooks._install_codex_statusline(first, "/old/python") == first
|
|
91
|
+
# 换 python(用户升级了 Python / 切了环境)→ 必须重写、删旧段、装新段
|
|
92
|
+
second = hooks._install_codex_statusline(first, "/new/python")
|
|
93
|
+
assert "/new/python" in second
|
|
94
|
+
assert "/old/python" not in second # 死路径清干净
|
|
95
|
+
assert second.count("[[hooks.Stop]]") == 1 # 没残留两段
|
|
96
|
+
|
|
97
|
+
|
|
83
98
|
def test_codex_statusline_windows_path_toml_parses(tmp_path, monkeypatch):
|
|
84
99
|
# 回归:Windows 路径含 \U \A \P 等被 TOML basic string 当 unicode 转义起始符(issue 用户反馈)
|
|
85
100
|
# —— 写入的 command 必须用 literal string(单引号)包裹,写出来后能被 tomllib 原样解析回来。
|
|
86
101
|
import tomllib
|
|
87
102
|
monkeypatch.setattr(hooks, "CODEX_STATUSLINE_HOOK_PATH",
|
|
88
103
|
r"C:\Users\test\.config\token-tracker\codex-statusline.py")
|
|
104
|
+
monkeypatch.setattr(hooks, "_write_codex_statusline_script", lambda: None) # 别在 macOS 上真写 Windows 路径
|
|
89
105
|
py = r"C:\Users\test\AppData\Local\Programs\Python\Python313\python.exe"
|
|
90
106
|
content = hooks._install_codex_statusline("", py)
|
|
91
107
|
parsed = tomllib.loads(content) # 旧 bug:basic string 下 \U 等触发 TOML 解析错误
|
|
@@ -128,8 +144,8 @@ def test_setup_components_off_skips_install(tmp_path, monkeypatch):
|
|
|
128
144
|
|
|
129
145
|
hooks.setup(components=hooks.SetupComponents(codex_faux_statusline=False))
|
|
130
146
|
|
|
131
|
-
# CC statusline
|
|
132
|
-
assert json.loads(settings_path.read_text())["statusLine"]["command"].endswith(
|
|
147
|
+
# CC statusline 仍装(command 现在带引号包裹,issue #13 修复)
|
|
148
|
+
assert json.loads(settings_path.read_text())["statusLine"]["command"].endswith('tt-statusline.py"')
|
|
133
149
|
# Codex 端:不再动 [tui].status_line(保持用户原配置);Stop hook(tt-statusline)也不在 config 里
|
|
134
150
|
codex_content = codex_config.read_text()
|
|
135
151
|
assert "status_line = []" in codex_content # 用户原 status_line 没被动
|
|
@@ -213,6 +229,12 @@ def test_statusline_shows_git_diff_stat(tmp_path):
|
|
|
213
229
|
line_dirty = _run_statusline(script_path, repo)
|
|
214
230
|
assert "+2" in line_dirty and "-1" in line_dirty
|
|
215
231
|
|
|
232
|
+
# 再造 2 个未跟踪文件 → 应额外显示 ?2(按文件数计、不读行数)
|
|
233
|
+
(repo / "new1.txt").write_text("x\n")
|
|
234
|
+
(repo / "new2.txt").write_text("x\ny\n")
|
|
235
|
+
line_with_untracked = _run_statusline(script_path, repo)
|
|
236
|
+
assert "?2" in line_with_untracked
|
|
237
|
+
|
|
216
238
|
|
|
217
239
|
def _run_statusline_home(script_path, payload, home):
|
|
218
240
|
"""隔离 HOME 下跑落盘 statusline 脚本,返回完整 stdout(不污染真实 ~/.claude)。"""
|
|
@@ -464,3 +486,77 @@ def test_cli_setup_up_to_date_skips_flow(monkeypatch, tmp_path):
|
|
|
464
486
|
|
|
465
487
|
cli.main()
|
|
466
488
|
assert calls == {}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def test_build_cc_command_windows_quotes_and_slashes(monkeypatch):
|
|
492
|
+
# issue #13/#14:Windows 上 statusLine command 必须正斜杠 + 引号包裹,
|
|
493
|
+
# 否则 CC 走 Git Bash 执行时反斜杠被吞,状态栏静默空白。
|
|
494
|
+
monkeypatch.setattr(hooks.os, "name", "nt")
|
|
495
|
+
cmd = hooks._build_cc_command(
|
|
496
|
+
r"C:\Users\X\pipx\venvs\token-tracker\Scripts\python.exe",
|
|
497
|
+
r"C:\Users\X\.config\token-tracker\claude-statusline.py",
|
|
498
|
+
)
|
|
499
|
+
assert cmd == '"C:/Users/X/pipx/venvs/token-tracker/Scripts/python.exe" "C:/Users/X/.config/token-tracker/claude-statusline.py"'
|
|
500
|
+
assert "\\" not in cmd # 反斜杠全转完
|
|
501
|
+
assert cmd.count('"') == 4 # 两段路径各包一对引号
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def test_build_cc_command_unix_always_quoted(monkeypatch):
|
|
505
|
+
# Unix 平台不转换路径分隔符,但始终加引号(防路径含空格断词)。
|
|
506
|
+
monkeypatch.setattr(hooks.os, "name", "posix")
|
|
507
|
+
cmd = hooks._build_cc_command(
|
|
508
|
+
"/Users/John Doe/.local/share/uv/tools/token-tracker/bin/python3",
|
|
509
|
+
"/Users/John Doe/.config/token-tracker/claude-statusline.py",
|
|
510
|
+
)
|
|
511
|
+
assert cmd.startswith('"') and cmd.count('"') == 4
|
|
512
|
+
assert "John Doe" in cmd # 含空格路径被引号包住、能正确执行
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def test_cc_command_outdated_detects_legacy_format(monkeypatch):
|
|
516
|
+
# 旧格式(裸拼接、无引号)应被检测为过时;新格式不动。
|
|
517
|
+
monkeypatch.setattr(hooks.os, "name", "posix")
|
|
518
|
+
assert hooks._cc_command_outdated("/usr/bin/python3 /home/u/.config/token-tracker/claude-statusline.py")
|
|
519
|
+
assert not hooks._cc_command_outdated('"/usr/bin/python3" "/home/u/.config/token-tracker/claude-statusline.py"')
|
|
520
|
+
# Windows 上即便有引号,含反斜杠也算过时
|
|
521
|
+
monkeypatch.setattr(hooks.os, "name", "nt")
|
|
522
|
+
assert hooks._cc_command_outdated(r'"C:\Users\X\python.exe" "C:\Users\X\claude-statusline.py"')
|
|
523
|
+
assert not hooks._cc_command_outdated('"C:/Users/X/python.exe" "C:/Users/X/claude-statusline.py"')
|
|
524
|
+
# 空命令 / 非 tt 命令交给上层 _is_tt_cc_command 过滤;这里仅断言空串返回 False
|
|
525
|
+
assert not hooks._cc_command_outdated("")
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def test_update_hook_rewrites_outdated_cc_command(tmp_path, monkeypatch):
|
|
529
|
+
# 老用户场景:HOOK_SCRIPT_PATH 存在 + settings.json 里 command 是旧格式 →
|
|
530
|
+
# 跑任意 tt 命令触发 update_hook 自动重写为新格式(用户其它字段不动)。
|
|
531
|
+
settings_file = tmp_path / "settings.json"
|
|
532
|
+
script_file = tmp_path / "claude-statusline.py"
|
|
533
|
+
script_file.write_text(hooks._render_hook_script(), encoding="utf-8")
|
|
534
|
+
settings_file.write_text(json.dumps({
|
|
535
|
+
"statusLine": {"type": "command",
|
|
536
|
+
"command": "/old/python3 /old/path/claude-statusline.py"},
|
|
537
|
+
"userField": "keep me",
|
|
538
|
+
}), encoding="utf-8")
|
|
539
|
+
monkeypatch.setattr(hooks, "CLAUDE_SETTINGS", str(settings_file))
|
|
540
|
+
monkeypatch.setattr(hooks, "HOOK_SCRIPT_PATH", str(script_file))
|
|
541
|
+
monkeypatch.setattr(hooks.sys, "executable", "/new/python3")
|
|
542
|
+
monkeypatch.setattr(hooks.os, "name", "posix")
|
|
543
|
+
|
|
544
|
+
assert hooks._cc_command_needs_sync() # 检测到过时
|
|
545
|
+
hooks.update_hook()
|
|
546
|
+
new_settings = json.loads(settings_file.read_text(encoding="utf-8"))
|
|
547
|
+
assert new_settings["statusLine"]["command"].startswith('"/new/python3"')
|
|
548
|
+
assert new_settings["userField"] == "keep me" # 用户其它字段保留
|
|
549
|
+
assert not hooks._cc_command_needs_sync() # 重写后不再触发
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def test_cc_command_sync_skips_non_tt_command(tmp_path, monkeypatch):
|
|
553
|
+
# 用户自己的 statusLine(非 tt)即便没引号也不动——只管 tt 自己装的。
|
|
554
|
+
settings_file = tmp_path / "settings.json"
|
|
555
|
+
user_cmd = "/usr/bin/my-own-statusline --foo"
|
|
556
|
+
settings_file.write_text(json.dumps({
|
|
557
|
+
"statusLine": {"type": "command", "command": user_cmd},
|
|
558
|
+
}), encoding="utf-8")
|
|
559
|
+
monkeypatch.setattr(hooks, "CLAUDE_SETTINGS", str(settings_file))
|
|
560
|
+
assert not hooks._cc_command_needs_sync()
|
|
561
|
+
hooks._sync_cc_command() # no-op
|
|
562
|
+
assert json.loads(settings_file.read_text(encoding="utf-8"))["statusLine"]["command"] == user_cmd
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|