token-tracker 0.2.4__tar.gz → 0.3.1__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.2.4 → token_tracker-0.3.1}/PKG-INFO +1 -1
- {token_tracker-0.2.4 → token_tracker-0.3.1}/README.md +2 -1
- {token_tracker-0.2.4 → token_tracker-0.3.1}/pyproject.toml +1 -1
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/analyzer/cost.py +12 -3
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/hooks.py +109 -40
- {token_tracker-0.2.4 → token_tracker-0.3.1}/token_tracker.egg-info/PKG-INFO +1 -1
- {token_tracker-0.2.4 → token_tracker-0.3.1}/setup.cfg +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/__init__.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/adapters/__init__.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/adapters/claude.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/adapters/codex.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/adapters/rate_limits.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/adapters/registry.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/adapters/types.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/analyzer/__init__.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/analyzer/aggregator.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/analyzer/blocks.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/cli.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/ui/__init__.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/src/ui/tables.py +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/token_tracker.egg-info/SOURCES.txt +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/token_tracker.egg-info/dependency_links.txt +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/token_tracker.egg-info/entry_points.txt +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/token_tracker.egg-info/requires.txt +0 -0
- {token_tracker-0.2.4 → token_tracker-0.3.1}/token_tracker.egg-info/top_level.txt +0 -0
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
- **成本分析** — 按会话、日、周、月维度的等效成本统计,多 Agent 按来源分组展示
|
|
39
39
|
- **会话洞察** — 项目、模型、时长、消息数一览
|
|
40
40
|
- **零配置** — 自动检测已安装的 Agent,直接读取本地数据
|
|
41
|
+
- **隐私安全** — 数据纯本地存储,不采集、不上传任何用户信息,极轻量无后顾之忧
|
|
41
42
|
|
|
42
43
|
## 安装
|
|
43
44
|
|
|
@@ -48,7 +49,7 @@ curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/master/inst
|
|
|
48
49
|
或者通过 pip:
|
|
49
50
|
|
|
50
51
|
```bash
|
|
51
|
-
pip install token-tracker
|
|
52
|
+
pip install --force-reinstall token-tracker
|
|
52
53
|
tt setup
|
|
53
54
|
```
|
|
54
55
|
|
|
@@ -73,9 +73,18 @@ def _load_pricing() -> dict:
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def _fetch_and_cache() -> dict:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
import ssl
|
|
77
|
+
ctx = ssl.create_default_context()
|
|
78
|
+
try:
|
|
79
|
+
req = urllib.request.Request(LITELLM_URL, headers={"User-Agent": "token-tracker/0.1"})
|
|
80
|
+
with urllib.request.urlopen(req, timeout=10, context=ctx) as resp:
|
|
81
|
+
data = json.loads(resp.read().decode())
|
|
82
|
+
except ssl.SSLCertVerificationError:
|
|
83
|
+
ctx.check_hostname = False
|
|
84
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
85
|
+
req = urllib.request.Request(LITELLM_URL, headers={"User-Agent": "token-tracker/0.1"})
|
|
86
|
+
with urllib.request.urlopen(req, timeout=10, context=ctx) as resp:
|
|
87
|
+
data = json.loads(resp.read().decode())
|
|
79
88
|
|
|
80
89
|
try:
|
|
81
90
|
with open(CACHE_PATH, "w") as f:
|
|
@@ -10,7 +10,7 @@ CLAUDE_SETTINGS = os.path.expanduser("~/.claude/settings.json")
|
|
|
10
10
|
HOOK_SCRIPT_PATH = os.path.expanduser("~/.claude/tt-statusline.py")
|
|
11
11
|
CODEX_CONFIG = os.path.expanduser("~/.codex/config.toml")
|
|
12
12
|
CODEX_BACKUP = os.path.expanduser("~/.codex/tt-backup.json")
|
|
13
|
-
HOOK_VERSION = "1.
|
|
13
|
+
HOOK_VERSION = "1.2"
|
|
14
14
|
_BACKUP_KEY = "tokenTracker"
|
|
15
15
|
_PREV_SL_KEY = "previousStatusLine"
|
|
16
16
|
_SL_REGEX = re.compile(r'status_line\s*=\s*\[.*?\]', re.DOTALL)
|
|
@@ -25,8 +25,8 @@ CODEX_STATUS_LINE = [
|
|
|
25
25
|
|
|
26
26
|
HOOK_SCRIPT = r'''#!/usr/bin/env python3
|
|
27
27
|
"""Claude Code statusLine — 状态栏显示 + 数据持久化到 tt-status.json"""
|
|
28
|
-
__version__ = "1.
|
|
29
|
-
import json, os, sys, tempfile
|
|
28
|
+
__version__ = "1.2"
|
|
29
|
+
import json, os, subprocess, sys, tempfile
|
|
30
30
|
from datetime import datetime, timezone
|
|
31
31
|
|
|
32
32
|
STATUS_FILE = os.path.expanduser("~/.claude/tt-status.json")
|
|
@@ -57,60 +57,129 @@ def progress_bar(value):
|
|
|
57
57
|
return f"{color_by_pct(pct)}{filled_char * filled}{C['reset']}{empty_char * (width - filled)} {pct:.0f}%"
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
def
|
|
61
|
-
|
|
60
|
+
def fmt_duration(seconds):
|
|
61
|
+
if seconds >= 86400:
|
|
62
|
+
d, rem = int(seconds // 86400), int(seconds % 86400)
|
|
63
|
+
return f"{d}d{rem // 3600}h"
|
|
64
|
+
if seconds >= 3600:
|
|
65
|
+
h, m = int(seconds // 3600), int((seconds % 3600) // 60)
|
|
66
|
+
return f"{h}h{m}m"
|
|
67
|
+
if seconds >= 60:
|
|
68
|
+
return f"{int(seconds // 60)}min"
|
|
69
|
+
return f"{int(seconds)}s"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def git_branch(cwd):
|
|
73
|
+
try:
|
|
74
|
+
branch = subprocess.check_output(
|
|
75
|
+
["git", "branch", "--show-current"], cwd=cwd,
|
|
76
|
+
stderr=subprocess.DEVNULL, text=True, timeout=2,
|
|
77
|
+
).strip()
|
|
78
|
+
except Exception:
|
|
79
|
+
return ""
|
|
80
|
+
if not branch:
|
|
81
|
+
return ""
|
|
82
|
+
try:
|
|
83
|
+
dirty = subprocess.check_output(
|
|
84
|
+
["git", "status", "--porcelain", "--untracked-files=no"], cwd=cwd,
|
|
85
|
+
stderr=subprocess.DEVNULL, text=True, timeout=2,
|
|
86
|
+
).strip()
|
|
87
|
+
if dirty:
|
|
88
|
+
branch += "*"
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
return branch
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def save_data(data, now):
|
|
95
|
+
data["_received_at"] = now.isoformat()
|
|
96
|
+
tmp = None
|
|
62
97
|
try:
|
|
63
98
|
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(STATUS_FILE), suffix=".tmp")
|
|
64
99
|
with os.fdopen(fd, "w") as f:
|
|
65
100
|
json.dump(data, f)
|
|
66
101
|
os.replace(tmp, STATUS_FILE)
|
|
67
102
|
except OSError:
|
|
68
|
-
|
|
103
|
+
if tmp:
|
|
104
|
+
try:
|
|
105
|
+
os.unlink(tmp)
|
|
106
|
+
except OSError:
|
|
107
|
+
pass
|
|
69
108
|
|
|
70
109
|
|
|
71
|
-
def render(data):
|
|
72
|
-
|
|
110
|
+
def render(data, now):
|
|
111
|
+
ctx = data.get("context_window") or {}
|
|
73
112
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
parts.append(f"{C['cyan']}{os.path.basename(project)}{C['reset']}")
|
|
113
|
+
# --- Line 1: Project | 5h | 7d | CTX ---
|
|
114
|
+
line1 = []
|
|
77
115
|
|
|
78
|
-
|
|
79
|
-
|
|
116
|
+
project = (data.get("workspace") or {}).get("project_dir", "")
|
|
117
|
+
if project:
|
|
118
|
+
name = os.path.basename(project)
|
|
119
|
+
branch = git_branch(project)
|
|
120
|
+
if branch:
|
|
121
|
+
line1.append(f"{C['green']}{name}{C['reset']}({C['magenta']}{branch}{C['reset']})")
|
|
122
|
+
else:
|
|
123
|
+
line1.append(f"{C['green']}{name}{C['reset']}")
|
|
124
|
+
|
|
125
|
+
rl = data.get("rate_limits") or {}
|
|
80
126
|
for key, label in [("five_hour", "5h"), ("seven_day", "7d")]:
|
|
81
|
-
|
|
127
|
+
entry = rl.get(key) or {}
|
|
128
|
+
pct = entry.get("used_percentage")
|
|
82
129
|
if pct is not None:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
parts.append(f"{C['blue']}Cost:{C['reset']}{C['peach']}${usd:.2f}{C['reset']}")
|
|
130
|
+
reset_str = ""
|
|
131
|
+
resets_at = entry.get("resets_at")
|
|
132
|
+
if resets_at:
|
|
133
|
+
remain = int(resets_at) - int(now.timestamp())
|
|
134
|
+
if remain > 0:
|
|
135
|
+
reset_str = f" {C['dim']}({fmt_duration(remain)}){C['reset']}"
|
|
136
|
+
line1.append(f"{C['blue']}{label}:{C['reset']}{progress_bar(pct)}{reset_str}")
|
|
91
137
|
|
|
92
|
-
ctx = data.get("context_window", {})
|
|
93
138
|
if ctx.get("used_percentage") is not None:
|
|
94
139
|
size = ctx.get("context_window_size", 0)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
line1.append(f"{C['blue']}{fmt_tokens(size)} Context:{C['reset']}{progress_bar(ctx['used_percentage'])}")
|
|
141
|
+
|
|
142
|
+
# --- Line 2: Tokens + Cache + Cost ---
|
|
143
|
+
line2 = []
|
|
98
144
|
|
|
99
145
|
total_in = ctx.get("total_input_tokens", 0)
|
|
100
146
|
total_out = ctx.get("total_output_tokens", 0)
|
|
101
|
-
|
|
147
|
+
curr_usage = (ctx.get("current_usage") or {})
|
|
148
|
+
turn_in_total = curr_usage.get("input_tokens", 0) + curr_usage.get("cache_creation_input_tokens", 0)
|
|
149
|
+
turn_out = curr_usage.get("output_tokens", 0)
|
|
102
150
|
if total_in or total_out:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
151
|
+
tok = f"{C['peach']}Tokens: in {fmt_tokens(total_in)}, out {fmt_tokens(total_out)}"
|
|
152
|
+
tok += f" {C['dim']}(本轮: in {fmt_tokens(turn_in_total)}, out {fmt_tokens(turn_out)})"
|
|
153
|
+
tok += C['reset']
|
|
154
|
+
line2.append(tok)
|
|
155
|
+
cache_read = curr_usage.get("cache_read_input_tokens", 0)
|
|
156
|
+
if cache_read > 0:
|
|
157
|
+
line2.append(f"{C['cyan']}Cached: {fmt_tokens(cache_read)}{C['reset']}")
|
|
158
|
+
|
|
159
|
+
cost = data.get("cost") or {}
|
|
160
|
+
usd = cost.get("total_cost_usd")
|
|
161
|
+
if usd is not None:
|
|
162
|
+
line2.append(f"{C['magenta']}Cost: ${usd:.2f}{C['reset']}")
|
|
163
|
+
|
|
164
|
+
# --- Line 3: Duration + Model ---
|
|
165
|
+
line3 = []
|
|
166
|
+
|
|
167
|
+
duration_ms = cost.get("total_duration_ms")
|
|
168
|
+
if duration_ms and duration_ms > 0:
|
|
169
|
+
line3.append(f"{C['dim']}{C['magenta']}会话时长: {fmt_duration(duration_ms / 1000)}{C['reset']}")
|
|
170
|
+
|
|
171
|
+
model_name = (data.get("model") or {}).get("display_name", "")
|
|
106
172
|
if model_name:
|
|
107
|
-
effort = data.get("effort"
|
|
173
|
+
effort = (data.get("effort") or {}).get("level", "")
|
|
108
174
|
if effort:
|
|
109
|
-
model_name += f"
|
|
110
|
-
|
|
175
|
+
model_name += f"/{effort}"
|
|
176
|
+
fast = data.get("fast_mode")
|
|
177
|
+
model_name += f"/{'fast' if fast else 'nofast'}"
|
|
178
|
+
line3.append(f"{C['dim']}{C['magenta']}{model_name}{C['reset']}")
|
|
111
179
|
|
|
112
|
-
if
|
|
113
|
-
|
|
180
|
+
output = [" | ".join(line) for line in (line1, line2, line3) if line]
|
|
181
|
+
if output:
|
|
182
|
+
print("\n".join(output))
|
|
114
183
|
|
|
115
184
|
|
|
116
185
|
def main():
|
|
@@ -121,8 +190,10 @@ def main():
|
|
|
121
190
|
data = json.loads(raw)
|
|
122
191
|
except Exception:
|
|
123
192
|
return
|
|
124
|
-
|
|
125
|
-
|
|
193
|
+
|
|
194
|
+
now = datetime.now(timezone.utc)
|
|
195
|
+
save_data(data, now)
|
|
196
|
+
render(data, now)
|
|
126
197
|
|
|
127
198
|
|
|
128
199
|
if __name__ == "__main__":
|
|
@@ -218,9 +289,7 @@ def setup(auto: bool = False) -> None:
|
|
|
218
289
|
|
|
219
290
|
|
|
220
291
|
def _setup_claude() -> None:
|
|
221
|
-
|
|
222
|
-
f.write(HOOK_SCRIPT)
|
|
223
|
-
os.chmod(HOOK_SCRIPT_PATH, os.stat(HOOK_SCRIPT_PATH).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
292
|
+
update_hook()
|
|
224
293
|
|
|
225
294
|
settings: dict = {}
|
|
226
295
|
if os.path.exists(CLAUDE_SETTINGS):
|
|
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
|