claude-pace 0.6.2
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.
- package/README.md +91 -0
- package/claude-pace.sh +275 -0
- package/cli.js +70 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Claude Pace
|
|
2
|
+
|
|
3
|
+
Know your quota before you hit the wall. A statusline for Claude Code — single Bash file, zero npm.
|
|
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.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
- **⇣15%** green = you've used 15% less than expected. Headroom. Keep going.
|
|
10
|
+
- **⇡15%** red = you're burning 15% faster than sustainable. Slow down.
|
|
11
|
+
- **15%** / **20%** = used in the 5h and 7d windows. **3h** = resets in 3 hours.
|
|
12
|
+
- Top line: model, effort, project `(branch)`, `3f +24 -7` = git diff stats
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Requires `jq`. Node.js is only needed for install, not runtime.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx claude-pace
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Restart Claude Code. Done.
|
|
23
|
+
|
|
24
|
+
<details>
|
|
25
|
+
<summary>Other methods</summary>
|
|
26
|
+
|
|
27
|
+
**Plugin:**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
claude plugin marketplace add Astro-Han/claude-pace
|
|
31
|
+
claude plugin install claude-pace
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then inside Claude Code, type `/claude-pace:setup`.
|
|
35
|
+
|
|
36
|
+
**Manual:**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
curl -o ~/.claude/statusline.sh \
|
|
40
|
+
https://raw.githubusercontent.com/Astro-Han/claude-pace/main/claude-pace.sh
|
|
41
|
+
chmod +x ~/.claude/statusline.sh
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Add to `~/.claude/settings.json`:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"statusLine": {
|
|
49
|
+
"type": "command",
|
|
50
|
+
"command": "~/.claude/statusline.sh"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Restart Claude Code. Done.
|
|
56
|
+
|
|
57
|
+
</details>
|
|
58
|
+
|
|
59
|
+
To remove: delete the `statusLine` block from `~/.claude/settings.json`.
|
|
60
|
+
|
|
61
|
+
## How It Compares
|
|
62
|
+
|
|
63
|
+
| | claude-pace | Node.js/TypeScript statuslines | Rust/Go statuslines |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| Runtime | `jq` | Node.js 18+ / npm | Compiled binary |
|
|
66
|
+
| Codebase | Single file | 1000+ lines + node_modules | Compiled, not inspectable |
|
|
67
|
+
| Execution | ~10ms, 3% of refresh cycle | ~90ms, 30% of refresh cycle | ~5ms (est.) |
|
|
68
|
+
| Memory | ~2 MB | ~57 MB | ~3 MB (est.) |
|
|
69
|
+
| Failure modes | Read-only, worst case prints "Claude" | Runtime dependency, package manager | Generally stable |
|
|
70
|
+
| Pace tracking | Usage rate vs time remaining | Trend-only or none | None |
|
|
71
|
+
|
|
72
|
+
Execution and memory measured on Apple Silicon, 300 runs, same stdin JSON. Rust/Go values are estimates.
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
## Under the Hood
|
|
77
|
+
|
|
78
|
+
Claude Code polls the statusline every ~300ms:
|
|
79
|
+
|
|
80
|
+
| Data | Source | Cache |
|
|
81
|
+
|------|--------|-------|
|
|
82
|
+
| Model, context, cost | stdin JSON (single `jq` call) | None needed |
|
|
83
|
+
| Quota (5h, 7d, pace) | stdin `rate_limits` (CC >= 2.1.80) | None needed (real-time) |
|
|
84
|
+
| Quota fallback | Anthropic Usage API (CC < 2.1.80) | `/tmp`, 300s TTL, async background refresh |
|
|
85
|
+
| Git branch + diff | `git` commands | `/tmp`, 5s TTL |
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/claude-pace.sh
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Claude Code statusline plugin
|
|
3
|
+
# Line1: model (ctx) effort | project (branch) Nf +A -D
|
|
4
|
+
# Line2: bar PCT% CL | 5h used% [⇡⇣pace] countdown 7d used% [⇡⇣pace] countdown
|
|
5
|
+
|
|
6
|
+
# Disable glob expansion so unquoted vars with wildcards (e.g. DIR paths)
|
|
7
|
+
# are never accidentally expanded into filename lists.
|
|
8
|
+
set -f
|
|
9
|
+
input=$(cat)
|
|
10
|
+
[ -z "$input" ] && {
|
|
11
|
+
echo "Claude"
|
|
12
|
+
exit 0
|
|
13
|
+
}
|
|
14
|
+
command -v jq >/dev/null || {
|
|
15
|
+
echo "Claude [needs jq]"
|
|
16
|
+
exit 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# ── Colors & Utilities ──
|
|
20
|
+
# C=Cyan G=Green Y=Yellow R=Red D=Dim N=Normal (reset)
|
|
21
|
+
C='\033[36m' G='\033[32m' Y='\033[33m' R='\033[31m' D='\033[2m' N='\033[0m'
|
|
22
|
+
NOW=$(date +%s)
|
|
23
|
+
# Returns true (exit 0) when file is missing or older than $2 seconds.
|
|
24
|
+
_stale() { [ ! -f "$1" ] || [ $((NOW - $(stat -f%m "$1" 2>/dev/null || stat -c%Y "$1" 2>/dev/null || echo 0))) -gt "$2" ]; }
|
|
25
|
+
|
|
26
|
+
# ── Parse stdin + settings in one jq call ──
|
|
27
|
+
# Fields: MODEL DIR PCT CTX COST EFF HAS_RL U5 U7 R5 R7
|
|
28
|
+
HAS_RL=0
|
|
29
|
+
IFS=$'\t' read -r MODEL DIR PCT CTX COST EFF HAS_RL U5 U7 R5 R7 < <(
|
|
30
|
+
jq -r --slurpfile cfg <(cat ~/.claude/settings.json 2>/dev/null || echo '{}') \
|
|
31
|
+
'[(.model.display_name//"?"),(.workspace.project_dir//"."),
|
|
32
|
+
(.context_window.used_percentage//0|floor),(.context_window.context_window_size//0),
|
|
33
|
+
(.cost.total_cost_usd//0),
|
|
34
|
+
($cfg[0].effortLevel//"default"),
|
|
35
|
+
(if .rate_limits then 1 else 0 end),
|
|
36
|
+
(.rate_limits.five_hour.used_percentage//null|if type=="number" then floor else "--" end),
|
|
37
|
+
(.rate_limits.seven_day.used_percentage//null|if type=="number" then floor else "--" end),
|
|
38
|
+
(.rate_limits.five_hour.resets_at//0),
|
|
39
|
+
(.rate_limits.seven_day.resets_at//0)]|@tsv' <<<"$input"
|
|
40
|
+
)
|
|
41
|
+
case "${EFF:-default}" in high) EF='●' ;; low) EF='◔' ;; *) EF='◑' ;; esac
|
|
42
|
+
|
|
43
|
+
# ── Context label (needed by MODEL_SHORT and line 2) ──
|
|
44
|
+
if ((CTX >= 1000000)); then
|
|
45
|
+
CL="$((CTX / 1000000))M"
|
|
46
|
+
elif ((CTX > 0)); then
|
|
47
|
+
CL="$((CTX / 1000))K"
|
|
48
|
+
else CL=""; fi
|
|
49
|
+
|
|
50
|
+
# ── MODEL_SHORT: strip redundant context label ──
|
|
51
|
+
MODEL=${MODEL/ context)/)}
|
|
52
|
+
[[ "$CTX" -gt 0 && "$MODEL" != *"("* ]] && MODEL="${MODEL} (${CL})"
|
|
53
|
+
# Truncate long model names to keep padding within 0-5 chars.
|
|
54
|
+
_ML="${MODEL} ${EF}"
|
|
55
|
+
((${#_ML} > 22)) && MODEL="${MODEL:0:$((22 - 2 - ${#EF}))}…"
|
|
56
|
+
|
|
57
|
+
# ── Progress Bar ──
|
|
58
|
+
F=$((PCT / 10))
|
|
59
|
+
((F < 0)) && F=0
|
|
60
|
+
((F > 10)) && F=10
|
|
61
|
+
if ((PCT >= 90)); then BC=$R; elif ((PCT >= 70)); then BC=$Y; else BC=$G; fi
|
|
62
|
+
BAR=""
|
|
63
|
+
for ((i = 0; i < F; i++)); do BAR+='█'; done
|
|
64
|
+
for ((i = F; i < 10; i++)); do BAR+='░'; done
|
|
65
|
+
|
|
66
|
+
# ── Git Info (5s cache, atomic write) ──
|
|
67
|
+
# Cache key encodes DIR so concurrent sessions in different repos don't clash.
|
|
68
|
+
# Atomic write: write to a temp file first, then mv to avoid partial reads.
|
|
69
|
+
GC="/tmp/claude-sl-git-${DIR//[^a-zA-Z0-9]/_}"
|
|
70
|
+
if _stale "$GC" 5; then
|
|
71
|
+
if git -C "$DIR" rev-parse --git-dir >/dev/null 2>&1; then
|
|
72
|
+
_BR=$(git -C "$DIR" --no-optional-locks branch --show-current 2>/dev/null)
|
|
73
|
+
_FC=0 _AD=0 _DL=0
|
|
74
|
+
while IFS=$'\t' read -r a d _; do
|
|
75
|
+
# Skip binary files (reported as "-" instead of a number).
|
|
76
|
+
[[ "$a" =~ ^[0-9]+$ ]] && {
|
|
77
|
+
_FC=$((_FC + 1))
|
|
78
|
+
_AD=$((_AD + a))
|
|
79
|
+
_DL=$((_DL + d))
|
|
80
|
+
}
|
|
81
|
+
done < <(git -C "$DIR" --no-optional-locks diff HEAD --numstat 2>/dev/null)
|
|
82
|
+
_TMP=$(mktemp /tmp/claude-sl-g-XXXXXX)
|
|
83
|
+
echo "${_BR}|${_FC}|${_AD}|${_DL}" >"$_TMP" && mv "$_TMP" "$GC"
|
|
84
|
+
else
|
|
85
|
+
echo "|||" >"$GC"
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
IFS='|' read -r BR FC AD DL <"$GC" 2>/dev/null
|
|
89
|
+
|
|
90
|
+
# ── Project Name + Line 1 Right Section ──
|
|
91
|
+
# Extract project name. Worktree: save repo name explicitly.
|
|
92
|
+
PN="${DIR##*/}"
|
|
93
|
+
IS_WT=0 _REPO=""
|
|
94
|
+
if [[ "${DIR/#$HOME/\~}" =~ /([^/]+)/\.claude/worktrees/([^/]+) ]]; then
|
|
95
|
+
IS_WT=1
|
|
96
|
+
_REPO="${BASH_REMATCH[1]}"
|
|
97
|
+
_WT_NAME="${BASH_REMATCH[2]}"
|
|
98
|
+
PN="$_REPO"
|
|
99
|
+
fi
|
|
100
|
+
((${#PN} > 25)) && PN="${PN:0:25}…"
|
|
101
|
+
|
|
102
|
+
# Format: project (branch) [git stats]
|
|
103
|
+
L1R="$PN"
|
|
104
|
+
if [ -n "$BR" ]; then
|
|
105
|
+
((${#BR} > 35)) && BR="${BR:0:35}…"
|
|
106
|
+
L1R+=" (${BR})"
|
|
107
|
+
((FC > 0)) 2>/dev/null && L1R+=" ${FC}f ${G}+${AD}${N} ${R}-${DL}${N}"
|
|
108
|
+
elif [[ "$IS_WT" == "1" ]]; then
|
|
109
|
+
# Detached HEAD in worktree: show repo/worktree to preserve identity
|
|
110
|
+
L1R="${_REPO}/${_WT_NAME}"
|
|
111
|
+
((${#L1R} > 25)) && L1R="${L1R:0:25}…"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Usage data: prefer stdin rate_limits (CC >=2.1.80), fall back to API polling
|
|
115
|
+
SHOW_COST=0
|
|
116
|
+
if [[ "$HAS_RL" == "1" ]]; then
|
|
117
|
+
# Stdin path: real-time, no network. U5/U7 already set by jq read above.
|
|
118
|
+
# Guard: resets_at=0 means field missing, leave RM empty so _pace/_rc skip it
|
|
119
|
+
RM5=""
|
|
120
|
+
((R5 > 0)) && {
|
|
121
|
+
RM5=$(((R5 - NOW) / 60))
|
|
122
|
+
((RM5 < 0)) && RM5=0
|
|
123
|
+
}
|
|
124
|
+
RM7=""
|
|
125
|
+
((R7 > 0)) && {
|
|
126
|
+
RM7=$(((R7 - NOW) / 60))
|
|
127
|
+
((RM7 < 0)) && RM7=0
|
|
128
|
+
}
|
|
129
|
+
# Extra usage (XO/XU/XL) only available via API fallback; stdin lacks this data
|
|
130
|
+
else
|
|
131
|
+
# ── API fallback (remove when CC <2.1.80 no longer supported) ──
|
|
132
|
+
UC="/tmp/claude-sl-usage" UL="/tmp/claude-sl-usage.lock"
|
|
133
|
+
|
|
134
|
+
# ── _get_token: credential source priority ──
|
|
135
|
+
# Check in order: env var → macOS Keychain → credentials file → secret-tool (Linux).
|
|
136
|
+
_get_token() {
|
|
137
|
+
[ -n "$CLAUDE_CODE_OAUTH_TOKEN" ] && {
|
|
138
|
+
echo "$CLAUDE_CODE_OAUTH_TOKEN"
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
local b=""
|
|
142
|
+
command -v security >/dev/null &&
|
|
143
|
+
b=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null)
|
|
144
|
+
[ -z "$b" ] && [ -f ~/.claude/.credentials.json ] && b=$(<~/.claude/.credentials.json)
|
|
145
|
+
[ -z "$b" ] && command -v secret-tool >/dev/null &&
|
|
146
|
+
b=$(timeout 2 secret-tool lookup service "Claude Code-credentials" 2>/dev/null)
|
|
147
|
+
[ -n "$b" ] && jq -r '.claudeAiOauth.accessToken//empty' <<<"$b" 2>/dev/null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# ── _fetch_usage: background stale-while-revalidate fetch ──
|
|
151
|
+
# Runs in a subshell (&) so the main process returns immediately with cached data.
|
|
152
|
+
# On API failure, touches the cache file to reset the 300s TTL and avoid a
|
|
153
|
+
# retry storm; placeholder "--" values leave the display unchanged.
|
|
154
|
+
_fetch_usage() {
|
|
155
|
+
(
|
|
156
|
+
trap 'rm -f "$UL"' EXIT
|
|
157
|
+
TK=$(_get_token)
|
|
158
|
+
[ -z "$TK" ] && return
|
|
159
|
+
RESP=$(curl -s --max-time 3 \
|
|
160
|
+
-H "Authorization: Bearer $TK" -H "anthropic-beta: oauth-2025-04-20" \
|
|
161
|
+
-H "Content-Type: application/json" \
|
|
162
|
+
"https://api.anthropic.com/api/oauth/usage" 2>/dev/null)
|
|
163
|
+
IFS=$'\t' read -r F5 S7 EX EU EL RM5 RM7 < <(jq -r '
|
|
164
|
+
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;
|
|
165
|
+
[(.five_hour.utilization|floor),(.seven_day.utilization|floor),
|
|
166
|
+
(if .extra_usage.is_enabled then 1 else 0 end),
|
|
167
|
+
(.extra_usage.used_credits//0|floor),(.extra_usage.monthly_limit//0|floor),
|
|
168
|
+
(.five_hour.resets_at|rmins//""),(.seven_day.resets_at|rmins//"")]|@tsv' \
|
|
169
|
+
<<<"$RESP" 2>/dev/null) || {
|
|
170
|
+
[ ! -f "$UC" ] || [[ $(head -c2 "$UC") == -- ]] && echo "--|--|0|0|0||" >"$UC"
|
|
171
|
+
touch "$UC"
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
TMP=$(mktemp /tmp/claude-sl-u-XXXXXX)
|
|
175
|
+
echo "${F5}|${S7}|${EX}|${EU}|${EL}|${RM5}|${RM7}" >"$TMP" && mv "$TMP" "$UC"
|
|
176
|
+
) &
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# ── Lock mechanism (noclobber mutex) ──
|
|
180
|
+
# `set -o noclobber` makes `>` fail atomically if the file already exists,
|
|
181
|
+
# providing a lock without external tools. The stale-lock check (10s) ensures
|
|
182
|
+
# a crashed worker can't block refreshes indefinitely.
|
|
183
|
+
if _stale "$UC" 300; then
|
|
184
|
+
if (
|
|
185
|
+
set -o noclobber
|
|
186
|
+
echo $$ >"$UL"
|
|
187
|
+
) 2>/dev/null; then
|
|
188
|
+
_fetch_usage
|
|
189
|
+
elif [ -f "$UL" ] && _stale "$UL" 10; then
|
|
190
|
+
rm -f "$UL"
|
|
191
|
+
(
|
|
192
|
+
set -o noclobber
|
|
193
|
+
echo $$ >"$UL"
|
|
194
|
+
) 2>/dev/null && _fetch_usage
|
|
195
|
+
fi
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# ── Read cache + drift correction ──
|
|
199
|
+
# The cache stores countdown minutes at write time; subtract elapsed seconds
|
|
200
|
+
# (in whole minutes) since the file was written to keep the countdown accurate
|
|
201
|
+
# between 300s refresh cycles without a network call.
|
|
202
|
+
U5="--" U7="--" XO=0 XU=0 XL=0 RM5="" RM7=""
|
|
203
|
+
[ -f "$UC" ] && IFS='|' read -r U5 U7 XO XU XL RM5 RM7 <"$UC"
|
|
204
|
+
U5=${U5%%.*} U7=${U7%%.*} XU=${XU%%.*} XL=${XL%%.*}
|
|
205
|
+
if [[ "$RM5" =~ ^[0-9]+$ ]] && [ -f "$UC" ]; then
|
|
206
|
+
_CA=$((NOW - $(stat -f%m "$UC" 2>/dev/null || stat -c%Y "$UC" 2>/dev/null || echo "$NOW")))
|
|
207
|
+
RM5=$((RM5 - _CA / 60))
|
|
208
|
+
((RM5 < 0)) && RM5=0
|
|
209
|
+
[[ "$RM7" =~ ^[0-9]+$ ]] && {
|
|
210
|
+
RM7=$((RM7 - _CA / 60))
|
|
211
|
+
((RM7 < 0)) && RM7=0
|
|
212
|
+
}
|
|
213
|
+
fi
|
|
214
|
+
[ ! -f "$UC" ] && SHOW_COST=1
|
|
215
|
+
# ── End API fallback ──
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
# Combined usage formatter: used% [pace delta] (countdown)
|
|
219
|
+
_usage() {
|
|
220
|
+
local u="${1:---}" rm="$2" w="$3"
|
|
221
|
+
if [[ ! "$u" =~ ^[0-9]+$ ]]; then
|
|
222
|
+
printf "%s" "$u"
|
|
223
|
+
else
|
|
224
|
+
if ((u >= 90)); then printf "${R}%d%%${N}" "$u"; elif ((u >= 70)); then printf "${Y}%d%%${N}" "$u"; else printf "${G}%d%%${N}" "$u"; fi
|
|
225
|
+
if [[ "$rm" =~ ^[0-9]+$ ]] && ((rm <= w)); then
|
|
226
|
+
# Pace delta: positive = over pace (overspend), negative = under pace (surplus).
|
|
227
|
+
local d=$((u - (w - rm) * 100 / w))
|
|
228
|
+
((d > 0)) && printf " ${R}⇡%d%%${N}" "$d"
|
|
229
|
+
((d < 0)) && printf " ${G}⇣%d%%${N}" "${d#-}"
|
|
230
|
+
fi
|
|
231
|
+
fi
|
|
232
|
+
[[ "$rm" =~ ^[0-9]+$ ]] || return
|
|
233
|
+
((rm >= 1440)) && {
|
|
234
|
+
printf " ${D}%dd${N}" $((rm / 1440))
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
((rm >= 60)) && {
|
|
238
|
+
printf " ${D}%dh${N}" $((rm / 60))
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
printf " ${D}%dm${N}" "$rm"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# ── Output Assembly (symmetric single-pipe alignment) ──
|
|
245
|
+
# Default XO/XU/XL for stdin path (extra usage only available via API fallback).
|
|
246
|
+
: "${XO:=0}" "${XU:=0}" "${XL:=0}"
|
|
247
|
+
|
|
248
|
+
# Build plain-text left sections for width measurement (no ANSI codes).
|
|
249
|
+
L1_PLAIN="${MODEL} ${EF}"
|
|
250
|
+
L2_PLAIN="${BAR} ${PCT}% ${CL}"
|
|
251
|
+
# Pad shorter side so | aligns on both lines.
|
|
252
|
+
W1=${#L1_PLAIN} W2=${#L2_PLAIN}
|
|
253
|
+
PAD1="" PAD2=""
|
|
254
|
+
if ((W1 > W2)); then
|
|
255
|
+
printf -v PAD2 "%*s" $((W1 - W2)) ""
|
|
256
|
+
elif ((W2 > W1)); then
|
|
257
|
+
printf -v PAD1 "%*s" $((W2 - W1)) ""
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
# Line 1: model (context) effort | project (branch) git-stats
|
|
261
|
+
L1="${C}${MODEL} ${EF}${N}${PAD1} ${D}|${N} ${L1R}"
|
|
262
|
+
|
|
263
|
+
# Line 2: bar pct% CL | 5h used% ... 7d used% ...
|
|
264
|
+
L2="${BC}${BAR}${N} ${PCT}% ${CL}${PAD2} ${D}|${N} 5h $(_usage "$U5" "$RM5" 300) 7d $(_usage "$U7" "$RM7" 10080)"
|
|
265
|
+
# Extra usage: only when enabled and has actual spending (API fallback only)
|
|
266
|
+
[ "$XO" = 1 ] && ((XU > 0)) &&
|
|
267
|
+
printf -v _XS " ${Y}\$%d.%02d${N}/\$%d.%02d" $((XU / 100)) $((XU % 100)) $((XL / 100)) $((XL % 100)) && L2+="$_XS"
|
|
268
|
+
# Session cost: only when /tmp/claude-sl-usage does not exist
|
|
269
|
+
if [[ "$SHOW_COST" == "1" ]]; then
|
|
270
|
+
printf -v _CS "\$%.2f" "$COST" 2>/dev/null
|
|
271
|
+
[[ "$_CS" != "\$0.00" ]] && L2+=" $_CS"
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
echo -e "$L1"
|
|
275
|
+
echo -e "$L2"
|
package/cli.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// One-step installer for claude-pace statusline.
|
|
4
|
+
// Copies the Bash script to ~/.claude/ and configures settings.json.
|
|
5
|
+
// After install, Node is not needed. The statusline runs as pure Bash + jq.
|
|
6
|
+
|
|
7
|
+
const { execSync } = require("child_process");
|
|
8
|
+
const { readFileSync, writeFileSync, copyFileSync, chmodSync, renameSync, existsSync, mkdirSync } = require("fs");
|
|
9
|
+
const { join } = require("path");
|
|
10
|
+
const { homedir, platform } = require("os");
|
|
11
|
+
|
|
12
|
+
// Platform guard: statusline is Bash, won't run on Windows
|
|
13
|
+
if (platform() === "win32") {
|
|
14
|
+
console.error("Error: claude-pace requires Bash and only works on macOS/Linux.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check jq dependency
|
|
19
|
+
try {
|
|
20
|
+
execSync("command -v jq", { stdio: "ignore" });
|
|
21
|
+
} catch {
|
|
22
|
+
console.error("Error: jq is required but not found.");
|
|
23
|
+
console.error("Install it: brew install jq (macOS) or apt install jq (Linux)");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const claudeDir = join(homedir(), ".claude");
|
|
28
|
+
const dest = join(claudeDir, "statusline.sh");
|
|
29
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
30
|
+
|
|
31
|
+
// Ensure ~/.claude/ exists
|
|
32
|
+
if (!existsSync(claudeDir)) {
|
|
33
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Copy statusline script and make it executable
|
|
37
|
+
copyFileSync(join(__dirname, "claude-pace.sh"), dest);
|
|
38
|
+
chmodSync(dest, 0o755);
|
|
39
|
+
|
|
40
|
+
// Read existing settings (empty file treated as fresh)
|
|
41
|
+
let settings = {};
|
|
42
|
+
if (existsSync(settingsPath)) {
|
|
43
|
+
const raw = readFileSync(settingsPath, "utf8").trim();
|
|
44
|
+
if (raw) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
// settings.json must be a plain object
|
|
48
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
49
|
+
settings = parsed;
|
|
50
|
+
} else {
|
|
51
|
+
console.error("Error: ~/.claude/settings.json is not a JSON object. Fix it manually, then re-run.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
console.error("Error: ~/.claude/settings.json is not valid JSON. Fix it manually, then re-run.");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Merge statusLine config (spread preserves any future sub-fields)
|
|
62
|
+
const updating = settings.statusLine && settings.statusLine.command;
|
|
63
|
+
settings.statusLine = { ...settings.statusLine, type: "command", command: "~/.claude/statusline.sh" };
|
|
64
|
+
|
|
65
|
+
// Atomic write: tmp file + rename to prevent truncation on crash
|
|
66
|
+
const tmp = settingsPath + ".tmp";
|
|
67
|
+
writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
68
|
+
renameSync(tmp, settingsPath);
|
|
69
|
+
|
|
70
|
+
console.log(updating ? "claude-pace updated. Restart Claude Code." : "claude-pace installed. Restart Claude Code to see the statusline.");
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-pace",
|
|
3
|
+
"version": "0.6.2",
|
|
4
|
+
"description": "A statusline for Claude Code. Pure Bash + jq, single file.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-pace": "cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"cli.js",
|
|
10
|
+
"claude-pace.sh"
|
|
11
|
+
],
|
|
12
|
+
"author": "Yuhan Lei (https://github.com/Astro-Han)",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"prepublishOnly": "cp ../claude-pace.sh . && cp ../README.md ."
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"claude-code",
|
|
18
|
+
"statusline",
|
|
19
|
+
"quota",
|
|
20
|
+
"usage",
|
|
21
|
+
"pace-tracking",
|
|
22
|
+
"bash"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/Astro-Han/claude-pace"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=16"
|
|
31
|
+
}
|
|
32
|
+
}
|