claude-code-cache-fix 3.3.0 → 3.5.0

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.
@@ -1,25 +1,82 @@
1
1
  #!/bin/bash
2
- # Status line: show quota % and burn rate from quota-status.json
2
+ # Status line: show quota % and burn rate from per-session quota-status files.
3
3
  # Written by cache-fix proxy's cache-telemetry extension on every API call.
4
+ #
5
+ # Layout (post-v3.5.0):
6
+ # ~/.claude/quota-status/account.json — global quota fields (5h/7d, status, overage)
7
+ # ~/.claude/quota-status/sessions/<filename>.json — per-session cache fields (ttl_tier, hit_rate)
8
+ #
9
+ # CC pipes hook input as JSON on stdin including `session_id`, which we map to
10
+ # the per-session filename via the canonical rule (matches the writer in
11
+ # proxy/extensions/cache-telemetry.mjs:sessionFilename).
4
12
 
5
13
  input=$(cat)
6
14
 
7
- QS="$HOME/.claude/quota-status.json"
15
+ ACCOUNT="$HOME/.claude/quota-status/account.json"
16
+ SESSIONS_DIR="$HOME/.claude/quota-status/sessions"
8
17
 
9
- if [ -f "$QS" ]; then
10
- result=$(python3 -c "
11
- import sys, json, os
18
+ # Show quota even if no per-session file exists yet (fresh session, first
19
+ # request hasn't fired). Per-session block just gets blank.
20
+ if [ ! -f "$ACCOUNT" ]; then
21
+ exit 0
22
+ fi
23
+
24
+ result=$(python3 -c "
25
+ import sys, json, os, re, hashlib
12
26
  from datetime import datetime, timezone, timedelta
13
27
 
14
- qs = json.load(open(os.path.expanduser('~/.claude/quota-status.json')))
28
+ home = os.path.expanduser('~')
29
+ account_path = os.path.join(home, '.claude', 'quota-status', 'account.json')
30
+ sessions_dir = os.path.join(home, '.claude', 'quota-status', 'sessions')
31
+
32
+ # Parse stdin JSON (CC hook input) for session_id. Pass the raw value
33
+ # (including null / "" / whitespace) through session_filename so the
34
+ # canonical rule decides — the writer maps all those to 'unknown',
35
+ # the reader must do the same to keep the contract identical.
36
+ try:
37
+ stdin_data = json.loads('''$input''') if '''$input''' else {}
38
+ except Exception:
39
+ stdin_data = {}
40
+ sess_id_raw = stdin_data.get('session_id')
41
+
42
+ # Canonical filename derivation — must match cache-telemetry.mjs:sessionFilename.
43
+ # Allowlist: [A-Za-z0-9_-]{1,128}; else inv-<sha256(s)[:16]>; null/empty/whitespace -> 'unknown'.
44
+ SAFE = re.compile(r'^[A-Za-z0-9_-]{1,128}\$')
45
+ def session_filename(raw):
46
+ if raw is None:
47
+ return 'unknown'
48
+ s = str(raw).strip()
49
+ if not s:
50
+ return 'unknown'
51
+ if SAFE.match(s):
52
+ return s
53
+ return 'inv-' + hashlib.sha256(s.encode('utf-8')).hexdigest()[:16]
15
54
 
16
- q5h = qs.get('five_hour', {}).get('pct', 0)
17
- q7d = qs.get('seven_day', {}).get('pct', 0)
18
- q5h_reset = qs.get('five_hour', {}).get('resets_at', 0)
19
- q7d_reset = qs.get('seven_day', {}).get('resets_at', 0)
20
- status = qs.get('status', '')
21
- overage = qs.get('overage_status', '')
22
- ts = qs.get('timestamp', '')
55
+ # Read account.json (account-global fields).
56
+ try:
57
+ acc = json.load(open(account_path))
58
+ except Exception:
59
+ sys.exit(0)
60
+
61
+ # Read this session's per-session file (cache fields). Apply the rule
62
+ # unconditionally — null/empty/whitespace land at sessions/unknown.json,
63
+ # matching where the writer would have placed them. If the file doesn't
64
+ # exist (e.g. unknown.json never written, or this is a fresh session
65
+ # whose first request hasn't fired), statusline still shows quota % —
66
+ # just no TTL/hit-rate block.
67
+ sess_filename = session_filename(sess_id_raw)
68
+ try:
69
+ sess = json.load(open(os.path.join(sessions_dir, sess_filename + '.json')))
70
+ except Exception:
71
+ sess = {}
72
+
73
+ q5h = acc.get('five_hour', {}).get('pct', 0)
74
+ q7d = acc.get('seven_day', {}).get('pct', 0)
75
+ q5h_reset = acc.get('five_hour', {}).get('resets_at', 0)
76
+ q7d_reset = acc.get('seven_day', {}).get('resets_at', 0)
77
+ status = acc.get('status', '')
78
+ overage = acc.get('overage_status', '')
79
+ ts = sess.get('timestamp') or acc.get('timestamp', '')
23
80
 
24
81
  now = datetime.fromisoformat(ts.replace('Z', '+00:00')) if ts else datetime.now(timezone.utc)
25
82
 
@@ -48,9 +105,9 @@ if rate7:
48
105
  if overage == 'active':
49
106
  label += ' | OVERAGE'
50
107
 
51
- # TTL and cache stats
52
- ttl = qs.get('cache', {}).get('ttl_tier', '')
53
- hit = qs.get('cache', {}).get('hit_rate', '')
108
+ # Per-session TTL and cache stats
109
+ ttl = sess.get('cache', {}).get('ttl_tier', '')
110
+ hit = sess.get('cache', {}).get('hit_rate', '')
54
111
  if ttl:
55
112
  if ttl == '5m':
56
113
  label += ' | \033[31mTTL:5m\033[0m'
@@ -59,12 +116,11 @@ if ttl:
59
116
  if hit and hit != 'N/A':
60
117
  label += ' ' + hit + '%'
61
118
 
62
- peak = qs.get('peak_hour', False)
119
+ peak = acc.get('peak_hour', False)
63
120
  if peak:
64
121
  label += ' | \033[33mPEAK\033[0m'
65
122
 
66
123
  print(label)
67
124
  " 2>/dev/null)
68
125
 
69
- [ -n "$result" ] && echo "$result"
70
- fi
126
+ [ -n "$result" ] && echo "$result"