Repository navigation
Expand file tree
/
Copy pathfable
More file actions
executable file
·176 lines (158 loc) · 5.5 KB
/
Copy pathfable
File metadata and controls
executable file
·176 lines (158 loc) · 5.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/bin/zsh
#
# Report Claude Code usage (session, weekly all-models, weekly Fable) for one
# or more Claude accounts.
#
# Accounts are listed in ${XDG_CONFIG_HOME:-~/.config}/fable/accounts, one per
# line: a label, whitespace, then the CLAUDE_CONFIG_DIR path (may contain
# spaces; ~ is expanded). A label with no path, or a path of - or ~/.claude, is
# the default account and runs with CLAUDE_CONFIG_DIR unset (even if the calling
# shell exports one). A boilerplate file is
# written on first run.
#
# Usage: fable [label ...] (no args = all accounts)
set -euo pipefail
setopt extendedglob
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/fable"
config="$config_dir/accounts"
if [[ ! -f $config ]]; then
mkdir -p "$config_dir"
cat > "$config" <<'BOILERPLATE'
# Claude accounts to report on, one per line:
#
# <label> <CLAUDE_CONFIG_DIR path>
#
# The label is what's printed; the path is the account's config directory
# (CLAUDE_CONFIG_DIR). Omit the path (or use -) for the default account
# (~/.claude), which runs with CLAUDE_CONFIG_DIR unset. Paths may contain spaces and may
# start with ~. Blank lines and lines beginning with # are ignored.
#
# Examples:
# personal
# work ~/.claude-work
default
BOILERPLATE
print -u2 "Wrote boilerplate config to $config — edit it to add accounts."
fi
typeset -a labels dirs
while IFS= read -r line || [[ -n $line ]]; do
line="${line##[[:space:]]##}"
line="${line%%[[:space:]]##}"
[[ -z $line || $line == \#* ]] && continue
label="${line%%[[:space:]]*}"
dir="${line#$label}"
dir="${dir##[[:space:]]##}"
dir="${dir/#\~/$HOME}"
# Default account: run without CLAUDE_CONFIG_DIR.
[[ $dir == "$HOME/.claude" || $dir == "-" ]] && dir=""
labels+=("$label")
dirs+=("$dir")
done < "$config"
if (( $# > 0 )); then
typeset -a sel_labels sel_dirs
for want in "$@"; do
found=0
for i in {1..$#labels}; do
if [[ $labels[i] == $want ]]; then
sel_labels+=("$labels[i]"); sel_dirs+=("$dirs[i]"); found=1
fi
done
(( found )) || { print -u2 "Unknown account '$want' (see $config)"; exit 1; }
done
labels=("${sel_labels[@]}"); dirs=("${sel_dirs[@]}")
fi
if (( $#labels == 0 )); then
print -u2 "No accounts configured in $config"
exit 1
fi
# Format a reset time. Week rows: "Aug 7 at 1pm" -> "Aug 07 (Thu) 01:00pm", so
# rows line up. Session rows (kind=session): "Aug 25 at 6:40pm" -> "6:40pm (4h3m)".
format_resets() {
local raw=$1 kind=${2:-week} mon day hr min ampm year now epoch dow
if [[ ! $raw =~ '^([A-Z][a-z]{2}) ([0-9]{1,2}) at ([0-9]{1,2})(:([0-9]{2}))?(am|pm)$' ]]; then
print -- "$raw"
return
fi
mon=$match[1]; day=$match[2]; hr=$match[3]; min=${match[5]:-00}; ampm=$match[6]
year=$(date +%Y)
now=$(date +%s)
epoch=$(date -j -f '%b %d %Y %I:%M%p' "$mon $day $year $hr:$min$ampm" +%s 2>/dev/null) \
|| { print -- "$raw"; return; }
# Reset dates are in the future; a date that's already passed means next year.
if (( epoch < now - 86400 )); then
(( year++ ))
epoch=$(date -j -f '%b %d %Y %I:%M%p' "$mon $day $year $hr:$min$ampm" +%s 2>/dev/null) \
|| { print -- "$raw"; return; }
fi
if [[ $kind == session ]]; then
local left=$(( epoch - now )) rel
(( left < 0 )) && left=0
rel="$(( left / 3600 ))h$(( left % 3600 / 60 ))m"
printf '%d:%s%s (%s)' "$hr" "$min" "$ampm" "$rel"
return
fi
dow=$(date -j -f '%b %d %Y' "$mon $day $year" +%a 2>/dev/null) || { print -- "$raw"; return; }
printf '%s %02d (%s) %02d:%s%s' "$mon" "$day" "$dow" "$hr" "$min" "$ampm"
}
width=20
reset_color=$'\e[0m'
dim=$'\e[2m'
bold=$'\e[1m'
rc=0
# Print one usage row: "<row label> <bar> <pct>% · resets <when>".
print_row() {
local label=$1 pct=$2 resets=$3 color filled bar
filled=$(( pct * width / 100 ))
(( filled > width )) && filled=$width
if (( pct >= 85 )); then
color=$'\e[31m' # red
elif (( pct >= 60 )); then
color=$'\e[33m' # yellow
else
color=$'\e[32m' # green
fi
bar="${color}${(l:$filled::█:):-}${reset_color}${dim}${(l:$(( width - filled ))::░:):-}${reset_color}"
local tail=""
[[ -n $resets ]] && tail=" ${dim}· resets ${resets}${reset_color}"
print -- " ${(r:$row_width:)label} $bar ${(l:3:)pct}%$tail"
}
for i in {1..$#labels}; do
label=$labels[i]
dir=$dirs[i]
if [[ -n $dir ]]; then
out=$(CLAUDE_CONFIG_DIR="$dir" claude -p '/usage' 2>/dev/null) || out=""
else
out=$(env -u CLAUDE_CONFIG_DIR claude -p '/usage' 2>/dev/null) || out=""
fi
# Rows look like:
# Current session: 4% used · resets Aug 25 at 6:40pm (America/New_York)
# Current week (all models): 1% used · resets ...
# Current week (Fable): 2% used · resets ...
typeset -a row_labels row_pcts row_resets
row_labels=(); row_pcts=(); row_resets=()
for line in ${(f)out}; do
[[ $line == Current* ]] || continue
[[ $line =~ '^Current ([a-z]+)( \(([^)]*)\))?: ([0-9]+)% used' ]] || continue
rl=$match[1]
[[ -n $match[3] ]] && rl="$rl ${match[3]% models}"
row_labels+=("$rl")
row_pcts+=("$match[4]")
resets=""
if [[ $line =~ 'resets ([^(]*[^( ])' ]]; then
resets=$(format_resets "$match[1]" "$rl")
fi
row_resets+=("$resets")
done
print -- "${bold}${label}${reset_color}"
if (( $#row_labels == 0 )); then
print -- " ${dim}(couldn't get usage from ${dir:-default account})${reset_color}"
rc=1
continue
fi
row_width=0
for rl in "${row_labels[@]}"; do (( $#rl > row_width )) && row_width=$#rl; done
for j in {1..$#row_labels}; do
print_row "$row_labels[j]" "$row_pcts[j]" "$row_resets[j]"
done
done
exit $rc