@ictechgy/context-guard 0.4.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/CHANGELOG.md +49 -0
- package/LICENSE +201 -0
- package/NOTICE +4 -0
- package/README.ko.md +353 -0
- package/README.md +353 -0
- package/context-guard-kit/README.md +76 -0
- package/context-guard-kit/benchmark_runner.py +1898 -0
- package/context-guard-kit/claude_transcript_cost_audit.py +1591 -0
- package/context-guard-kit/context_compress.py +543 -0
- package/context-guard-kit/context_escrow.py +919 -0
- package/context-guard-kit/context_guard_cli.py +149 -0
- package/context-guard-kit/context_guard_diet.py +1036 -0
- package/context-guard-kit/context_pack.py +929 -0
- package/context-guard-kit/failed_attempt_nudge.py +567 -0
- package/context-guard-kit/guard_large_read.py +690 -0
- package/context-guard-kit/hook_secret_patterns.py +43 -0
- package/context-guard-kit/read_symbol.py +483 -0
- package/context-guard-kit/rewrite_bash_for_token_budget.py +501 -0
- package/context-guard-kit/sanitize_output.py +725 -0
- package/context-guard-kit/settings.example.json +67 -0
- package/context-guard-kit/setup_wizard.py +1724 -0
- package/context-guard-kit/statusline.sh +362 -0
- package/context-guard-kit/statusline_merged.sh +157 -0
- package/context-guard-kit/tool_schema_pruner.py +837 -0
- package/context-guard-kit/trim_command_output.py +1098 -0
- package/docs/distribution.md +55 -0
- package/package.json +70 -0
- package/packaging/homebrew/context-guard.rb.template +34 -0
- package/plugins/context-guard/.claude-plugin/plugin.json +41 -0
- package/plugins/context-guard/LICENSE +201 -0
- package/plugins/context-guard/NOTICE +4 -0
- package/plugins/context-guard/README.ko.md +135 -0
- package/plugins/context-guard/README.md +135 -0
- package/plugins/context-guard/bin/claude-read-symbol +6 -0
- package/plugins/context-guard/bin/claude-sanitize-output +6 -0
- package/plugins/context-guard/bin/claude-token-artifact +6 -0
- package/plugins/context-guard/bin/claude-token-audit +6 -0
- package/plugins/context-guard/bin/claude-token-bench +6 -0
- package/plugins/context-guard/bin/claude-token-diet +6 -0
- package/plugins/context-guard/bin/claude-token-failed-nudge +6 -0
- package/plugins/context-guard/bin/claude-token-guard-read +6 -0
- package/plugins/context-guard/bin/claude-token-rewrite-bash +6 -0
- package/plugins/context-guard/bin/claude-token-setup +6 -0
- package/plugins/context-guard/bin/claude-token-statusline +6 -0
- package/plugins/context-guard/bin/claude-token-statusline-merged +6 -0
- package/plugins/context-guard/bin/claude-trim-output +6 -0
- package/plugins/context-guard/bin/context-guard +149 -0
- package/plugins/context-guard/bin/context-guard-artifact +919 -0
- package/plugins/context-guard/bin/context-guard-audit +1591 -0
- package/plugins/context-guard/bin/context-guard-bench +1898 -0
- package/plugins/context-guard/bin/context-guard-compress +543 -0
- package/plugins/context-guard/bin/context-guard-diet +1036 -0
- package/plugins/context-guard/bin/context-guard-failed-nudge +567 -0
- package/plugins/context-guard/bin/context-guard-guard-read +690 -0
- package/plugins/context-guard/bin/context-guard-pack +929 -0
- package/plugins/context-guard/bin/context-guard-read-symbol +483 -0
- package/plugins/context-guard/bin/context-guard-rewrite-bash +501 -0
- package/plugins/context-guard/bin/context-guard-sanitize-output +725 -0
- package/plugins/context-guard/bin/context-guard-setup +1724 -0
- package/plugins/context-guard/bin/context-guard-statusline +362 -0
- package/plugins/context-guard/bin/context-guard-statusline-merged +157 -0
- package/plugins/context-guard/bin/context-guard-tool-prune +837 -0
- package/plugins/context-guard/bin/context-guard-trim-output +1098 -0
- package/plugins/context-guard/brief/README.md +65 -0
- package/plugins/context-guard/brief/brief-mode.lite.md +29 -0
- package/plugins/context-guard/brief/brief-mode.standard.md +31 -0
- package/plugins/context-guard/brief/brief-mode.ultra.md +32 -0
- package/plugins/context-guard/lib/hook_secret_patterns.py +43 -0
- package/plugins/context-guard/skills/audit/SKILL.md +39 -0
- package/plugins/context-guard/skills/optimize/SKILL.md +48 -0
- package/plugins/context-guard/skills/setup/SKILL.md +40 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [[ -t 0 ]]; then
|
|
5
|
+
echo "usage: pass Claude Code statusline JSON on stdin"
|
|
6
|
+
exit 0
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
statusline_input_tmp=''
|
|
10
|
+
|
|
11
|
+
statusline_tmp_base() {
|
|
12
|
+
local candidate="${TMPDIR:-/tmp}" resolved
|
|
13
|
+
if [[ "$candidate" != "/" ]]; then
|
|
14
|
+
candidate="${candidate%/}"
|
|
15
|
+
fi
|
|
16
|
+
if [[ -z "$candidate" || "$candidate" != /* || ! -d "$candidate" || ! -w "$candidate" ]]; then
|
|
17
|
+
candidate="/tmp"
|
|
18
|
+
fi
|
|
19
|
+
if resolved=$(cd "$candidate" 2>/dev/null && pwd -P); then
|
|
20
|
+
if [[ "$resolved" != "/" ]]; then
|
|
21
|
+
resolved="${resolved%/}"
|
|
22
|
+
fi
|
|
23
|
+
printf '%s\n' "${resolved:-/}"
|
|
24
|
+
else
|
|
25
|
+
printf '/tmp\n'
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
statusline_input_max_bytes() {
|
|
30
|
+
local raw="${CONTEXT_GUARD_STATUSLINE_INPUT_MAX_BYTES:-${CLAUDE_TOKEN_STATUSLINE_INPUT_MAX_BYTES:-65536}}" max=65536
|
|
31
|
+
if [[ "$raw" =~ ^[0-9]+$ ]] && (( ${#raw} <= 7 )); then
|
|
32
|
+
max=$((10#$raw))
|
|
33
|
+
fi
|
|
34
|
+
if (( max < 1 || max > 1048576 )); then
|
|
35
|
+
max=65536
|
|
36
|
+
fi
|
|
37
|
+
printf '%s\n' "$max"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
statusline_context_warn_threshold() {
|
|
41
|
+
local raw="${CONTEXT_GUARD_STATUSLINE_CTX_WARN:-${CLAUDE_TOKEN_STATUSLINE_CTX_WARN:-80}}" threshold=80
|
|
42
|
+
if [[ "$raw" =~ ^[0-9]{1,3}$ ]]; then
|
|
43
|
+
threshold=$((10#$raw))
|
|
44
|
+
if (( threshold < 1 )); then
|
|
45
|
+
threshold=1
|
|
46
|
+
elif (( threshold > 100 )); then
|
|
47
|
+
threshold=100
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
printf '%s\n' "$threshold"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
read_bounded_statusline_input() {
|
|
54
|
+
local max input_len tmp_base
|
|
55
|
+
max=$(statusline_input_max_bytes)
|
|
56
|
+
tmp_base=$(statusline_tmp_base)
|
|
57
|
+
statusline_input_tmp=$(mktemp "$tmp_base/context-guard-statusline.XXXXXX") || {
|
|
58
|
+
printf '[input-error] could not create statusline input buffer\n'
|
|
59
|
+
exit 0
|
|
60
|
+
}
|
|
61
|
+
trap 'rm -f "${statusline_input_tmp:-}"' EXIT
|
|
62
|
+
LC_ALL=C head -c "$((max + 1))" >"$statusline_input_tmp" 2>/dev/null || true
|
|
63
|
+
input_len=$(LC_ALL=C wc -c <"$statusline_input_tmp" | tr -d '[:space:]')
|
|
64
|
+
if (( input_len > max )); then
|
|
65
|
+
printf '[input-too-large] Claude statusline JSON exceeds %s bytes\n' "$max"
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
input=$(cat "$statusline_input_tmp" 2>/dev/null || true)
|
|
69
|
+
rm -f "$statusline_input_tmp"
|
|
70
|
+
statusline_input_tmp=''
|
|
71
|
+
trap - EXIT
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
read_bounded_statusline_input
|
|
75
|
+
|
|
76
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
77
|
+
echo "[needs-jq] install jq for Claude token statusline"
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
jq_get() {
|
|
82
|
+
jq -r "$1 // empty" <<<"$input" 2>/dev/null || true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
strip_terminal_sequences() {
|
|
86
|
+
if command -v perl >/dev/null 2>&1; then
|
|
87
|
+
perl -pe 's/\e\][^\a\e]*(?:\a|\e\\)//g; s/\e[@-_][0-?]*[ -\/]*[@-~]//g'
|
|
88
|
+
else
|
|
89
|
+
cat
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
sanitize_status() {
|
|
94
|
+
# Statusline values may come from untrusted workspace metadata; keep one-line printable text.
|
|
95
|
+
local cleaned
|
|
96
|
+
cleaned=$(printf '%s' "$1" \
|
|
97
|
+
| strip_terminal_sequences \
|
|
98
|
+
| LC_ALL=C tr '\r\n' ' ' \
|
|
99
|
+
| LC_ALL=C tr -d '\000-\010\013\014\016-\037\177-\237' \
|
|
100
|
+
| cut -c 1-160)
|
|
101
|
+
if printf '%s' "$cleaned" | LC_ALL=C grep -Eiq '(gh[pousr]_|github_pat_|glpat-|xox[abprs]-|AKIA|ASIA|sk-|npm_|AIza|Bearer[[:space:]]|Basic[[:space:]])'; then
|
|
102
|
+
printf '[redacted]'
|
|
103
|
+
else
|
|
104
|
+
printf '%s' "$cleaned"
|
|
105
|
+
fi
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
git_head_branch() {
|
|
109
|
+
# Keep the statusline cheap and non-blocking: do not invoke `git` here. Some
|
|
110
|
+
# workspaces have slow network filesystems, hydrated-on-demand git objects, or
|
|
111
|
+
# broken config; reading .git/HEAD is enough for a best-effort branch label.
|
|
112
|
+
local current="$1"
|
|
113
|
+
local dotgit gitdir_line gitdir head_file head_line branch
|
|
114
|
+
[[ -n "$current" && -d "$current" ]] || return 1
|
|
115
|
+
current=$(cd "$current" 2>/dev/null && pwd -P) || return 1
|
|
116
|
+
|
|
117
|
+
while [[ -n "$current" ]]; do
|
|
118
|
+
head_file=''
|
|
119
|
+
dotgit="$current/.git"
|
|
120
|
+
if [[ -d "$dotgit" && ! -L "$dotgit" ]]; then
|
|
121
|
+
head_file="$dotgit/HEAD"
|
|
122
|
+
elif [[ -f "$dotgit" && ! -L "$dotgit" ]]; then
|
|
123
|
+
IFS= read -r gitdir_line <"$dotgit" 2>/dev/null || gitdir_line=''
|
|
124
|
+
if [[ "$gitdir_line" == gitdir:\ * ]]; then
|
|
125
|
+
gitdir="${gitdir_line#gitdir: }"
|
|
126
|
+
[[ "$gitdir" == /* ]] || gitdir="$current/$gitdir"
|
|
127
|
+
if gitdir=$(cd "$gitdir" 2>/dev/null && pwd -P) && [[ -f "$gitdir/HEAD" && ! -L "$gitdir/HEAD" ]]; then
|
|
128
|
+
head_file="$gitdir/HEAD"
|
|
129
|
+
fi
|
|
130
|
+
fi
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
if [[ -n "$head_file" && -f "$head_file" && ! -L "$head_file" ]]; then
|
|
134
|
+
IFS= read -r head_line <"$head_file" 2>/dev/null || return 1
|
|
135
|
+
if [[ "$head_line" == ref:\ refs/heads/* ]]; then
|
|
136
|
+
branch="${head_line#ref: refs/heads/}"
|
|
137
|
+
[[ -n "$branch" ]] && printf '%s\n' "$branch"
|
|
138
|
+
return 0
|
|
139
|
+
fi
|
|
140
|
+
if [[ "$head_line" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
|
|
141
|
+
printf '%s\n' "${head_line:0:12}"
|
|
142
|
+
return 0
|
|
143
|
+
fi
|
|
144
|
+
return 1
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
[[ "$current" == "/" ]] && break
|
|
148
|
+
current="${current%/*}"
|
|
149
|
+
[[ -n "$current" ]] || current="/"
|
|
150
|
+
done
|
|
151
|
+
return 1
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
model=$(jq_get '.model.display_name')
|
|
155
|
+
model=${model:-$(jq_get '.model.id')}
|
|
156
|
+
model=${model:-unknown}
|
|
157
|
+
model=$(sanitize_status "$model")
|
|
158
|
+
|
|
159
|
+
context_raw=$(jq_get '.context_window.used_percentage')
|
|
160
|
+
context_is_numeric=0
|
|
161
|
+
if [[ -n "$context_raw" ]]; then
|
|
162
|
+
if context_pct=$(LC_NUMERIC=C printf '%.0f' "$context_raw" 2>/dev/null); then
|
|
163
|
+
if [[ "$context_pct" =~ ^-?[0-9]+$ ]]; then
|
|
164
|
+
context_is_numeric=1
|
|
165
|
+
else
|
|
166
|
+
context_pct=$(sanitize_status "$context_raw")
|
|
167
|
+
fi
|
|
168
|
+
else
|
|
169
|
+
context_pct=$(sanitize_status "$context_raw")
|
|
170
|
+
fi
|
|
171
|
+
else
|
|
172
|
+
context_pct="?"
|
|
173
|
+
fi
|
|
174
|
+
context_label="${context_pct}%"
|
|
175
|
+
if (( context_is_numeric )); then
|
|
176
|
+
context_warn_threshold=$(statusline_context_warn_threshold)
|
|
177
|
+
if (( context_pct >= context_warn_threshold )); then
|
|
178
|
+
context_label="${context_label} ⚠"
|
|
179
|
+
fi
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
cost=$(jq_get '.cost.total_cost_usd')
|
|
183
|
+
if [[ -n "$cost" ]]; then
|
|
184
|
+
cost=$(printf '$%.3f' "$cost" 2>/dev/null || sanitize_status "$cost")
|
|
185
|
+
else
|
|
186
|
+
cost='n/a'
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
cwd=$(jq_get '.workspace.current_dir')
|
|
190
|
+
dir=${cwd##*/}
|
|
191
|
+
dir=${dir:-.}
|
|
192
|
+
dir=$(sanitize_status "$dir")
|
|
193
|
+
|
|
194
|
+
branch=''
|
|
195
|
+
branch_dir=${cwd:-$PWD}
|
|
196
|
+
b=$(git_head_branch "$branch_dir" 2>/dev/null || true)
|
|
197
|
+
if [[ -n "$b" ]]; then
|
|
198
|
+
b=$(sanitize_status "$b")
|
|
199
|
+
branch=" | ${b}"
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# Cache metrics from the transcript tail (best-effort, fast — reads only the last 1MB).
|
|
203
|
+
# Stays empty when transcript is unavailable or python3 fails so the status line never breaks.
|
|
204
|
+
# NOTE: keep the token-key list and usage-extraction shape in sync with claude_transcript_cost_audit.py
|
|
205
|
+
# so the statusline metric matches the audit metric for the same transcript.
|
|
206
|
+
metrics_label=''
|
|
207
|
+
transcript_path=$(jq_get '.transcript_path')
|
|
208
|
+
if [[ -n "$transcript_path" && -r "$transcript_path" ]] && command -v python3 >/dev/null 2>&1; then
|
|
209
|
+
transcript_metrics=$(python3 - "$transcript_path" 2>/dev/null <<'PYEOF' || true
|
|
210
|
+
import json
|
|
211
|
+
import os
|
|
212
|
+
import stat
|
|
213
|
+
import sys
|
|
214
|
+
|
|
215
|
+
path = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
216
|
+
if not path:
|
|
217
|
+
sys.exit(0)
|
|
218
|
+
|
|
219
|
+
# Bounded tail read so the statusline never stalls on huge transcripts.
|
|
220
|
+
TAIL_BYTES = 1024 * 1024
|
|
221
|
+
MAX_RECORDS = 300
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _int_or_zero(value):
|
|
225
|
+
"""transcript usage 토큰값을 정수로 강제. bool은 int 서브클래스이므로 별도 차단."""
|
|
226
|
+
if isinstance(value, bool):
|
|
227
|
+
return 0
|
|
228
|
+
if isinstance(value, int):
|
|
229
|
+
return max(0, value)
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _extract_usage(record):
|
|
234
|
+
"""transcript record에서 알려진 usage 객체 1개만 꺼낸다.
|
|
235
|
+
|
|
236
|
+
Claude Code transcript JSONL은 record 당 한 번의 LLM 호출 usage를 다음 중 한 자리에
|
|
237
|
+
넣는 것이 일반적이다 — top-level "usage", "message.usage", "response.usage".
|
|
238
|
+
재귀 walk 대신 알려진 경로만 보아야 동일 값이 여러 nested 사본으로 들어왔을 때
|
|
239
|
+
이중 합산되는 문제를 피할 수 있다.
|
|
240
|
+
"""
|
|
241
|
+
if not isinstance(record, dict):
|
|
242
|
+
return None
|
|
243
|
+
for path_keys in (("usage",), ("message", "usage"), ("response", "usage")):
|
|
244
|
+
cur = record
|
|
245
|
+
for key in path_keys:
|
|
246
|
+
if not isinstance(cur, dict):
|
|
247
|
+
cur = None
|
|
248
|
+
break
|
|
249
|
+
cur = cur.get(key)
|
|
250
|
+
if isinstance(cur, dict):
|
|
251
|
+
return cur
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
input_tokens = cache_read = cache_creation = 0
|
|
256
|
+
|
|
257
|
+
def _open_regular_transcript(path):
|
|
258
|
+
flags = os.O_RDONLY
|
|
259
|
+
if hasattr(os, "O_CLOEXEC"):
|
|
260
|
+
flags |= os.O_CLOEXEC
|
|
261
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
262
|
+
flags |= os.O_NOFOLLOW
|
|
263
|
+
if hasattr(os, "O_NONBLOCK"):
|
|
264
|
+
flags |= os.O_NONBLOCK
|
|
265
|
+
st = os.lstat(path)
|
|
266
|
+
if not stat.S_ISREG(st.st_mode):
|
|
267
|
+
return None
|
|
268
|
+
fd = os.open(path, flags)
|
|
269
|
+
try:
|
|
270
|
+
opened = os.fstat(fd)
|
|
271
|
+
if not stat.S_ISREG(opened.st_mode):
|
|
272
|
+
os.close(fd)
|
|
273
|
+
return None
|
|
274
|
+
return fd, opened.st_size
|
|
275
|
+
except Exception:
|
|
276
|
+
os.close(fd)
|
|
277
|
+
raise
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _read_tail(fd, size):
|
|
281
|
+
read_size = min(size, TAIL_BYTES)
|
|
282
|
+
if size > read_size:
|
|
283
|
+
os.lseek(fd, size - read_size, os.SEEK_SET)
|
|
284
|
+
chunks = []
|
|
285
|
+
remaining = read_size
|
|
286
|
+
while remaining > 0:
|
|
287
|
+
chunk = os.read(fd, remaining)
|
|
288
|
+
if not chunk:
|
|
289
|
+
break
|
|
290
|
+
chunks.append(chunk)
|
|
291
|
+
remaining -= len(chunk)
|
|
292
|
+
return b"".join(chunks), read_size
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
opened = _open_regular_transcript(path)
|
|
297
|
+
if opened is None:
|
|
298
|
+
sys.exit(0)
|
|
299
|
+
fd, size = opened
|
|
300
|
+
try:
|
|
301
|
+
chunk, read_size = _read_tail(fd, size)
|
|
302
|
+
finally:
|
|
303
|
+
os.close(fd)
|
|
304
|
+
lines = chunk.splitlines()
|
|
305
|
+
if size > read_size and lines:
|
|
306
|
+
# First line in the tail window is likely partial; drop it.
|
|
307
|
+
lines = lines[1:]
|
|
308
|
+
for raw in lines[-MAX_RECORDS:]:
|
|
309
|
+
if not raw.strip():
|
|
310
|
+
continue
|
|
311
|
+
try:
|
|
312
|
+
obj = json.loads(raw)
|
|
313
|
+
except Exception:
|
|
314
|
+
continue
|
|
315
|
+
usage = _extract_usage(obj)
|
|
316
|
+
if not usage:
|
|
317
|
+
continue
|
|
318
|
+
input_tokens += _int_or_zero(usage.get("input_tokens"))
|
|
319
|
+
cr = usage.get("cache_read_input_tokens")
|
|
320
|
+
if cr is None:
|
|
321
|
+
cr = usage.get("cacheRead")
|
|
322
|
+
cache_read += _int_or_zero(cr)
|
|
323
|
+
cc = usage.get("cache_creation_input_tokens")
|
|
324
|
+
if cc is None:
|
|
325
|
+
cc = usage.get("cacheCreation")
|
|
326
|
+
cache_creation += _int_or_zero(cc)
|
|
327
|
+
denom = input_tokens + cache_read + cache_creation
|
|
328
|
+
# Skip the label entirely on empty / cache-cold sessions so the user does not see a
|
|
329
|
+
# confusing "cache 0%" before the cache has had a chance to warm up.
|
|
330
|
+
if denom <= 0 or cache_read <= 0:
|
|
331
|
+
sys.exit(0)
|
|
332
|
+
pct = max(0.0, min(100.0, cache_read / denom * 100))
|
|
333
|
+
parts = [f"cache_pct={pct:.0f}"]
|
|
334
|
+
if cache_creation > 0:
|
|
335
|
+
parts.append(f"reuse_x={cache_read / cache_creation:.1f}")
|
|
336
|
+
print(" ".join(parts))
|
|
337
|
+
except Exception:
|
|
338
|
+
sys.exit(0)
|
|
339
|
+
PYEOF
|
|
340
|
+
)
|
|
341
|
+
if [[ -n "$transcript_metrics" ]]; then
|
|
342
|
+
cache_pct=''
|
|
343
|
+
reuse_x=''
|
|
344
|
+
for metric in $transcript_metrics; do
|
|
345
|
+
case "$metric" in
|
|
346
|
+
cache_pct=*) cache_pct="${metric#cache_pct=}" ;;
|
|
347
|
+
reuse_x=*) reuse_x="${metric#reuse_x=}" ;;
|
|
348
|
+
esac
|
|
349
|
+
done
|
|
350
|
+
if [[ -n "$cache_pct" ]]; then
|
|
351
|
+
cache_pct=$(sanitize_status "$cache_pct")
|
|
352
|
+
metrics_label=" | cache ${cache_pct}%"
|
|
353
|
+
if [[ -n "$reuse_x" ]]; then
|
|
354
|
+
reuse_x=$(sanitize_status "$reuse_x")
|
|
355
|
+
metrics_label+=" | reuse ${reuse_x}x"
|
|
356
|
+
fi
|
|
357
|
+
fi
|
|
358
|
+
fi
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
# Keep it one line and cheap: this script runs locally and should not do expensive git status.
|
|
362
|
+
echo "[$model] ${dir}${branch} | ctx ${context_label} | cost ${cost}${metrics_label}"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# OMC HUD 와 context-guard-statusline 을 하나로 결합하는 statusline wrapper.
|
|
3
|
+
#
|
|
4
|
+
# 동작 매트릭스:
|
|
5
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
6
|
+
# OMC HUD 존재? │ token-statusline 존재? │ 출력
|
|
7
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
8
|
+
# yes │ yes │ OMC HUD + cost/cache/reuse 결합 (1줄)
|
|
9
|
+
# yes │ no │ OMC HUD 단독
|
|
10
|
+
# no │ yes │ token-statusline 단독
|
|
11
|
+
# no │ no │ "[hud unavailable]"
|
|
12
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
13
|
+
#
|
|
14
|
+
# 입력: stdin 으로 Claude Code 가 넘기는 statusline JSON 한 줄.
|
|
15
|
+
# 출력: stdout 한 줄.
|
|
16
|
+
#
|
|
17
|
+
# 환경변수(선택, 테스트/커스텀 설치용):
|
|
18
|
+
# OMC_HUD_SCRIPT OMC HUD 스크립트 경로 (기본 $HOME/.claude/hud/omc-hud.mjs)
|
|
19
|
+
# CONTEXT_GUARD_STATUSLINE_BIN context-guard-statusline 바이너리 경로
|
|
20
|
+
# (legacy: CLAUDE_TOKEN_STATUSLINE_BIN)
|
|
21
|
+
# (미지정 시 자기 옆 디렉토리만 사용; PATH 탐색 안 함)
|
|
22
|
+
set -u
|
|
23
|
+
|
|
24
|
+
statusline_input_tmp=''
|
|
25
|
+
|
|
26
|
+
statusline_tmp_base() {
|
|
27
|
+
local candidate="${TMPDIR:-/tmp}" resolved
|
|
28
|
+
if [[ "$candidate" != "/" ]]; then
|
|
29
|
+
candidate="${candidate%/}"
|
|
30
|
+
fi
|
|
31
|
+
if [[ -z "$candidate" || "$candidate" != /* || ! -d "$candidate" || ! -w "$candidate" ]]; then
|
|
32
|
+
candidate="/tmp"
|
|
33
|
+
fi
|
|
34
|
+
if resolved=$(cd "$candidate" 2>/dev/null && pwd -P); then
|
|
35
|
+
if [[ "$resolved" != "/" ]]; then
|
|
36
|
+
resolved="${resolved%/}"
|
|
37
|
+
fi
|
|
38
|
+
printf '%s\n' "${resolved:-/}"
|
|
39
|
+
else
|
|
40
|
+
printf '/tmp\n'
|
|
41
|
+
fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
statusline_input_max_bytes() {
|
|
45
|
+
local raw="${CONTEXT_GUARD_STATUSLINE_INPUT_MAX_BYTES:-${CLAUDE_TOKEN_STATUSLINE_INPUT_MAX_BYTES:-65536}}" max=65536
|
|
46
|
+
if [[ "$raw" =~ ^[0-9]+$ ]] && (( ${#raw} <= 7 )); then
|
|
47
|
+
max=$((10#$raw))
|
|
48
|
+
fi
|
|
49
|
+
if (( max < 1 || max > 1048576 )); then
|
|
50
|
+
max=65536
|
|
51
|
+
fi
|
|
52
|
+
printf '%s\n' "$max"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
read_bounded_statusline_input() {
|
|
56
|
+
local max input_len tmp_base
|
|
57
|
+
max=$(statusline_input_max_bytes)
|
|
58
|
+
tmp_base=$(statusline_tmp_base)
|
|
59
|
+
statusline_input_tmp=$(mktemp "$tmp_base/context-guard-statusline.XXXXXX") || {
|
|
60
|
+
printf '[input-error] could not create statusline input buffer\n'
|
|
61
|
+
exit 0
|
|
62
|
+
}
|
|
63
|
+
trap 'rm -f "${statusline_input_tmp:-}"' EXIT
|
|
64
|
+
LC_ALL=C head -c "$((max + 1))" >"$statusline_input_tmp" 2>/dev/null || true
|
|
65
|
+
input_len=$(LC_ALL=C wc -c <"$statusline_input_tmp" | tr -d '[:space:]')
|
|
66
|
+
if (( input_len > max )); then
|
|
67
|
+
printf '[input-too-large] Claude statusline JSON exceeds %s bytes\n' "$max"
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
input=$(cat "$statusline_input_tmp" 2>/dev/null || true)
|
|
71
|
+
rm -f "$statusline_input_tmp"
|
|
72
|
+
statusline_input_tmp=''
|
|
73
|
+
trap - EXIT
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
read_bounded_statusline_input
|
|
77
|
+
|
|
78
|
+
strip_terminal_sequences() {
|
|
79
|
+
if command -v perl >/dev/null 2>&1; then
|
|
80
|
+
perl -pe 's/\e\][^\a\e]*(?:\a|\e\\)//g; s/\e[@-_][0-?]*[ -\/]*[@-~]//g'
|
|
81
|
+
else
|
|
82
|
+
cat
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sanitize_statusline() {
|
|
87
|
+
# Claude statusline output must stay a single bounded terminal line. Treat
|
|
88
|
+
# helper output as display data, not trusted terminal control text.
|
|
89
|
+
local cleaned
|
|
90
|
+
cleaned=$(printf '%s' "$1" \
|
|
91
|
+
| strip_terminal_sequences \
|
|
92
|
+
| LC_ALL=C tr '\r\n' ' ' \
|
|
93
|
+
| LC_ALL=C tr -d '\000-\010\013\014\016-\037\177-\237' \
|
|
94
|
+
| cut -c 1-1000)
|
|
95
|
+
if printf '%s' "$cleaned" | LC_ALL=C grep -Eiq '(gh[pousr]_|github_pat_|glpat-|xox[abprs]-|AKIA|ASIA|sk-|npm_|AIza|Bearer[[:space:]]|Basic[[:space:]])'; then
|
|
96
|
+
printf '[redacted]'
|
|
97
|
+
else
|
|
98
|
+
printf '%s' "$cleaned"
|
|
99
|
+
fi
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ── 1) OMC HUD 출력 ──────────────────────────────────────────────────────────
|
|
103
|
+
omc_out=''
|
|
104
|
+
omc_script="${OMC_HUD_SCRIPT:-$HOME/.claude/hud/omc-hud.mjs}"
|
|
105
|
+
if [[ -r "$omc_script" ]] && command -v node >/dev/null 2>&1; then
|
|
106
|
+
omc_out=$(printf '%s' "$input" | node "$omc_script" 2>/dev/null || true)
|
|
107
|
+
omc_out=$(sanitize_statusline "$omc_out")
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# ── 2) context-guard-statusline 바이너리 위치 결정 ────────────────────────────
|
|
111
|
+
# 우선순위: 환경변수 → 자기 옆 디렉토리
|
|
112
|
+
# PATH fallback 은 workspace/plugin 경로 shadowing 으로 untrusted binary 를
|
|
113
|
+
# 실행할 수 있어 사용하지 않는다. 외부 바이너리를 쓰려면 명시적으로
|
|
114
|
+
# CONTEXT_GUARD_STATUSLINE_BIN 을 지정해야 한다.
|
|
115
|
+
tok_bin="${CONTEXT_GUARD_STATUSLINE_BIN:-${CLAUDE_TOKEN_STATUSLINE_BIN:-}}"
|
|
116
|
+
if [[ -z "$tok_bin" ]]; then
|
|
117
|
+
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)"
|
|
118
|
+
for cand in \
|
|
119
|
+
"$self_dir/context-guard-statusline" \
|
|
120
|
+
"$self_dir/statusline.sh"; do
|
|
121
|
+
if [[ -x "$cand" ]]; then
|
|
122
|
+
tok_bin="$cand"
|
|
123
|
+
break
|
|
124
|
+
fi
|
|
125
|
+
done
|
|
126
|
+
fi
|
|
127
|
+
tok_out=''
|
|
128
|
+
if [[ -n "$tok_bin" && -x "$tok_bin" ]]; then
|
|
129
|
+
tok_out=$(printf '%s' "$input" | "$tok_bin" 2>/dev/null || true)
|
|
130
|
+
tok_out=$(sanitize_statusline "$tok_out")
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# ── 3) 결합: OMC HUD 가 살아있을 때만 token 출력에서 compact extras 만 뽑아 붙임 ─
|
|
134
|
+
# token-statusline 형식:
|
|
135
|
+
# "[model] dir | branch | ctx N% | cost $N.NNN | cache N% | reuse N.Nx"
|
|
136
|
+
# OMC HUD 와 중복되는 model/dir/branch/ctx 는 버리고 cost/cache/reuse 만 채택한다.
|
|
137
|
+
extras=''
|
|
138
|
+
if [[ -n "$omc_out" && -n "$tok_out" ]]; then
|
|
139
|
+
if [[ "$tok_out" =~ \|[[:space:]]+cost[[:space:]]+(\$[0-9.]+|n/a) ]]; then
|
|
140
|
+
extras+=" | cost ${BASH_REMATCH[1]}"
|
|
141
|
+
fi
|
|
142
|
+
if [[ "$tok_out" =~ \|[[:space:]]+cache[[:space:]]+([0-9]+%) ]]; then
|
|
143
|
+
extras+=" | cache ${BASH_REMATCH[1]}"
|
|
144
|
+
fi
|
|
145
|
+
if [[ "$tok_out" =~ \|[[:space:]]+reuse[[:space:]]+([0-9]+(\.[0-9]+)?x|n/a) ]]; then
|
|
146
|
+
extras+=" | reuse ${BASH_REMATCH[1]}"
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# ── 4) 출력 ──────────────────────────────────────────────────────────────────
|
|
151
|
+
if [[ -n "$omc_out" ]]; then
|
|
152
|
+
printf '%s%s\n' "$omc_out" "$extras"
|
|
153
|
+
elif [[ -n "$tok_out" ]]; then
|
|
154
|
+
printf '%s\n' "$tok_out"
|
|
155
|
+
else
|
|
156
|
+
printf '[hud unavailable]\n'
|
|
157
|
+
fi
|