claude-pace 0.7.2 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +35 -13
  2. package/claude-pace.sh +6 -131
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Claude Pace
2
2
 
3
- Know your quota before you hit the wall. A statusline for Claude Code single Bash file, zero npm.
3
+ A lightweight status line for Claude Code that tracks your 5-hour and 7-day rate limit usage in real time. Pure Bash + jq, single file, zero npm.
4
4
 
5
- Most statuslines show "you used 60%." That number means nothing without context. 60% with 30 minutes left? Fine, the window resets soon. 60% with 4 hours left? You're about to hit the wall. claude-pace compares your usage rate to the time remaining and shows the delta. No Node.js, no npm, no lock files. Single Bash file.
5
+ Most statuslines show "you used 60%." That number means nothing without context. 60% with 30 minutes left? Fine, the window resets soon. 60% with 4 hours left? You're about to hit the wall. claude-pace compares your burn rate to the time remaining and shows the delta: are you ahead of pace or behind?
6
6
 
7
7
  ![claude-pace statusline demo](.github/claude-pace-demo.gif)
8
8
 
@@ -57,18 +57,25 @@ Restart Claude Code. Done.
57
57
 
58
58
  To remove: delete the `statusLine` block from `~/.claude/settings.json`.
59
59
 
60
+ ## Upgrade
61
+
62
+ - **Plugin:** `/claude-pace:setup` (pulls the latest from GitHub)
63
+ - **npx:** `npx claude-pace@latest`
64
+ - **Manual:** Re-run the `curl` command above.
65
+
66
+ Release notifications: Watch this repo → Custom → Releases.
67
+
60
68
  ## How It Compares
61
69
 
62
- | | claude-pace | Node.js/TypeScript statuslines | Rust/Go statuslines |
63
- |---|---|---|---|
64
- | Runtime | `jq` | Node.js 18+ / npm | Compiled binary |
65
- | Codebase | Single file | 1000+ lines + node_modules | Compiled, not inspectable |
66
- | Execution | ~10ms, 3% of refresh cycle | ~90ms, 30% of refresh cycle | ~5ms (est.) |
67
- | Memory | ~2 MB | ~57 MB | ~3 MB (est.) |
68
- | Failure modes | Read-only, worst case prints "Claude" | Runtime dependency, package manager | Generally stable |
69
- | Pace tracking | Usage rate vs time remaining | Trend-only or none | None |
70
+ | | claude-pace | [claude-hud](https://github.com/jarrodwatts/claude-hud) | [CCometixLine](https://github.com/Haleclipse/CCometixLine) | [ccstatusline](https://github.com/sirmalloc/ccstatusline) |
71
+ |---|---|---|---|---|
72
+ | Runtime | `jq` | Node.js 18+ / npm | Compiled (Rust) | Node.js / npm |
73
+ | Codebase | Single Bash file | 1000+ lines + node_modules | Compiled binary | 1000+ lines + node_modules |
74
+ | Rate limit tracking | 5h + 7d usage %, pace delta, reset countdown | Usage % | Usage % (planned) | None (formatting only) |
75
+ | Execution | ~10ms | ~90ms | ~5ms | ~90ms |
76
+ | Memory | ~2 MB | ~57 MB | ~3 MB | ~57 MB |
70
77
 
71
- Execution and memory measured on Apple Silicon, 300 runs, same stdin JSON. Rust/Go values are estimates.
78
+ Execution and memory measured on Apple Silicon, 300 runs, same stdin JSON.
72
79
 
73
80
  Need themes, powerline aesthetics, or TUI config? Try [ccstatusline](https://github.com/sirmalloc/ccstatusline). The entire source of claude-pace is [one file](claude-pace.sh). Read it.
74
81
 
@@ -80,13 +87,26 @@ Claude Code polls the statusline every ~300ms:
80
87
  |------|--------|-------|
81
88
  | Model, context, cost | stdin JSON (single `jq` call) | None needed |
82
89
  | Quota (5h, 7d, pace) | stdin `rate_limits` (CC >= 2.1.80) | None needed (real-time) |
83
- | Quota fallback | Anthropic Usage API (CC < 2.1.80) | Private cache dir, 300s TTL, async background refresh |
84
90
  | Git branch + diff | `git` commands | Private cache dir, 5s TTL |
85
91
 
86
- On Claude Code >= 2.1.80, usage data comes directly from stdin. No network calls. On older versions, it falls back to the Usage API in a background subshell so the statusline never blocks.
92
+ Usage tracking requires Claude Code `2.1.80+`, where `rate_limits` is available in statusline stdin. claude-pace does not call the Anthropic Usage API.
87
93
 
88
94
  Cache files live in a private per-user directory (`$XDG_RUNTIME_DIR/claude-pace` or `~/.cache/claude-pace`, mode 700). All cache reads are validated before use. No files are ever written to shared `/tmp`.
89
95
 
96
+ ## FAQ
97
+
98
+ **Does it need Node.js?**
99
+ No. Only `jq` (available via `brew install jq` or your package manager). No npm, no node_modules, no lock files.
100
+
101
+ **How does pace tracking work?**
102
+ claude-pace compares your current usage percentage to the fraction of time elapsed in each window (5-hour and 7-day). If you've used 40% of your quota but only 30% of the time has passed, the pace delta shows ⇡10% (red, burning too fast). If you've used 30% with 40% of time elapsed, it shows ⇣10% (green, headroom).
103
+
104
+ **Does it make network calls?**
105
+ No. All displayed quota data comes from stdin. If `rate_limits` is missing, claude-pace shows `--` for quota and can still show the local session cost.
106
+
107
+ **Can I inspect the source?**
108
+ The entire tool is [one Bash file](claude-pace.sh). Read it before you install it.
109
+
90
110
  ## Also by the Author
91
111
 
92
112
  [**diffpane**](https://github.com/Astro-Han/diffpane) - Real-time TUI diff viewer for AI coding agents. See what Claude Code changes as it happens.
@@ -94,3 +114,5 @@ Cache files live in a private per-user directory (`$XDG_RUNTIME_DIR/claude-pace`
94
114
  ## License
95
115
 
96
116
  MIT
117
+
118
+ *Last updated: 2026-04-13 · v0.8.0*
package/claude-pace.sh CHANGED
@@ -94,6 +94,7 @@ _CD="" CACHE_OK=0
94
94
  for _BASE in "${XDG_RUNTIME_DIR:-}" "${HOME}/.cache"; do
95
95
  [ -n "$_BASE" ] || continue
96
96
  _CAND="${_BASE%/}/claude-pace"
97
+ # shellcheck disable=SC2174 # -p only creates leaf here; parent already exists
97
98
  [ -e "$_CAND" ] || mkdir -p -m 700 "$_CAND" 2>/dev/null || continue
98
99
  _cache_dir_ok "$_CAND" || continue
99
100
  _CD="$_CAND"
@@ -148,7 +149,7 @@ for ((i = F; i < 10; i++)); do BAR+='░'; done
148
149
  # Atomic write: write to a temp file first, then mv to avoid partial reads.
149
150
  BR="" FC=0 AD=0 DL=0
150
151
  if [[ "$CACHE_OK" == "1" ]]; then
151
- GC="${_CD}/claude-sl-git-${DIR//[^a-zA-Z0-9]/_}"
152
+ GC="${_CD}/claude-sl-git-$(printf '%s' "$DIR" | { shasum 2>/dev/null || sha1sum; } | cut -c1-16)"
152
153
  if _stale "$GC" 5; then
153
154
  if _collect_git_info; then
154
155
  _write_cache_record "$GC" "$BR" "$FC" "$AD" "$DL"
@@ -193,137 +194,16 @@ elif [[ "$IS_WT" == "1" ]]; then
193
194
  ((${#L1R} > 25)) && L1R="${L1R:0:25}…"
194
195
  fi
195
196
 
196
- # Usage data: prefer stdin rate_limits (CC >=2.1.80), fall back to API polling
197
+ # Usage data: read stdin rate_limits when available, otherwise show session cost.
197
198
  SHOW_COST=0
198
199
  if [[ "$HAS_RL" == "1" ]]; then
199
200
  # Stdin path: real-time, no network. U5/U7 already set by jq read above.
200
201
  # Guard: resets_at=0 means field missing, leave RM empty so _pace/_rc skip it
201
202
  RM5=$(_minutes_until "$R5")
202
203
  RM7=$(_minutes_until "$R7")
203
- # Extra usage (XO/XU/XL) only available via API fallback; stdin lacks this data
204
204
  else
205
- # ── API fallback (remove when CC <2.1.80 no longer supported) ──
206
- UC="" UL=""
207
- [[ "$CACHE_OK" == "1" ]] && {
208
- UC="${_CD}/claude-sl-usage"
209
- UL="${_CD}/claude-sl-usage.lock"
210
- }
211
-
212
- # ── _get_token: credential source priority ──
213
- # Check in order: env var → macOS Keychain → credentials file → secret-tool (Linux).
214
- _get_token() {
215
- [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ] && {
216
- printf '%s' "$CLAUDE_CODE_OAUTH_TOKEN"
217
- return
218
- }
219
- local b=""
220
- command -v security >/dev/null &&
221
- b=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null)
222
- [ -z "$b" ] && [ -f ~/.claude/.credentials.json ] && b=$(<~/.claude/.credentials.json)
223
- [ -z "$b" ] && command -v secret-tool >/dev/null &&
224
- b=$(timeout 2 secret-tool lookup service "Claude Code-credentials" 2>/dev/null)
225
- [ -n "$b" ] && jq -j '.claudeAiOauth.accessToken//empty' <<<"$b" 2>/dev/null
226
- }
227
-
228
- # ── _fetch_usage_api: direct API read into usage globals ──
229
- # Used by both the cached background refresh path and the no-cache fallback.
230
- _fetch_usage_api() {
231
- local tk resp
232
- # Command substitution strips trailing newlines. Append a sentinel byte only
233
- # on success so malformed tokens with a trailing LF remain detectable here.
234
- tk=$(_get_token && printf '\001') || return 1
235
- [[ "$tk" == *$'\001' ]] || return 1
236
- tk=${tk%$'\001'}
237
- [ -n "$tk" ] || return 1
238
- # OAuth bearer tokens must remain a single header line. Reject malformed
239
- # credentials up front instead of letting curl parse injected CR/LF bytes.
240
- case "$tk" in *$'\n'* | *$'\r'*) return 1 ;; esac
241
- # Feed headers through process substitution so the bearer token stays out
242
- # of curl argv while preserving literal bytes like quotes and backslashes.
243
- resp=$(curl -s --max-time 3 \
244
- -H @<(printf 'Authorization: Bearer %s\n' "$tk"
245
- printf '%s\n' 'anthropic-beta: oauth-2025-04-20'
246
- printf '%s\n' 'Content-Type: application/json') \
247
- "https://api.anthropic.com/api/oauth/usage" 2>/dev/null)
248
- IFS=$'\t' read -r U5 U7 XO XU XL RM5 RM7 < <(jq -r '
249
- def rmins: if . and . != "" then (sub("\\.[0-9]+"; "") | sub("\\+00:00$"; "Z") | fromdateiso8601) - (now|floor) | ./60|floor | if .<0 then 0 else . end else null end;
250
- [(.five_hour.utilization|floor),(.seven_day.utilization|floor),
251
- (if .extra_usage.is_enabled then 1 else 0 end),
252
- (.extra_usage.used_credits//0|floor),(.extra_usage.monthly_limit//0|floor),
253
- (.five_hour.resets_at|rmins//""),(.seven_day.resets_at|rmins//"")]|@tsv' \
254
- <<<"$resp" 2>/dev/null) || return 1
255
- }
256
-
257
- # ── _fetch_usage: background stale-while-revalidate fetch ──
258
- # Runs in a subshell (&) so the main process returns immediately with cached data.
259
- # On API failure, writes placeholder values once so the UI stays stable and
260
- # avoids repeated refresh attempts until the cache TTL expires.
261
- _fetch_usage() {
262
- (
263
- trap 'rm -f "$UL"' EXIT
264
- if _fetch_usage_api; then
265
- _write_cache_record "$UC" "$U5" "$U7" "$XO" "$XU" "$XL" "$RM5" "$RM7"
266
- else
267
- if [ ! -f "$UC" ] || [[ $(head -c2 "$UC") == -- ]]; then
268
- _write_cache_record "$UC" "--" "--" "0" "0" "0" "" ""
269
- fi
270
- fi
271
- ) &
272
- }
273
-
274
- # ── Lock mechanism (noclobber mutex) ──
275
- # `set -o noclobber` makes `>` fail atomically if the file already exists,
276
- # providing a lock without external tools. The stale-lock check (10s) ensures
277
- # a crashed worker can't block refreshes indefinitely.
278
- if [[ "$CACHE_OK" == "1" ]] && _stale "$UC" 300; then
279
- if (
280
- set -o noclobber
281
- echo $$ >"$UL"
282
- ) 2>/dev/null; then
283
- _fetch_usage
284
- elif [ -f "$UL" ] && _stale "$UL" 10; then
285
- rm -f "$UL"
286
- (
287
- set -o noclobber
288
- echo $$ >"$UL"
289
- ) 2>/dev/null && _fetch_usage
290
- fi
291
- fi
292
-
293
- # ── Read cache + drift correction ──
294
- # The cache stores countdown minutes at write time; subtract elapsed seconds
295
- # (in whole minutes) since the file was written to keep the countdown accurate
296
- # between 300s refresh cycles without a network call.
297
- U5="--" U7="--" XO=0 XU=0 XL=0 RM5="" RM7=""
298
- if [[ "$CACHE_OK" == "1" ]]; then
299
- if _load_cache_record_file "$UC"; then
300
- U5=${CACHE_FIELDS[0]:---}
301
- U7=${CACHE_FIELDS[1]:---}
302
- XO=${CACHE_FIELDS[2]:-0}
303
- XU=${CACHE_FIELDS[3]:-0}
304
- XL=${CACHE_FIELDS[4]:-0}
305
- RM5=${CACHE_FIELDS[5]:-}
306
- RM7=${CACHE_FIELDS[6]:-}
307
- fi
308
- if [[ "$RM5" =~ ^[0-9]+$ ]] && [ -f "$UC" ]; then
309
- _CA=$((NOW - $(stat -f%m "$UC" 2>/dev/null || stat -c%Y "$UC" 2>/dev/null || echo "$NOW")))
310
- RM5=$((RM5 - _CA / 60))
311
- ((RM5 < 0)) && RM5=0
312
- [[ "$RM7" =~ ^[0-9]+$ ]] && {
313
- RM7=$((RM7 - _CA / 60))
314
- ((RM7 < 0)) && RM7=0
315
- }
316
- fi
317
- [ ! -f "$UC" ] && SHOW_COST=1
318
- elif ! _fetch_usage_api; then
319
- SHOW_COST=1
320
- fi
321
- U5=${U5%%.*} U7=${U7%%.*} XU=${XU%%.*} XL=${XL%%.*}
322
- # Reject cache corruption or malformed API data before arithmetic formatting.
323
- [[ "$XO" =~ ^[01]$ ]] || XO=0
324
- [[ "$XU" =~ ^[0-9]+$ ]] || XU=0
325
- [[ "$XL" =~ ^[0-9]+$ ]] || XL=0
326
- # ── End API fallback ──
205
+ U5="--" U7="--" RM5="" RM7=""
206
+ SHOW_COST=1
327
207
  fi
328
208
 
329
209
  # Combined usage formatter: used% [pace delta] (countdown)
@@ -353,8 +233,6 @@ _usage() {
353
233
  }
354
234
 
355
235
  # ── Output Assembly (symmetric single-pipe alignment) ──
356
- # Default XO/XU/XL for stdin path (extra usage only available via API fallback).
357
- : "${XO:=0}" "${XU:=0}" "${XL:=0}"
358
236
 
359
237
  # Build plain-text left sections for width measurement (no ANSI codes).
360
238
  L1_PLAIN="${MODEL} ${EF}"
@@ -373,10 +251,7 @@ L1="${C}${MODEL} ${EF}${N}${PAD1} ${D}|${N} ${L1R}"
373
251
 
374
252
  # Line 2: bar pct% CL | 5h used% ... 7d used% ...
375
253
  L2="${BC}${BAR}${N} ${PCT}% ${CL}${PAD2} ${D}|${N} 5h $(_usage "$U5" "$RM5" 300) 7d $(_usage "$U7" "$RM7" 10080)"
376
- # Extra usage: only when enabled and has actual spending (API fallback only)
377
- [ "$XO" = 1 ] && ((XU > 0)) &&
378
- printf -v _XS " ${Y}\$%d.%02d${N}/\$%d.%02d" $((XU / 100)) $((XU % 100)) $((XL / 100)) $((XL % 100)) && L2+="$_XS"
379
- # Session cost: only when this run has no readable usage cache data.
254
+ # Session cost: only when usage data is unavailable in stdin.
380
255
  if [[ "$SHOW_COST" == "1" ]]; then
381
256
  printf -v _CS "\$%.2f" "$COST" 2>/dev/null
382
257
  [[ "$_CS" != "\$0.00" ]] && L2+=" $_CS"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-pace",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "A statusline for Claude Code. Pure Bash + jq, single file.",
5
5
  "bin": {
6
6
  "claude-pace": "cli.js"
@@ -24,7 +24,7 @@
24
24
  "license": "MIT",
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "https://github.com/Astro-Han/claude-pace"
27
+ "url": "git+https://github.com/Astro-Han/claude-pace.git"
28
28
  },
29
29
  "engines": {
30
30
  "node": ">=16"