token-tracker 0.3.1__tar.gz → 0.3.2__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.3.1 → token_tracker-0.3.2}/PKG-INFO +1 -1
- {token_tracker-0.3.1 → token_tracker-0.3.2}/pyproject.toml +1 -1
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/hooks.py +74 -17
- {token_tracker-0.3.1 → token_tracker-0.3.2}/token_tracker.egg-info/PKG-INFO +1 -1
- {token_tracker-0.3.1 → token_tracker-0.3.2}/README.md +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/setup.cfg +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/__init__.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/adapters/__init__.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/adapters/claude.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/adapters/codex.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/adapters/rate_limits.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/adapters/registry.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/adapters/types.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/analyzer/__init__.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/analyzer/aggregator.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/analyzer/blocks.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/analyzer/cost.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/cli.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/ui/__init__.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/src/ui/tables.py +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/token_tracker.egg-info/SOURCES.txt +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/token_tracker.egg-info/dependency_links.txt +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/token_tracker.egg-info/entry_points.txt +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/token_tracker.egg-info/requires.txt +0 -0
- {token_tracker-0.3.1 → token_tracker-0.3.2}/token_tracker.egg-info/top_level.txt +0 -0
|
@@ -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.4"
|
|
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,12 +25,12 @@ 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, subprocess, sys, tempfile
|
|
28
|
+
__version__ = "1.4"
|
|
29
|
+
import json, os, re, subprocess, sys, tempfile
|
|
30
30
|
from datetime import datetime, timezone
|
|
31
31
|
|
|
32
32
|
STATUS_FILE = os.path.expanduser("~/.claude/tt-status.json")
|
|
33
|
-
|
|
33
|
+
ANSI_RE = re.compile(r'\033\[[0-9;]*m')
|
|
34
34
|
C = {
|
|
35
35
|
"green": "\033[32m", "yellow": "\033[33m", "red": "\033[31m",
|
|
36
36
|
"cyan": "\033[36m", "blue": "\033[34m", "magenta": "\033[35m",
|
|
@@ -38,6 +38,24 @@ C = {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def vlen(s):
|
|
42
|
+
return len(ANSI_RE.sub("", s))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_width():
|
|
46
|
+
try:
|
|
47
|
+
return max(1, os.get_terminal_size(2).columns - 4)
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
import fcntl, struct, termios
|
|
51
|
+
try:
|
|
52
|
+
with open('/dev/tty', 'r') as tty:
|
|
53
|
+
res = fcntl.ioctl(tty, termios.TIOCGWINSZ, b'\x00' * 8)
|
|
54
|
+
return max(1, struct.unpack('hh', res[:4])[1] - 4)
|
|
55
|
+
except Exception:
|
|
56
|
+
return 116
|
|
57
|
+
|
|
58
|
+
|
|
41
59
|
def color_by_pct(pct):
|
|
42
60
|
return C["green"] if pct < 50 else C["yellow"] if pct < 80 else C["red"]
|
|
43
61
|
|
|
@@ -48,13 +66,13 @@ def fmt_tokens(n):
|
|
|
48
66
|
return str(n)
|
|
49
67
|
|
|
50
68
|
|
|
51
|
-
def progress_bar(value):
|
|
52
|
-
filled_char, empty_char
|
|
69
|
+
def progress_bar(value, bar_width=8):
|
|
70
|
+
filled_char, empty_char = "█", "░"
|
|
53
71
|
if value is None:
|
|
54
|
-
return empty_char *
|
|
72
|
+
return empty_char * bar_width + " n/a"
|
|
55
73
|
pct = max(0.0, min(100.0, float(value)))
|
|
56
|
-
filled = round(pct / 100 *
|
|
57
|
-
return f"{color_by_pct(pct)}{filled_char * filled}{C['reset']}{empty_char * (
|
|
74
|
+
filled = round(pct / 100 * bar_width)
|
|
75
|
+
return f"{color_by_pct(pct)}{filled_char * filled}{C['reset']}{empty_char * (bar_width - filled)} {pct:.0f}%"
|
|
58
76
|
|
|
59
77
|
|
|
60
78
|
def fmt_duration(seconds):
|
|
@@ -108,7 +126,9 @@ def save_data(data, now):
|
|
|
108
126
|
|
|
109
127
|
|
|
110
128
|
def render(data, now):
|
|
129
|
+
W = get_width()
|
|
111
130
|
ctx = data.get("context_window") or {}
|
|
131
|
+
bar_w = 8 if W >= 100 else 6 if W >= 60 else 4
|
|
112
132
|
|
|
113
133
|
# --- Line 1: Project | 5h | 7d | CTX ---
|
|
114
134
|
line1 = []
|
|
@@ -123,6 +143,7 @@ def render(data, now):
|
|
|
123
143
|
line1.append(f"{C['green']}{name}{C['reset']}")
|
|
124
144
|
|
|
125
145
|
rl = data.get("rate_limits") or {}
|
|
146
|
+
rl_parts = []
|
|
126
147
|
for key, label in [("five_hour", "5h"), ("seven_day", "7d")]:
|
|
127
148
|
entry = rl.get(key) or {}
|
|
128
149
|
pct = entry.get("used_percentage")
|
|
@@ -133,11 +154,35 @@ def render(data, now):
|
|
|
133
154
|
remain = int(resets_at) - int(now.timestamp())
|
|
134
155
|
if remain > 0:
|
|
135
156
|
reset_str = f" {C['dim']}({fmt_duration(remain)}){C['reset']}"
|
|
136
|
-
|
|
157
|
+
rl_parts.append((
|
|
158
|
+
f"{C['blue']}{label}:{C['reset']}{progress_bar(pct, bar_w)}{reset_str}",
|
|
159
|
+
f"{C['blue']}{label}:{C['reset']}{progress_bar(pct, bar_w)}",
|
|
160
|
+
f"{C['blue']}{label}:{C['reset']}{pct:.0f}%",
|
|
161
|
+
))
|
|
137
162
|
|
|
163
|
+
ctx_parts = []
|
|
138
164
|
if ctx.get("used_percentage") is not None:
|
|
139
165
|
size = ctx.get("context_window_size", 0)
|
|
140
|
-
|
|
166
|
+
ctx_parts = [
|
|
167
|
+
f"{C['blue']}{fmt_tokens(size)} Context:{C['reset']}{progress_bar(ctx['used_percentage'], bar_w)}",
|
|
168
|
+
f"{C['blue']}{fmt_tokens(size)} CTX:{C['reset']}{ctx['used_percentage']:.0f}%",
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
# 尝试完整版(带进度条+reset time)
|
|
172
|
+
full = line1 + [p[0] for p in rl_parts] + (ctx_parts[:1] if ctx_parts else [])
|
|
173
|
+
candidate = " | ".join(full)
|
|
174
|
+
if vlen(candidate) <= W:
|
|
175
|
+
line1 = full
|
|
176
|
+
else:
|
|
177
|
+
# 去掉 reset time
|
|
178
|
+
no_reset = line1 + [p[1] for p in rl_parts] + (ctx_parts[:1] if ctx_parts else [])
|
|
179
|
+
candidate = " | ".join(no_reset)
|
|
180
|
+
if vlen(candidate) <= W:
|
|
181
|
+
line1 = no_reset
|
|
182
|
+
else:
|
|
183
|
+
# 去掉进度条,只留百分比
|
|
184
|
+
minimal = line1 + [p[2] for p in rl_parts] + (ctx_parts[1:2] if ctx_parts else [])
|
|
185
|
+
line1 = minimal
|
|
141
186
|
|
|
142
187
|
# --- Line 2: Tokens + Cache + Cost ---
|
|
143
188
|
line2 = []
|
|
@@ -147,11 +192,11 @@ def render(data, now):
|
|
|
147
192
|
curr_usage = (ctx.get("current_usage") or {})
|
|
148
193
|
turn_in_total = curr_usage.get("input_tokens", 0) + curr_usage.get("cache_creation_input_tokens", 0)
|
|
149
194
|
turn_out = curr_usage.get("output_tokens", 0)
|
|
195
|
+
turn_str = f" {C['dim']}(本轮: in {fmt_tokens(turn_in_total)}, out {fmt_tokens(turn_out)}){C['reset']}"
|
|
150
196
|
if total_in or total_out:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
line2.append(tok)
|
|
197
|
+
tok_full = f"{C['peach']}Tokens: in {fmt_tokens(total_in)}, out {fmt_tokens(total_out)}{turn_str}"
|
|
198
|
+
tok_short = f"{C['peach']}Tokens: in {fmt_tokens(total_in)}, out {fmt_tokens(total_out)}{C['reset']}"
|
|
199
|
+
line2.append(tok_full)
|
|
155
200
|
cache_read = curr_usage.get("cache_read_input_tokens", 0)
|
|
156
201
|
if cache_read > 0:
|
|
157
202
|
line2.append(f"{C['cyan']}Cached: {fmt_tokens(cache_read)}{C['reset']}")
|
|
@@ -161,12 +206,20 @@ def render(data, now):
|
|
|
161
206
|
if usd is not None:
|
|
162
207
|
line2.append(f"{C['magenta']}Cost: ${usd:.2f}{C['reset']}")
|
|
163
208
|
|
|
209
|
+
# 宽度不够时隐藏本轮数据
|
|
210
|
+
if vlen(" | ".join(line2)) > W and (total_in or total_out):
|
|
211
|
+
line2[0] = tok_short
|
|
212
|
+
if vlen(" | ".join(line2)) > W:
|
|
213
|
+
line2 = line2[1:]
|
|
214
|
+
|
|
164
215
|
# --- Line 3: Duration + Model ---
|
|
165
216
|
line3 = []
|
|
166
217
|
|
|
167
218
|
duration_ms = cost.get("total_duration_ms")
|
|
219
|
+
duration_part = ""
|
|
168
220
|
if duration_ms and duration_ms > 0:
|
|
169
|
-
|
|
221
|
+
duration_part = f"{C['dim']}{C['magenta']}会话时长: {fmt_duration(duration_ms / 1000)}{C['reset']}"
|
|
222
|
+
line3.append(duration_part)
|
|
170
223
|
|
|
171
224
|
model_name = (data.get("model") or {}).get("display_name", "")
|
|
172
225
|
if model_name:
|
|
@@ -177,6 +230,10 @@ def render(data, now):
|
|
|
177
230
|
model_name += f"/{'fast' if fast else 'nofast'}"
|
|
178
231
|
line3.append(f"{C['dim']}{C['magenta']}{model_name}{C['reset']}")
|
|
179
232
|
|
|
233
|
+
# 宽度不够时隐藏会话时长
|
|
234
|
+
if vlen(" | ".join(line3)) > W and duration_part:
|
|
235
|
+
line3 = [p for p in line3 if p != duration_part]
|
|
236
|
+
|
|
180
237
|
output = [" | ".join(line) for line in (line1, line2, line3) if line]
|
|
181
238
|
if output:
|
|
182
239
|
print("\n".join(output))
|
|
@@ -301,7 +358,7 @@ def _setup_claude() -> None:
|
|
|
301
358
|
console.print(f"[yellow]检测到已有 statusLine,备份后替换[/yellow]")
|
|
302
359
|
settings.setdefault(_BACKUP_KEY, {})[_PREV_SL_KEY] = existing
|
|
303
360
|
|
|
304
|
-
settings["statusLine"] = {"type": "command", "command": HOOK_SCRIPT_PATH}
|
|
361
|
+
settings["statusLine"] = {"type": "command", "command": f"python3 {HOOK_SCRIPT_PATH}"}
|
|
305
362
|
|
|
306
363
|
with open(CLAUDE_SETTINGS, "w", encoding="utf-8") as f:
|
|
307
364
|
json.dump(settings, f, indent=2, ensure_ascii=False)
|
|
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
|