@zalom/plastic 1.11.0 → 1.13.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.
- package/hooks/statusline +300 -155
- package/package.json +1 -1
- package/scripts/doctor.rb +68 -19
- package/scripts/lib/bridge.rb +4 -3
- package/scripts/lib/doctor_core.rb +44 -10
- package/scripts/lib/doctor_exclusions.rb +93 -0
- package/scripts/lib/hook_registry.rb +104 -0
- package/scripts/lib/installer_core.rb +73 -20
- package/scripts/lib/rule_catalog.rb +62 -0
- package/scripts/maintenance-run +138 -4
- package/scripts/scaffold-intent +2 -2
- package/skills/conventions/SKILL.md +1 -1
- package/skills/conventions/references/gates-and-enforcement.md +9 -0
- package/skills/conventions/references/maintenance-and-revisions.md +38 -4
- package/skills/doctor/SKILL.md +28 -0
package/hooks/statusline
CHANGED
|
@@ -1,57 +1,258 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# plastic-hook-version:
|
|
2
|
+
# plastic-hook-version: 4.0.0
|
|
3
3
|
# StatusLine hook - Plastic owns the full statusline (no chaining).
|
|
4
|
-
# Renders: {model} -
|
|
5
|
-
#
|
|
4
|
+
# Renders: {model} - ctx {pct}% ({used}/{size}) - 5h {p}% - 7d {p}% - ${cost}
|
|
5
|
+
# - claudish {n} rw / {tok} - Plastic {version}[ -> {next}] - {path}
|
|
6
|
+
# Every segment but claudish and the Plastic version comes from the stdin payload.
|
|
7
|
+
# Pure bash (macOS 3.2 compatible). No ruby, no jq. Reads stdin once; small file reads
|
|
8
|
+
# only. The stdin JSON is walked in a single awk pass (below); nothing else in this
|
|
9
|
+
# script forks a second process per field, which is what keeps a render cheap.
|
|
6
10
|
|
|
7
|
-
INPUT
|
|
11
|
+
IFS= read -r -d '' INPUT
|
|
8
12
|
|
|
9
13
|
# --- Colors (ANSI-C quoting; bash 3.2 supports $'\033') ---
|
|
10
|
-
C_MODEL=$'\033[38;2;90;140;220m'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
C_MODEL=$'\033[38;2;90;140;220m' # navy blue - model
|
|
15
|
+
C_CTX=$'\033[38;2;45;212;191m' # teal - context window
|
|
16
|
+
C_OK='' # default - meter below 70%
|
|
17
|
+
C_WARN=$'\033[33m' # yellow - meter at 70% and above
|
|
18
|
+
C_CRIT=$'\033[38;2;231;76;60m' # red - meter at 90% and above
|
|
19
|
+
C_COST=$'\033[37m' # white - session cost
|
|
20
|
+
C_CLAUDISH=$'\033[38;2;249;140;30m' # orange - claudish rewrites
|
|
21
|
+
C_VER=$'\033[37m' # white - "Plastic {version}" and update arrow
|
|
22
|
+
C_NEXT=$'\033[33m' # yellow - next version
|
|
23
|
+
C_PATH=$'\033[38;2;128;134;144m' # gray - path (matches dim UI text)
|
|
24
|
+
C_SEP=$'\033[38;2;128;134;144m' # gray - separators
|
|
19
25
|
RST=$'\033[0m'
|
|
20
26
|
SEP="${C_SEP} "$'\302\267'" ${RST}" # " . " middot, dim
|
|
21
27
|
|
|
22
|
-
# ---
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
# --- Truncate a JSON number to an integer ("42.5" -> "42"); empty stays empty ---
|
|
29
|
+
to_int() {
|
|
30
|
+
case "$1" in
|
|
31
|
+
''|*[!0-9.]*) return ;;
|
|
32
|
+
esac
|
|
33
|
+
_i=${1%%.*}
|
|
34
|
+
[ -z "$_i" ] && _i=0
|
|
35
|
+
printf '%s' "$_i"
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
# ---
|
|
31
|
-
|
|
38
|
+
# --- Format a non-negative integer token count with k/M, rounded to the nearest
|
|
39
|
+
# unit; "M" once the count reaches 1,000,000, "k" below it. Empty stays empty. ---
|
|
40
|
+
fmt_tok() {
|
|
41
|
+
case "$1" in
|
|
42
|
+
''|*[!0-9]*) return ;;
|
|
43
|
+
esac
|
|
44
|
+
if [ "$1" -ge 1000000 ]; then
|
|
45
|
+
printf '%dM' "$(( ($1 + 500000) / 1000000 ))"
|
|
46
|
+
else
|
|
47
|
+
printf '%dk' "$(( ($1 + 500) / 1000 ))"
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# --- Round a JSON decimal string to two places ("1.2345" -> "1.23"); empty or
|
|
52
|
+
# non-numeric (including anything with a stray sign or exponent letter) stays
|
|
53
|
+
# empty, matching to_int's guard. Pure bash arithmetic, no awk/printf fork. ---
|
|
54
|
+
fmt_money() {
|
|
55
|
+
case "$1" in
|
|
56
|
+
''|*[!0-9.]*) return ;;
|
|
57
|
+
esac
|
|
58
|
+
_whole=${1%%.*}
|
|
59
|
+
case "$1" in
|
|
60
|
+
*.*) _frac=${1#*.} ;;
|
|
61
|
+
*) _frac="" ;;
|
|
62
|
+
esac
|
|
63
|
+
[ -z "$_whole" ] && _whole=0
|
|
64
|
+
_frac3="${_frac}000"
|
|
65
|
+
_frac3=${_frac3:0:3}
|
|
66
|
+
_cents=${_frac3:0:2}
|
|
67
|
+
_third=${_frac3:2:1}
|
|
68
|
+
[ -z "$_cents" ] && _cents=00
|
|
69
|
+
_cents=$((10#$_cents))
|
|
70
|
+
if [ "$_third" -ge 5 ]; then
|
|
71
|
+
_cents=$((_cents + 1))
|
|
72
|
+
if [ "$_cents" -ge 100 ]; then
|
|
73
|
+
_cents=0
|
|
74
|
+
_whole=$((_whole + 1))
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
printf '%d.%02d' "$_whole" "$_cents"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# --- Walk the stdin JSON exactly once and emit "path<TAB>value" for every field
|
|
81
|
+
# this script reads. A recursive-descent walker (not a regex or a brace-balanced
|
|
82
|
+
# line range) so nesting, pretty-printing, and single-line payloads all parse the
|
|
83
|
+
# same way, and a value inside an array can never be mistaken for one of the
|
|
84
|
+
# named top-level fields (an array suppresses emission for everything under it).
|
|
85
|
+
# A number token containing "e"/"E" is treated as unsupported and dropped, so
|
|
86
|
+
# scientific-notation input renders as absent rather than a wrong magnitude.
|
|
87
|
+
FIELDS=$(printf '%s' "$INPUT" | awk '
|
|
88
|
+
function skip_ws() {
|
|
89
|
+
while (i <= n) {
|
|
90
|
+
c = substr(buf, i, 1)
|
|
91
|
+
if (c == " " || c == "\t" || c == "\n" || c == "\r") { i++ } else { break }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function parse_string( c, esc, start, s) {
|
|
95
|
+
i++
|
|
96
|
+
start = i
|
|
97
|
+
esc = 0
|
|
98
|
+
while (i <= n) {
|
|
99
|
+
c = substr(buf, i, 1)
|
|
100
|
+
if (esc) { esc = 0; i++; continue }
|
|
101
|
+
if (c == "\\") { esc = 1; i++; continue }
|
|
102
|
+
if (c == "\"") { break }
|
|
103
|
+
i++
|
|
104
|
+
}
|
|
105
|
+
s = substr(buf, start, i - start)
|
|
106
|
+
i++
|
|
107
|
+
return s
|
|
108
|
+
}
|
|
109
|
+
function parse_number( start, c) {
|
|
110
|
+
start = i
|
|
111
|
+
if (substr(buf, i, 1) == "-") { i++ }
|
|
112
|
+
while (i <= n) {
|
|
113
|
+
c = substr(buf, i, 1)
|
|
114
|
+
if (c ~ /[0-9]/) { i++; continue }
|
|
115
|
+
if (c == "." || c == "e" || c == "E" || c == "+" || c == "-") { i++; continue }
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
return substr(buf, start, i - start)
|
|
119
|
+
}
|
|
120
|
+
function parse_value(key, prefix, emit, c, qp, val) {
|
|
121
|
+
skip_ws()
|
|
122
|
+
c = substr(buf, i, 1)
|
|
123
|
+
if (key != "") { qp = (prefix == "" ? key : prefix "." key) } else { qp = "" }
|
|
124
|
+
if (c == "{") {
|
|
125
|
+
parse_object(qp, emit)
|
|
126
|
+
} else if (c == "[") {
|
|
127
|
+
parse_array()
|
|
128
|
+
} else if (c == "\"") {
|
|
129
|
+
val = parse_string()
|
|
130
|
+
if (emit && qp != "" && (qp in wanted)) { print qp "\t" val }
|
|
131
|
+
} else if (c == "-" || (c >= "0" && c <= "9")) {
|
|
132
|
+
val = parse_number()
|
|
133
|
+
if (emit && qp != "" && (qp in wanted) && val !~ /[eE]/) { print qp "\t" val }
|
|
134
|
+
} else if (substr(buf, i, 4) == "true") {
|
|
135
|
+
i += 4
|
|
136
|
+
} else if (substr(buf, i, 5) == "false") {
|
|
137
|
+
i += 5
|
|
138
|
+
} else if (substr(buf, i, 4) == "null") {
|
|
139
|
+
i += 4
|
|
140
|
+
} else {
|
|
141
|
+
i++
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function parse_object(prefix, emit, c, key) {
|
|
145
|
+
i++
|
|
146
|
+
skip_ws()
|
|
147
|
+
if (substr(buf, i, 1) == "}") { i++; return }
|
|
148
|
+
while (i <= n) {
|
|
149
|
+
skip_ws()
|
|
150
|
+
if (substr(buf, i, 1) != "\"") {
|
|
151
|
+
if (substr(buf, i, 1) == "}") { i++; return }
|
|
152
|
+
i++
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
key = parse_string()
|
|
156
|
+
skip_ws()
|
|
157
|
+
if (substr(buf, i, 1) == ":") { i++ }
|
|
158
|
+
parse_value(key, prefix, emit)
|
|
159
|
+
skip_ws()
|
|
160
|
+
c = substr(buf, i, 1)
|
|
161
|
+
if (c == ",") { i++; continue }
|
|
162
|
+
if (c == "}") { i++; return }
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function parse_array( c) {
|
|
167
|
+
i++
|
|
168
|
+
skip_ws()
|
|
169
|
+
if (substr(buf, i, 1) == "]") { i++; return }
|
|
170
|
+
while (i <= n) {
|
|
171
|
+
parse_value("", "", 0)
|
|
172
|
+
skip_ws()
|
|
173
|
+
c = substr(buf, i, 1)
|
|
174
|
+
if (c == ",") { i++; continue }
|
|
175
|
+
if (c == "]") { i++; return }
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
BEGIN {
|
|
180
|
+
wanted["model.display_name"] = 1
|
|
181
|
+
wanted["workspace.current_dir"] = 1
|
|
182
|
+
wanted["cwd"] = 1
|
|
183
|
+
wanted["session_id"] = 1
|
|
184
|
+
wanted["context_window.used_percentage"] = 1
|
|
185
|
+
wanted["context_window.context_window_size"] = 1
|
|
186
|
+
wanted["context_window.current_usage.input_tokens"] = 1
|
|
187
|
+
wanted["context_window.current_usage.cache_creation_input_tokens"] = 1
|
|
188
|
+
wanted["context_window.current_usage.cache_read_input_tokens"] = 1
|
|
189
|
+
wanted["cost.total_cost_usd"] = 1
|
|
190
|
+
wanted["rate_limits.five_hour.used_percentage"] = 1
|
|
191
|
+
wanted["rate_limits.seven_day.used_percentage"] = 1
|
|
192
|
+
}
|
|
193
|
+
{ buf = buf $0 " " }
|
|
194
|
+
END {
|
|
195
|
+
n = length(buf)
|
|
196
|
+
i = 1
|
|
197
|
+
skip_ws()
|
|
198
|
+
if (substr(buf, i, 1) == "{") { parse_value("", "", 1) }
|
|
199
|
+
}
|
|
200
|
+
')
|
|
201
|
+
|
|
202
|
+
MODEL=""
|
|
203
|
+
REAL_CWD=""
|
|
204
|
+
CWD_TOP=""
|
|
205
|
+
SID=""
|
|
206
|
+
CW_PCT=""
|
|
207
|
+
CW_SIZE=""
|
|
208
|
+
CU_IN=""
|
|
209
|
+
CU_CC=""
|
|
210
|
+
CU_CR=""
|
|
211
|
+
COST_RAW=""
|
|
212
|
+
M5_PCT=""
|
|
213
|
+
M7_PCT=""
|
|
214
|
+
|
|
215
|
+
while IFS=$'\t' read -r k v; do
|
|
216
|
+
case "$k" in
|
|
217
|
+
model.display_name) MODEL="$v" ;;
|
|
218
|
+
workspace.current_dir) REAL_CWD="$v" ;;
|
|
219
|
+
cwd) CWD_TOP="$v" ;;
|
|
220
|
+
session_id) SID="$v" ;;
|
|
221
|
+
context_window.used_percentage) CW_PCT="$v" ;;
|
|
222
|
+
context_window.context_window_size) CW_SIZE="$v" ;;
|
|
223
|
+
context_window.current_usage.input_tokens) CU_IN="$v" ;;
|
|
224
|
+
context_window.current_usage.cache_creation_input_tokens) CU_CC="$v" ;;
|
|
225
|
+
context_window.current_usage.cache_read_input_tokens) CU_CR="$v" ;;
|
|
226
|
+
cost.total_cost_usd) COST_RAW="$v" ;;
|
|
227
|
+
rate_limits.five_hour.used_percentage) M5_PCT="$v" ;;
|
|
228
|
+
rate_limits.seven_day.used_percentage) M7_PCT="$v" ;;
|
|
229
|
+
esac
|
|
230
|
+
done <<FIELDS_EOF
|
|
231
|
+
$FIELDS
|
|
232
|
+
FIELDS_EOF
|
|
32
233
|
|
|
33
234
|
# --- Path (workspace.current_dir, fallback top-level cwd) ---
|
|
34
|
-
REAL_CWD
|
|
35
|
-
[ -z "$REAL_CWD" ] && REAL_CWD=$(json_str "cwd")
|
|
36
|
-
# Abbreviate $HOME -> ~
|
|
235
|
+
[ -z "$REAL_CWD" ] && REAL_CWD="$CWD_TOP"
|
|
37
236
|
CWD="$REAL_CWD"
|
|
38
237
|
case "$CWD" in
|
|
39
238
|
"$HOME") CWD="~" ;;
|
|
40
239
|
"$HOME"/*) CWD="~${CWD#"$HOME"}" ;;
|
|
41
240
|
esac
|
|
42
241
|
|
|
43
|
-
# --- Plastic version ---
|
|
242
|
+
# --- Plastic version (read builtin, no tr fork) ---
|
|
44
243
|
VERSION=""
|
|
45
|
-
[ -f "$HOME/.plastic/VERSION" ]
|
|
244
|
+
if [ -f "$HOME/.plastic/VERSION" ]; then
|
|
245
|
+
IFS= read -r VERSION < "$HOME/.plastic/VERSION"
|
|
246
|
+
fi
|
|
46
247
|
|
|
47
|
-
# --- Update (next version when one is available) ---
|
|
248
|
+
# --- Update (next version when one is available); pure-bash regex, no fork ---
|
|
48
249
|
NEXT=""
|
|
49
250
|
CACHE_FILE="$HOME/.plastic/.cache/update-check.json"
|
|
50
251
|
if [ -f "$CACHE_FILE" ]; then
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
252
|
+
IFS= read -r -d '' CACHE_JSON < "$CACHE_FILE" || true
|
|
253
|
+
if [[ "$CACHE_JSON" =~ \"updateAvailable\"[[:space:]]*:[[:space:]]*true ]]; then
|
|
254
|
+
if [[ "$CACHE_JSON" =~ \"latest\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
|
|
255
|
+
LATEST="${BASH_REMATCH[1]}"
|
|
55
256
|
case "$LATEST" in
|
|
56
257
|
*-*) NEXT="${LATEST##*-}" ;; # 1.0.0-beta.3 -> beta.3
|
|
57
258
|
*) NEXT="$LATEST" ;;
|
|
@@ -60,144 +261,88 @@ if [ -f "$CACHE_FILE" ]; then
|
|
|
60
261
|
fi
|
|
61
262
|
fi
|
|
62
263
|
|
|
63
|
-
# ---
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
264
|
+
# --- Context window (stdin context_window) ---
|
|
265
|
+
# used_percentage is the documented input-side figure (input + cache creation +
|
|
266
|
+
# cache read), so it wins for the percentage. The token count in parentheses comes
|
|
267
|
+
# from current_usage when it is present. Each derives the other when only one is
|
|
268
|
+
# available; when both are null there is no ctx segment.
|
|
269
|
+
CTX_SEG=""
|
|
270
|
+
CW_SIZE=$(to_int "$CW_SIZE")
|
|
271
|
+
CW_PCT=$(to_int "$CW_PCT")
|
|
272
|
+
CW_USED=""
|
|
273
|
+
U_IN=$(to_int "$CU_IN")
|
|
274
|
+
U_CC=$(to_int "$CU_CC")
|
|
275
|
+
U_CR=$(to_int "$CU_CR")
|
|
276
|
+
if [ -n "$U_IN$U_CC$U_CR" ]; then
|
|
277
|
+
CW_USED=$(( ${U_IN:-0} + ${U_CC:-0} + ${U_CR:-0} ))
|
|
76
278
|
fi
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
INDEX="$HOME/.plastic/projects/$SLUG/INDEX.md"
|
|
80
|
-
PROJ_LABEL="$SLUG"
|
|
81
|
-
else
|
|
82
|
-
INDEX="$HOME/.plastic/INDEX.md"
|
|
83
|
-
PROJ_LABEL="global"
|
|
279
|
+
if [ -z "$CW_USED" ] && [ -n "$CW_PCT" ] && [ -n "$CW_SIZE" ] && [ "$CW_SIZE" -gt 0 ]; then
|
|
280
|
+
CW_USED=$(( CW_SIZE * CW_PCT / 100 ))
|
|
84
281
|
fi
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# the newest by mtime (ls -t is 3.2-safe); tolerate a legacy single-key
|
|
93
|
-
# plastic-{session_id}.json when no per-intent file exists. This beats the shared,
|
|
94
|
-
# savepoint-recency heuristic so parallel sessions on one store no longer
|
|
95
|
-
# overwrite each other's line. grep/sed/ls only - no jq, no ruby.
|
|
96
|
-
WORK=""
|
|
97
|
-
SID=$(json_str "session_id")
|
|
98
|
-
|
|
99
|
-
if [ -n "$SID" ]; then
|
|
100
|
-
BR_TMP="${PLASTIC_TMP:-/tmp}"
|
|
101
|
-
BRIDGE=""
|
|
102
|
-
CANDIDATES=$(ls -1t "${BR_TMP}"/plastic-"${SID}"--*.json 2>/dev/null)
|
|
103
|
-
if [ -n "$CANDIDATES" ]; then
|
|
104
|
-
while IFS= read -r cand; do
|
|
105
|
-
[ -z "$cand" ] && continue
|
|
106
|
-
WT_BLOCK=$(sed -n '/"worktree"[[:space:]]*:[[:space:]]*{/,/}/p' "$cand" 2>/dev/null)
|
|
107
|
-
WC=$(printf '%s\n' "$WT_BLOCK" | grep -o '"code"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 \
|
|
108
|
-
| sed 's/.*"code"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
109
|
-
IN_BLOCK=$(sed -n '/"intent"[[:space:]]*:[[:space:]]*{/,/}/p' "$cand" 2>/dev/null)
|
|
110
|
-
ISTORE=$(printf '%s\n' "$IN_BLOCK" | grep -o '"store"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 \
|
|
111
|
-
| sed 's/.*"store"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
112
|
-
IDIR=$(printf '%s\n' "$IN_BLOCK" | grep -o '"dir"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 \
|
|
113
|
-
| sed 's/.*"dir"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
114
|
-
MATCH=""
|
|
115
|
-
if [ -n "$WC" ] && [ -n "$REAL_CWD" ]; then
|
|
116
|
-
case "$REAL_CWD" in "$WC"|"$WC"/*) MATCH=1 ;; esac
|
|
117
|
-
fi
|
|
118
|
-
if [ -z "$MATCH" ] && [ -n "$ISTORE" ] && [ -n "$IDIR" ] && [ -n "$REAL_CWD" ]; then
|
|
119
|
-
IPATH="$ISTORE/$IDIR"
|
|
120
|
-
case "$REAL_CWD" in "$IPATH"|"$IPATH"/*) MATCH=1 ;; esac
|
|
121
|
-
fi
|
|
122
|
-
if [ -n "$MATCH" ]; then
|
|
123
|
-
BRIDGE="$cand"
|
|
124
|
-
break
|
|
125
|
-
fi
|
|
126
|
-
done <<BRIDGE_CAND_EOF
|
|
127
|
-
$CANDIDATES
|
|
128
|
-
BRIDGE_CAND_EOF
|
|
129
|
-
# No cwd/worktree signal on any candidate: fail open to the newest (ls -t
|
|
130
|
-
# already sorted CANDIDATES newest-first).
|
|
131
|
-
[ -z "$BRIDGE" ] && BRIDGE=$(printf '%s\n' "$CANDIDATES" | head -1)
|
|
132
|
-
else
|
|
133
|
-
# Legacy tolerance: no per-intent bridge for this session, but an old
|
|
134
|
-
# single-key file may still exist.
|
|
135
|
-
LEGACY="${BR_TMP}/plastic-${SID}.json"
|
|
136
|
-
[ -f "$LEGACY" ] && BRIDGE="$LEGACY"
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
if [ -n "$BRIDGE" ] && [ -f "$BRIDGE" ]; then
|
|
140
|
-
# Bridge JSON is pretty-printed (one field per line). Scope extraction to the
|
|
141
|
-
# "intent" object so a sibling top-level "id"/"name" can never win (the intent
|
|
142
|
-
# object holds only string fields, so the first "}" closes it). grep/sed only.
|
|
143
|
-
INTENT_BLOCK=$(sed -n '/"intent"[[:space:]]*:[[:space:]]*{/,/}/p' "$BRIDGE" 2>/dev/null)
|
|
144
|
-
B_ID=$(printf '%s\n' "$INTENT_BLOCK" | grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 \
|
|
145
|
-
| sed 's/.*"id"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
146
|
-
B_NAME=$(printf '%s\n' "$INTENT_BLOCK" | grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 \
|
|
147
|
-
| sed 's/.*"name"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
148
|
-
if [ -n "$B_ID" ] && [ -n "$B_NAME" ]; then
|
|
149
|
-
EMDASH=$'\342\200\224'
|
|
150
|
-
WORK="${B_ID} ${EMDASH} ${B_NAME}"
|
|
151
|
-
fi
|
|
282
|
+
if [ -z "$CW_PCT" ] && [ -n "$CW_USED" ] && [ -n "$CW_SIZE" ] && [ "$CW_SIZE" -gt 0 ]; then
|
|
283
|
+
CW_PCT=$(( CW_USED * 100 / CW_SIZE ))
|
|
284
|
+
fi
|
|
285
|
+
if [ -n "$CW_PCT" ]; then
|
|
286
|
+
CTX_SEG="${C_CTX}ctx ${CW_PCT}%"
|
|
287
|
+
if [ -n "$CW_USED" ] && [ -n "$CW_SIZE" ] && [ "$CW_SIZE" -gt 0 ]; then
|
|
288
|
+
CTX_SEG="${CTX_SEG} ($(fmt_tok "$CW_USED")/$(fmt_tok "$CW_SIZE"))"
|
|
152
289
|
fi
|
|
290
|
+
CTX_SEG="${CTX_SEG}${RST}"
|
|
153
291
|
fi
|
|
154
292
|
|
|
155
|
-
# ---
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
WORK="$inside"
|
|
173
|
-
fi
|
|
174
|
-
done <<EOF
|
|
175
|
-
$(sed -n '/^## Active$/,/^## /{/^- \[/p;}' "$INDEX")
|
|
176
|
-
EOF
|
|
293
|
+
# --- Subscription meters (stdin rate_limits; absent for API-key sessions) ---
|
|
294
|
+
meter_seg() { # $1 = label, $2 = raw percentage
|
|
295
|
+
_p=$(to_int "$2")
|
|
296
|
+
[ -z "$_p" ] && return
|
|
297
|
+
_c="$C_OK"
|
|
298
|
+
[ "$_p" -ge 70 ] && _c="$C_WARN"
|
|
299
|
+
[ "$_p" -ge 90 ] && _c="$C_CRIT"
|
|
300
|
+
printf '%s' "${_c}$1 ${_p}%${RST}"
|
|
301
|
+
}
|
|
302
|
+
M5=$(meter_seg "5h" "$M5_PCT")
|
|
303
|
+
M7=$(meter_seg "7d" "$M7_PCT")
|
|
304
|
+
|
|
305
|
+
# --- Session cost (stdin cost.total_cost_usd); hidden when it renders as $0.00 ---
|
|
306
|
+
COST_SEG=""
|
|
307
|
+
if [ -n "$COST_RAW" ]; then
|
|
308
|
+
COST_FMT=$(fmt_money "$COST_RAW")
|
|
309
|
+
[ -n "$COST_FMT" ] && [ "$COST_FMT" != "0.00" ] && COST_SEG="${C_COST}\$${COST_FMT}${RST}"
|
|
177
310
|
fi
|
|
178
311
|
|
|
179
|
-
# ---
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
312
|
+
# --- claudish rewrites for THIS session (fork ledger, optional) ---
|
|
313
|
+
# Tab separated, one call per line; column 11 is the session id. Rows written
|
|
314
|
+
# before that column existed have 9 or 10 fields and never match, so an old
|
|
315
|
+
# ledger renders no segment instead of a wrong one.
|
|
316
|
+
CL_SEG=""
|
|
317
|
+
CL_LOG="${CLAUDISH_LOCAL_DIR:-$HOME/.claude/claudish-local}/usage.log"
|
|
318
|
+
if [ -n "$SID" ] && [ -f "$CL_LOG" ]; then
|
|
319
|
+
CL=$(awk -F'\t' -v sid="$SID" '
|
|
320
|
+
NF >= 11 && $11 == sid { n++; t += $5 + $6 }
|
|
321
|
+
END {
|
|
322
|
+
if (n > 0) {
|
|
323
|
+
if (t >= 1000) {
|
|
324
|
+
s = sprintf("%.1f", t / 1000); sub(/\.0$/, "", s); printf "%d %sk", n, s
|
|
325
|
+
} else {
|
|
326
|
+
printf "%d %d", n, t
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}' "$CL_LOG" 2>/dev/null)
|
|
330
|
+
[ -n "$CL" ] && CL_SEG="${C_CLAUDISH}claudish ${CL%% *} rw / ${CL#* }${RST}"
|
|
191
331
|
fi
|
|
192
332
|
|
|
193
333
|
# --- Assemble segments in order, joined by " . " ---
|
|
194
334
|
PARTS=""
|
|
195
335
|
add() { [ -z "$1" ] && return; if [ -z "$PARTS" ]; then PARTS="$1"; else PARTS="$PARTS$SEP$1"; fi; }
|
|
196
336
|
|
|
197
|
-
# Order:
|
|
198
|
-
# values last (version, path) so the line does
|
|
337
|
+
# Order: identity first (model), then this session's spend (context, meters, cost,
|
|
338
|
+
# rewrites), then the volatile-length values last (version, path) so the line does
|
|
339
|
+
# not visually jump as they change.
|
|
199
340
|
[ -n "$MODEL" ] && add "${C_MODEL}${MODEL}${RST}"
|
|
200
|
-
|
|
341
|
+
add "$CTX_SEG"
|
|
342
|
+
add "$M5"
|
|
343
|
+
add "$M7"
|
|
344
|
+
add "$COST_SEG"
|
|
345
|
+
add "$CL_SEG"
|
|
201
346
|
if [ -n "$VERSION" ]; then
|
|
202
347
|
vseg="${C_VER}Plastic ${VERSION}${RST}"
|
|
203
348
|
[ -n "$NEXT" ] && vseg="${vseg}${C_VER} "$'\342\206\222'" ${RST}${C_NEXT}${NEXT}${RST}"
|