cc-context-stats 1.3.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 (72) hide show
  1. package/.claude/commands/context-stats.md +17 -0
  2. package/.claude/settings.local.json +85 -0
  3. package/.editorconfig +60 -0
  4. package/.eslintrc.json +35 -0
  5. package/.github/dependabot.yml +44 -0
  6. package/.github/workflows/ci.yml +255 -0
  7. package/.github/workflows/release.yml +149 -0
  8. package/.pre-commit-config.yaml +74 -0
  9. package/.prettierrc +33 -0
  10. package/.shellcheckrc +10 -0
  11. package/CHANGELOG.md +100 -0
  12. package/CONTRIBUTING.md +240 -0
  13. package/PUBLISHING_GUIDE.md +69 -0
  14. package/README.md +179 -0
  15. package/config/settings-example.json +7 -0
  16. package/config/settings-node.json +7 -0
  17. package/config/settings-python.json +7 -0
  18. package/docs/configuration.md +83 -0
  19. package/docs/context-stats.md +132 -0
  20. package/docs/installation.md +195 -0
  21. package/docs/scripts.md +116 -0
  22. package/docs/troubleshooting.md +189 -0
  23. package/images/claude-statusline-token-graph.gif +0 -0
  24. package/images/claude-statusline.png +0 -0
  25. package/images/context-status-dumbzone.png +0 -0
  26. package/images/context-status.png +0 -0
  27. package/images/statusline-detail.png +0 -0
  28. package/images/token-graph.jpeg +0 -0
  29. package/images/token-graph.png +0 -0
  30. package/install +344 -0
  31. package/install.sh +272 -0
  32. package/jest.config.js +11 -0
  33. package/npm-publish.sh +33 -0
  34. package/package.json +36 -0
  35. package/publish.sh +24 -0
  36. package/pyproject.toml +113 -0
  37. package/requirements-dev.txt +12 -0
  38. package/scripts/context-stats.sh +970 -0
  39. package/scripts/statusline-full.sh +241 -0
  40. package/scripts/statusline-git.sh +32 -0
  41. package/scripts/statusline-minimal.sh +11 -0
  42. package/scripts/statusline.js +350 -0
  43. package/scripts/statusline.py +312 -0
  44. package/show_raw_claude_code_api.js +11 -0
  45. package/src/claude_statusline/__init__.py +11 -0
  46. package/src/claude_statusline/__main__.py +6 -0
  47. package/src/claude_statusline/cli/__init__.py +1 -0
  48. package/src/claude_statusline/cli/context_stats.py +379 -0
  49. package/src/claude_statusline/cli/statusline.py +172 -0
  50. package/src/claude_statusline/core/__init__.py +1 -0
  51. package/src/claude_statusline/core/colors.py +55 -0
  52. package/src/claude_statusline/core/config.py +98 -0
  53. package/src/claude_statusline/core/git.py +67 -0
  54. package/src/claude_statusline/core/state.py +266 -0
  55. package/src/claude_statusline/formatters/__init__.py +1 -0
  56. package/src/claude_statusline/formatters/time.py +50 -0
  57. package/src/claude_statusline/formatters/tokens.py +70 -0
  58. package/src/claude_statusline/graphs/__init__.py +1 -0
  59. package/src/claude_statusline/graphs/renderer.py +346 -0
  60. package/src/claude_statusline/graphs/statistics.py +58 -0
  61. package/tests/bash/test_install.bats +29 -0
  62. package/tests/bash/test_statusline_full.bats +109 -0
  63. package/tests/bash/test_statusline_git.bats +42 -0
  64. package/tests/bash/test_statusline_minimal.bats +37 -0
  65. package/tests/fixtures/json/high_usage.json +17 -0
  66. package/tests/fixtures/json/low_usage.json +17 -0
  67. package/tests/fixtures/json/medium_usage.json +17 -0
  68. package/tests/fixtures/json/valid_full.json +30 -0
  69. package/tests/fixtures/json/valid_minimal.json +9 -0
  70. package/tests/node/statusline.test.js +199 -0
  71. package/tests/python/conftest.py +84 -0
  72. package/tests/python/test_statusline.py +154 -0
@@ -0,0 +1,970 @@
1
+ #!/bin/bash
2
+ # Context Stats Visualizer for Claude Code
3
+ # Displays ASCII graphs of token consumption over time
4
+ #
5
+ # Usage:
6
+ # context-stats.sh [session_id] [options]
7
+ #
8
+ # Options:
9
+ # --type <cumulative|delta|both> Graph type to display (default: both)
10
+ # --watch, -w [interval] Real-time monitoring mode (default: 2s)
11
+ # --no-color Disable color output
12
+ # --help Show this help
13
+ #
14
+ # Examples:
15
+ # context-stats.sh # Latest session, both graphs
16
+ # context-stats.sh abc123 # Specific session
17
+ # context-stats.sh --type delta # Only delta graph
18
+ # context-stats.sh --watch # Real-time mode (2s refresh)
19
+ # context-stats.sh -w 5 # Real-time mode (5s refresh)
20
+
21
+ # Note: This script is compatible with bash 3.2+ (macOS default)
22
+
23
+ # === CONFIGURATION ===
24
+ # shellcheck disable=SC2034
25
+ VERSION="1.2.3"
26
+ COMMIT_HASH="dev" # Will be replaced during installation
27
+ STATE_DIR=~/.claude/statusline
28
+ CONFIG_FILE=~/.claude/statusline.conf
29
+
30
+ # === COLOR DEFINITIONS ===
31
+ BLUE='\033[0;34m'
32
+ MAGENTA='\033[0;35m'
33
+ CYAN='\033[0;36m'
34
+ GREEN='\033[0;32m'
35
+ YELLOW='\033[0;33m'
36
+ RED='\033[0;31m'
37
+ BOLD='\033[1m'
38
+ DIM='\033[2m'
39
+ RESET='\033[0m'
40
+
41
+ # === GLOBAL VARIABLES ===
42
+ # Use simple arrays for bash 3.2 compatibility
43
+ TIMESTAMPS=""
44
+ TOKENS=""
45
+ INPUT_TOKENS=""
46
+ OUTPUT_TOKENS=""
47
+ DELTAS=""
48
+ DELTA_TIMES=""
49
+ DATA_COUNT=0
50
+ TERM_WIDTH=80
51
+ TERM_HEIGHT=24
52
+ GRAPH_WIDTH=60
53
+ GRAPH_HEIGHT=15
54
+ SESSION_ID=""
55
+ GRAPH_TYPE="delta"
56
+ COLOR_ENABLED=true
57
+ TOKEN_DETAIL_ENABLED=true
58
+ WATCH_MODE=true
59
+ WATCH_INTERVAL=2
60
+
61
+ # === UTILITY FUNCTIONS ===
62
+
63
+ show_help() {
64
+ cat <<'EOF'
65
+ Context Stats Visualizer for Claude Code
66
+
67
+ USAGE:
68
+ context-stats.sh [session_id] [options]
69
+
70
+ ARGUMENTS:
71
+ session_id Optional session ID. If not provided, uses the latest session.
72
+
73
+ OPTIONS:
74
+ --type <type> Graph type to display:
75
+ - delta: Context growth per interaction (default)
76
+ - cumulative: Total context usage over time
77
+ - io: Input/output tokens over time
78
+ - both: Show cumulative and delta graphs
79
+ - all: Show all graphs including I/O
80
+ -w [interval] Set refresh interval in seconds (default: 2)
81
+ --no-watch Show graphs once and exit (disable live monitoring)
82
+ --no-color Disable color output
83
+ --help Show this help message
84
+
85
+ NOTE:
86
+ By default, context-stats runs in live monitoring mode, refreshing every 2 seconds.
87
+ Press Ctrl+C to exit. Use --no-watch to display graphs once and exit.
88
+
89
+ EXAMPLES:
90
+ # Live monitoring (default, refreshes every 2s)
91
+ context-stats.sh
92
+
93
+ # Live monitoring with custom interval
94
+ context-stats.sh -w 5
95
+
96
+ # Show graphs once and exit
97
+ context-stats.sh --no-watch
98
+
99
+ # Show graphs for specific session
100
+ context-stats.sh abc123def
101
+
102
+ # Show cumulative graph instead of delta
103
+ context-stats.sh --type cumulative
104
+
105
+ # Combine options
106
+ context-stats.sh abc123 --type cumulative -w 3
107
+
108
+ # Output to file (no colors, single run)
109
+ context-stats.sh --no-watch --no-color > output.txt
110
+
111
+ DATA SOURCE:
112
+ Reads token history from ~/.claude/statusline/statusline.<session_id>.state
113
+
114
+ EOF
115
+ }
116
+
117
+ error_exit() {
118
+ echo -e "${RED}Error:${RESET} $1" >&2
119
+ exit "${2:-1}"
120
+ }
121
+
122
+ warn() {
123
+ echo -e "${YELLOW}Warning:${RESET} $1" >&2
124
+ }
125
+
126
+ info() {
127
+ echo -e "${DIM}$1${RESET}"
128
+ }
129
+
130
+ show_waiting_message() {
131
+ local session_id=$1
132
+ local message=${2:-"Waiting for session data..."}
133
+
134
+ echo ""
135
+ if [ -n "$session_id" ]; then
136
+ echo -e "${BOLD}${MAGENTA}Context Stats${RESET} ${DIM}(Session: $session_id)${RESET}"
137
+ else
138
+ echo -e "${BOLD}${MAGENTA}Context Stats${RESET}"
139
+ fi
140
+ echo ""
141
+ echo -e " ${CYAN}⏳ ${message}${RESET}"
142
+ echo ""
143
+ echo -e " ${DIM}The session has just started and no data has been recorded yet.${RESET}"
144
+ echo -e " ${DIM}Data will appear after the first Claude interaction.${RESET}"
145
+ echo ""
146
+ }
147
+
148
+ init_colors() {
149
+ if [ "$COLOR_ENABLED" != "true" ] || [ "${NO_COLOR:-}" = "1" ] || [ ! -t 1 ]; then
150
+ # shellcheck disable=SC2034
151
+ BLUE='' # Kept for consistency with other color definitions
152
+ MAGENTA=''
153
+ CYAN=''
154
+ GREEN=''
155
+ YELLOW=''
156
+ RED=''
157
+ BOLD=''
158
+ DIM=''
159
+ RESET=''
160
+ fi
161
+ }
162
+
163
+ get_terminal_dimensions() {
164
+ # Try tput first
165
+ if command -v tput >/dev/null 2>&1; then
166
+ TERM_WIDTH=$(tput cols 2>/dev/null || echo 80)
167
+ TERM_HEIGHT=$(tput lines 2>/dev/null || echo 24)
168
+ else
169
+ # Fallback to stty
170
+ local dims
171
+ dims=$(stty size 2>/dev/null || echo "24 80")
172
+ TERM_HEIGHT=$(echo "$dims" | cut -d' ' -f1)
173
+ TERM_WIDTH=$(echo "$dims" | cut -d' ' -f2)
174
+ fi
175
+
176
+ # Calculate graph dimensions
177
+ GRAPH_WIDTH=$((TERM_WIDTH - 15)) # Reserve space for Y-axis labels
178
+ GRAPH_HEIGHT=$((TERM_HEIGHT / 3)) # Each graph takes 1/3 of terminal
179
+
180
+ # Enforce minimums and maximums
181
+ [ $GRAPH_WIDTH -lt 30 ] && GRAPH_WIDTH=30
182
+ [ $GRAPH_HEIGHT -lt 8 ] && GRAPH_HEIGHT=8
183
+ [ $GRAPH_HEIGHT -gt 20 ] && GRAPH_HEIGHT=20
184
+ }
185
+
186
+ format_number() {
187
+ local num=$1
188
+ if [ "$TOKEN_DETAIL_ENABLED" = "true" ]; then
189
+ # Comma-separated format
190
+ echo "$num" | awk '{ printf "%\047d", $1 }' 2>/dev/null || echo "$num"
191
+ else
192
+ # Abbreviated format
193
+ echo "$num" | awk '{
194
+ if ($1 >= 1000000) printf "%.1fM", $1/1000000
195
+ else if ($1 >= 1000) printf "%.1fk", $1/1000
196
+ else printf "%d", $1
197
+ }'
198
+ fi
199
+ }
200
+
201
+ format_timestamp() {
202
+ local ts=$1
203
+ # Try BSD date first (macOS), then GNU date
204
+ date -r "$ts" +%H:%M 2>/dev/null || date -d "@$ts" +%H:%M 2>/dev/null || echo "$ts"
205
+ }
206
+
207
+ format_duration() {
208
+ local seconds=$1
209
+ local hours=$((seconds / 3600))
210
+ local minutes=$(((seconds % 3600) / 60))
211
+
212
+ if [ $hours -gt 0 ]; then
213
+ echo "${hours}h ${minutes}m"
214
+ elif [ $minutes -gt 0 ]; then
215
+ echo "${minutes}m"
216
+ else
217
+ echo "${seconds}s"
218
+ fi
219
+ }
220
+
221
+ # === DATA FUNCTIONS ===
222
+
223
+ migrate_old_state_files() {
224
+ local old_dir=~/.claude
225
+ local new_file
226
+ mkdir -p "$STATE_DIR"
227
+ for old_file in "$old_dir"/statusline*.state; do
228
+ if [ -f "$old_file" ]; then
229
+ new_file="${STATE_DIR}/$(basename "$old_file")"
230
+ if [ ! -f "$new_file" ]; then
231
+ mv "$old_file" "$new_file" 2>/dev/null || true
232
+ else
233
+ rm -f "$old_file" 2>/dev/null || true
234
+ fi
235
+ fi
236
+ done
237
+ }
238
+
239
+ find_latest_state_file() {
240
+ migrate_old_state_files
241
+
242
+ if [ -n "$SESSION_ID" ]; then
243
+ # Specific session requested - return path even if file doesn't exist yet
244
+ local file="$STATE_DIR/statusline.${SESSION_ID}.state"
245
+ echo "$file"
246
+ return 0
247
+ fi
248
+
249
+ # Find most recent state file
250
+ local latest
251
+ latest=$(find "$STATE_DIR" -maxdepth 1 -name 'statusline.*.state' -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
252
+
253
+ if [ -z "$latest" ]; then
254
+ # Try the default state file
255
+ if [ -f "$STATE_DIR/statusline.state" ]; then
256
+ echo "$STATE_DIR/statusline.state"
257
+ return 0
258
+ fi
259
+ # Return empty - no state files found
260
+ return 1
261
+ fi
262
+
263
+ echo "$latest"
264
+ }
265
+
266
+ validate_state_file() {
267
+ local file=$1
268
+
269
+ if [ ! -f "$file" ]; then
270
+ error_exit "State file not found: $file"
271
+ fi
272
+
273
+ if [ ! -r "$file" ]; then
274
+ error_exit "Cannot read state file: $file"
275
+ fi
276
+
277
+ local line_count
278
+ line_count=$(wc -l <"$file" | tr -d ' ')
279
+
280
+ if [ "$line_count" -lt 2 ]; then
281
+ error_exit "Need at least 2 data points to generate graphs.\nFound: $line_count entry. Use Claude Code to accumulate more data."
282
+ fi
283
+ }
284
+
285
+ load_token_history() {
286
+ local file=$1
287
+ local line_num=0
288
+ local valid_lines=0
289
+ local skipped_lines=0
290
+
291
+ TIMESTAMPS=""
292
+ TOKENS=""
293
+ INPUT_TOKENS=""
294
+ OUTPUT_TOKENS=""
295
+ CONTEXT_SIZES=""
296
+ CURRENT_USED_TOKENS=""
297
+ CURRENT_INPUT_TOKENS=""
298
+ CURRENT_OUTPUT_TOKENS=""
299
+ LAST_MODEL_ID=""
300
+ LAST_PROJECT_DIR=""
301
+ LAST_COST_USD=""
302
+ LAST_LINES_ADDED=""
303
+ LAST_LINES_REMOVED=""
304
+ DATA_COUNT=0
305
+
306
+ while IFS=',' read -r ts total_in total_out cur_in cur_out cache_creation cache_read cost_usd lines_added lines_removed session_id model_id workspace_project_dir context_size rest || [ -n "$ts" ]; do
307
+ line_num=$((line_num + 1))
308
+
309
+ # Skip empty lines
310
+ [ -z "$ts" ] && continue
311
+
312
+ # Validate timestamp (simple numeric check)
313
+ case "$ts" in
314
+ '' | *[!0-9]*)
315
+ skipped_lines=$((skipped_lines + 1))
316
+ [ $skipped_lines -le 3 ] && warn "Skipping invalid line $line_num"
317
+ continue
318
+ ;;
319
+ esac
320
+
321
+ # Handle both old format (timestamp,tokens) and new format (timestamp,total_in,total_out,...)
322
+ if [ -z "$total_out" ]; then
323
+ # Old format: timestamp,tokens - use tokens as both input and output combined
324
+ local tok="$total_in"
325
+ case "$tok" in
326
+ '' | *[!0-9]*)
327
+ skipped_lines=$((skipped_lines + 1))
328
+ continue
329
+ ;;
330
+ esac
331
+ total_in=$tok
332
+ total_out=0
333
+ fi
334
+
335
+ # Validate numeric fields
336
+ case "$total_in" in
337
+ '' | *[!0-9]*) total_in=0 ;;
338
+ esac
339
+ case "$total_out" in
340
+ '' | *[!0-9]*) total_out=0 ;;
341
+ esac
342
+ case "$cur_in" in
343
+ '' | *[!0-9]*) cur_in=0 ;;
344
+ esac
345
+ case "$cache_creation" in
346
+ '' | *[!0-9]*) cache_creation=0 ;;
347
+ esac
348
+ case "$cache_read" in
349
+ '' | *[!0-9]*) cache_read=0 ;;
350
+ esac
351
+
352
+ # Calculate combined tokens for backward compatibility
353
+ local combined=$((total_in + total_out))
354
+
355
+ # Calculate current context usage (what's actually in the context window)
356
+ local current_used=$((cur_in + cache_creation + cache_read))
357
+
358
+ # Validate context size (new format)
359
+ case "$context_size" in
360
+ '' | *[!0-9]*) context_size=0 ;;
361
+ esac
362
+
363
+ # Append to space-separated strings (bash 3.2 compatible)
364
+ if [ -z "$TIMESTAMPS" ]; then
365
+ TIMESTAMPS="$ts"
366
+ TOKENS="$combined"
367
+ INPUT_TOKENS="$total_in"
368
+ OUTPUT_TOKENS="$total_out"
369
+ CONTEXT_SIZES="$context_size"
370
+ CURRENT_USED_TOKENS="$current_used"
371
+ CURRENT_INPUT_TOKENS="$cur_in"
372
+ CURRENT_OUTPUT_TOKENS="$cur_out"
373
+ else
374
+ TIMESTAMPS="$TIMESTAMPS $ts"
375
+ TOKENS="$TOKENS $combined"
376
+ INPUT_TOKENS="$INPUT_TOKENS $total_in"
377
+ OUTPUT_TOKENS="$OUTPUT_TOKENS $total_out"
378
+ CONTEXT_SIZES="$CONTEXT_SIZES $context_size"
379
+ CURRENT_USED_TOKENS="$CURRENT_USED_TOKENS $current_used"
380
+ CURRENT_INPUT_TOKENS="$CURRENT_INPUT_TOKENS $cur_in"
381
+ CURRENT_OUTPUT_TOKENS="$CURRENT_OUTPUT_TOKENS $cur_out"
382
+ fi
383
+ # Store last values (will be kept)
384
+ LAST_MODEL_ID="$model_id"
385
+ LAST_PROJECT_DIR="$workspace_project_dir"
386
+ LAST_COST_USD="$cost_usd"
387
+ LAST_LINES_ADDED="$lines_added"
388
+ LAST_LINES_REMOVED="$lines_removed"
389
+ valid_lines=$((valid_lines + 1))
390
+ done <"$file"
391
+
392
+ DATA_COUNT=$valid_lines
393
+
394
+ if [ $skipped_lines -gt 3 ]; then
395
+ warn "... and $((skipped_lines - 3)) more invalid lines"
396
+ fi
397
+
398
+ if [ $valid_lines -lt 2 ]; then
399
+ error_exit "Loaded only $valid_lines valid data points. Need at least 2."
400
+ fi
401
+
402
+ # Only show info message in non-watch mode
403
+ if [ "$WATCH_MODE" != "true" ]; then
404
+ info "Loaded $valid_lines data points from $(basename "$file")"
405
+ fi
406
+ }
407
+
408
+ calculate_deltas() {
409
+ local prev_tok=""
410
+ local idx=0
411
+ DELTAS=""
412
+ DELTA_TIMES=""
413
+
414
+ # Use CURRENT_USED_TOKENS (actual context usage) for delta calculation
415
+ for tok in $CURRENT_USED_TOKENS; do
416
+ idx=$((idx + 1))
417
+ if [ -z "$prev_tok" ]; then
418
+ # Skip first data point - no previous value to compare against
419
+ prev_tok=$tok
420
+ continue
421
+ fi
422
+
423
+ local delta=$((tok - prev_tok))
424
+ # Handle negative deltas (session reset) by showing 0
425
+ [ $delta -lt 0 ] && delta=0
426
+
427
+ # Get corresponding timestamp for this delta
428
+ local ts
429
+ ts=$(get_element "$TIMESTAMPS" "$idx")
430
+
431
+ if [ -z "$DELTAS" ]; then
432
+ DELTAS="$delta"
433
+ DELTA_TIMES="$ts"
434
+ else
435
+ DELTAS="$DELTAS $delta"
436
+ DELTA_TIMES="$DELTA_TIMES $ts"
437
+ fi
438
+ prev_tok=$tok
439
+ done
440
+ }
441
+
442
+ # Get Nth element from space-separated string (1-indexed)
443
+ get_element() {
444
+ local str=$1
445
+ local idx=$2
446
+ echo "$str" | awk -v n="$idx" '{ print $n }'
447
+ }
448
+
449
+ # Get min/max/avg from space-separated numbers
450
+ get_stats() {
451
+ local data=$1
452
+ echo "$data" | tr ' ' '\n' | awk '
453
+ BEGIN { min=999999999999; max=0; sum=0; n=0 }
454
+ {
455
+ if ($1 < min) min = $1
456
+ if ($1 > max) max = $1
457
+ sum += $1
458
+ n++
459
+ }
460
+ END {
461
+ avg = (n > 0) ? int(sum/n) : 0
462
+ print min, max, avg, sum, n
463
+ }
464
+ '
465
+ }
466
+
467
+ # === GRAPH RENDERING ===
468
+
469
+ render_timeseries_graph() {
470
+ local title=$1
471
+ local data=$2
472
+ local times=$3
473
+ local color=$4
474
+
475
+ local n
476
+ n=$(echo "$data" | wc -w | tr -d ' ')
477
+ [ "$n" -eq 0 ] && return
478
+
479
+ # Get min/max
480
+ local stats
481
+ stats=$(get_stats "$data")
482
+ local min max
483
+ min=$(echo "$stats" | cut -d' ' -f1)
484
+ max=$(echo "$stats" | cut -d' ' -f2)
485
+ # avg is available but not used in graph rendering
486
+
487
+ # Avoid division by zero
488
+ [ "$min" -eq "$max" ] && max=$((min + 1))
489
+ local range=$((max - min))
490
+
491
+ # Print title
492
+ echo ""
493
+ echo -e "${BOLD}$title${RESET}"
494
+ echo -e "${DIM}Max: $(format_number "$max") Min: $(format_number "$min") Points: $n${RESET}"
495
+ echo ""
496
+
497
+ # Build grid using awk - smooth line with filled area below
498
+ local grid_output
499
+ grid_output=$(echo "$data" | awk -v width="$GRAPH_WIDTH" -v height="$GRAPH_HEIGHT" \
500
+ -v min="$min" -v max="$max" -v range="$range" '
501
+ BEGIN {
502
+ # Characters for different parts of the graph
503
+ # Line: dots for the trend line
504
+ # Fill: lighter shading below the line
505
+ dot = "●"
506
+ fill_dark = "░"
507
+ fill_light = "▒"
508
+ empty = " "
509
+
510
+ # Initialize grid with empty spaces
511
+ for (r = 0; r < height; r++) {
512
+ for (c = 0; c < width; c++) {
513
+ grid[r,c] = empty
514
+ }
515
+ }
516
+
517
+ # Store y values for each x position (for interpolation)
518
+ for (c = 0; c < width; c++) {
519
+ line_y[c] = -1
520
+ }
521
+ }
522
+ {
523
+ n = NF
524
+
525
+ # First pass: calculate y position for each data point
526
+ for (i = 1; i <= n; i++) {
527
+ val = $i
528
+
529
+ # Map index to x coordinate
530
+ if (n == 1) {
531
+ x = int(width / 2)
532
+ } else {
533
+ x = int((i - 1) * (width - 1) / (n - 1))
534
+ }
535
+ if (x >= width) x = width - 1
536
+ if (x < 0) x = 0
537
+
538
+ # Map value to y coordinate (inverted: 0=top)
539
+ y = (max - val) * (height - 1) / range
540
+ if (y >= height) y = height - 1
541
+ if (y < 0) y = 0
542
+
543
+ data_x[i] = x
544
+ data_y[i] = y
545
+ }
546
+
547
+ # Second pass: interpolate between points to fill every x position
548
+ for (i = 1; i < n; i++) {
549
+ x1 = data_x[i]
550
+ y1 = data_y[i]
551
+ x2 = data_x[i+1]
552
+ y2 = data_y[i+1]
553
+
554
+ # Linear interpolation for each x between x1 and x2
555
+ for (x = x1; x <= x2; x++) {
556
+ if (x2 == x1) {
557
+ y = y1
558
+ } else {
559
+ # Linear interpolation
560
+ t = (x - x1) / (x2 - x1)
561
+ y = y1 + t * (y2 - y1)
562
+ }
563
+ line_y[x] = y
564
+ }
565
+ }
566
+
567
+ # Third pass: draw the filled area and line
568
+ for (c = 0; c < width; c++) {
569
+ if (line_y[c] >= 0) {
570
+ line_row = int(line_y[c] + 0.5) # Round to nearest integer
571
+ if (line_row >= height) line_row = height - 1
572
+ if (line_row < 0) line_row = 0
573
+
574
+ # Fill area below the line with gradient
575
+ for (r = line_row; r < height; r++) {
576
+ if (r == line_row) {
577
+ grid[r, c] = dot # The line itself
578
+ } else if (r < line_row + 2) {
579
+ grid[r, c] = fill_light # Darker fill near line
580
+ } else {
581
+ grid[r, c] = fill_dark # Lighter fill further down
582
+ }
583
+ }
584
+ }
585
+ }
586
+
587
+ # Fourth pass: mark actual data points with larger dots
588
+ for (i = 1; i <= n; i++) {
589
+ x = data_x[i]
590
+ y = int(data_y[i] + 0.5)
591
+ if (y >= height) y = height - 1
592
+ if (y < 0) y = 0
593
+ grid[y, x] = dot
594
+ }
595
+ }
596
+ END {
597
+ # Print grid
598
+ for (r = 0; r < height; r++) {
599
+ row = ""
600
+ for (c = 0; c < width; c++) {
601
+ row = row grid[r,c]
602
+ }
603
+ print row
604
+ }
605
+ }')
606
+
607
+ # Print grid with Y-axis labels
608
+ local r=0
609
+ while [ $r -lt $GRAPH_HEIGHT ]; do
610
+ local val=$((max - r * range / (GRAPH_HEIGHT - 1)))
611
+ local label=""
612
+
613
+ # Show labels at top, middle, and bottom
614
+ if [ $r -eq 0 ] || [ $r -eq $((GRAPH_HEIGHT / 2)) ] || [ $r -eq $((GRAPH_HEIGHT - 1)) ]; then
615
+ label=$(format_number $val)
616
+ fi
617
+
618
+ local row
619
+ row=$(echo "$grid_output" | sed -n "$((r + 1))p")
620
+ printf '%10s %b│%b%b%s%b\n' "$label" "${DIM}" "${RESET}" "${color}" "$row" "${RESET}"
621
+ r=$((r + 1))
622
+ done
623
+
624
+ # X-axis
625
+ printf '%10s %b└' "" "${DIM}"
626
+ local c=0
627
+ while [ $c -lt $GRAPH_WIDTH ]; do
628
+ printf "─"
629
+ c=$((c + 1))
630
+ done
631
+ printf '%b\n' "${RESET}"
632
+
633
+ # Time labels
634
+ local first_time last_time mid_time
635
+ first_time=$(format_timestamp "$(get_element "$times" 1)")
636
+ last_time=$(format_timestamp "$(get_element "$times" "$n")")
637
+ local mid_idx=$(((n + 1) / 2))
638
+ mid_time=$(format_timestamp "$(get_element "$times" "$mid_idx")")
639
+
640
+ printf '%11s%b%-*s%s%*s%b\n' "" "${DIM}" "$((GRAPH_WIDTH / 3))" "$first_time" "$mid_time" "$((GRAPH_WIDTH / 3))" "$last_time" "${RESET}"
641
+ }
642
+
643
+ render_summary() {
644
+ local first_ts last_ts duration current_tokens total_growth
645
+ first_ts=$(get_element "$TIMESTAMPS" 1)
646
+ last_ts=$(get_element "$TIMESTAMPS" "$DATA_COUNT")
647
+ duration=$((last_ts - first_ts))
648
+ current_tokens=$(get_element "$TOKENS" "$DATA_COUNT")
649
+ local first_tokens
650
+ first_tokens=$(get_element "$TOKENS" 1)
651
+ total_growth=$((current_tokens - first_tokens))
652
+
653
+ # Get I/O token stats (current request tokens for display)
654
+ local current_input current_output
655
+ current_input=$(get_element "$CURRENT_INPUT_TOKENS" "$DATA_COUNT")
656
+ current_output=$(get_element "$CURRENT_OUTPUT_TOKENS" "$DATA_COUNT")
657
+ current_context=$(get_element "$CONTEXT_SIZES" "$DATA_COUNT")
658
+
659
+ # Get actual context window usage (current_input + cache_creation + cache_read)
660
+ local current_used
661
+ current_used=$(get_element "$CURRENT_USED_TOKENS" "$DATA_COUNT")
662
+
663
+ # Calculate remaining context window
664
+ local remaining_context=$((current_context - current_used))
665
+ local context_percentage=0
666
+ if [ "$current_context" -gt 0 ]; then
667
+ context_percentage=$((remaining_context * 100 / current_context))
668
+ fi
669
+
670
+ # Get statistics
671
+ local del_stats
672
+ del_stats=$(get_stats "$DELTAS")
673
+ local del_max del_avg
674
+ del_max=$(echo "$del_stats" | cut -d' ' -f2)
675
+ del_avg=$(echo "$del_stats" | cut -d' ' -f3)
676
+
677
+ echo ""
678
+ echo -e "${BOLD}Session Summary${RESET}"
679
+ local line_width=$((GRAPH_WIDTH + 11))
680
+ printf '%b' "${DIM}"
681
+ local i=0
682
+ while [ $i -lt $line_width ]; do
683
+ printf "-"
684
+ i=$((i + 1))
685
+ done
686
+ printf '%b\n' "${RESET}"
687
+
688
+ # Determine status zone based on context usage
689
+ if [ "$current_context" -gt 0 ]; then
690
+ local usage_percentage=$((100 - context_percentage))
691
+ local status_color status_text status_hint
692
+ if [ "$usage_percentage" -lt 40 ]; then
693
+ status_color="${GREEN}"
694
+ status_text="SMART ZONE"
695
+ status_hint="You are in the smart zone"
696
+ elif [ "$usage_percentage" -lt 80 ]; then
697
+ status_color="${YELLOW}"
698
+ status_text="DUMB ZONE"
699
+ status_hint="You are in the dumb zone - Dex Horthy says so"
700
+ else
701
+ status_color="${RED}"
702
+ status_text="WRAP UP ZONE"
703
+ status_hint="Better to wrap up and start a new session"
704
+ fi
705
+ # Context remaining (before status)
706
+ printf ' %b%-20s%b %s/%s (%s%%)\n' "${status_color}" "Context Remaining:" "${RESET}" "$(format_number "$remaining_context")" "$(format_number "$current_context")" "$context_percentage"
707
+ # Status indicator
708
+ printf ' %b%b>>> %s <<<%b %b(%s)%b\n' "${status_color}" "${BOLD}" "$status_text" "${RESET}" "${DIM}" "$status_hint" "${RESET}"
709
+ echo ""
710
+ fi
711
+ # Session details (ordered: Last Growth, I/O, Lines, Cost, Model, Duration)
712
+ if [ -n "$DELTAS" ]; then
713
+ local delta_count last_growth
714
+ delta_count=$(echo "$DELTAS" | wc -w | tr -d ' ')
715
+ last_growth=$(get_element "$DELTAS" "$delta_count")
716
+ if [ -n "$last_growth" ] && [ "$last_growth" -gt 0 ] 2>/dev/null; then
717
+ printf ' %b%-20s%b +%s\n' "${CYAN}" "Last Growth:" "${RESET}" "$(format_number "$last_growth")"
718
+ fi
719
+ fi
720
+ printf ' %b%-20s%b %s\n' "${BLUE}" "Input Tokens:" "${RESET}" "$(format_number "$current_input")"
721
+ printf ' %b%-20s%b %s\n' "${MAGENTA}" "Output Tokens:" "${RESET}" "$(format_number "$current_output")"
722
+ if [ -n "$LAST_LINES_ADDED" ] && [ "$LAST_LINES_ADDED" != "0" ] || [ -n "$LAST_LINES_REMOVED" ] && [ "$LAST_LINES_REMOVED" != "0" ]; then
723
+ printf ' %b%-20s%b %b+%s%b / %b-%s%b\n' "${DIM}" "Lines Changed:" "${RESET}" "${GREEN}" "$(format_number "$LAST_LINES_ADDED")" "${RESET}" "${RED}" "$(format_number "$LAST_LINES_REMOVED")" "${RESET}"
724
+ fi
725
+ if [ -n "$LAST_COST_USD" ] && [ "$LAST_COST_USD" != "0" ]; then
726
+ printf ' %b%-20s%b $%s\n' "${YELLOW}" "Total Cost:" "${RESET}" "$LAST_COST_USD"
727
+ fi
728
+ if [ -n "$LAST_MODEL_ID" ]; then
729
+ printf ' %b%-20s%b %s\n' "${DIM}" "Model:" "${RESET}" "$LAST_MODEL_ID"
730
+ fi
731
+ printf ' %b%-20s%b %s\n' "${CYAN}" "Session Duration:" "${RESET}" "$(format_duration "$duration")"
732
+ echo ""
733
+ }
734
+
735
+ render_footer() {
736
+ echo -e "${DIM}Powered by ${CYAN}claude-statusline${DIM} v${VERSION}-${COMMIT_HASH} - https://github.com/luongnv89/cc-context-stats${RESET}"
737
+ echo ""
738
+ }
739
+
740
+ # === ARGUMENT PARSING ===
741
+
742
+ parse_args() {
743
+ while [ $# -gt 0 ]; do
744
+ case "$1" in
745
+ --help | -h)
746
+ show_help
747
+ exit 0
748
+ ;;
749
+ --no-color)
750
+ COLOR_ENABLED=false
751
+ shift
752
+ ;;
753
+ --no-watch)
754
+ WATCH_MODE=false
755
+ shift
756
+ ;;
757
+ -w)
758
+ # Set refresh interval
759
+ if [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+$ ]]; then
760
+ WATCH_INTERVAL="$2"
761
+ shift 2
762
+ else
763
+ shift
764
+ fi
765
+ ;;
766
+ --type)
767
+ if [ $# -lt 2 ]; then
768
+ error_exit "--type requires an argument: cumulative, delta, or both"
769
+ fi
770
+ case "$2" in
771
+ cumulative | delta | io | both | all)
772
+ GRAPH_TYPE="$2"
773
+ ;;
774
+ *)
775
+ error_exit "Invalid graph type: $2. Use: cumulative, delta, io, both, or all"
776
+ ;;
777
+ esac
778
+ shift 2
779
+ ;;
780
+ --*)
781
+ error_exit "Unknown option: $1\nUse --help for usage information."
782
+ ;;
783
+ *)
784
+ # Assume it's a session ID
785
+ if [ -z "$SESSION_ID" ]; then
786
+ SESSION_ID="$1"
787
+ else
788
+ error_exit "Unexpected argument: $1"
789
+ fi
790
+ shift
791
+ ;;
792
+ esac
793
+ done
794
+ }
795
+
796
+ # === MAIN ===
797
+
798
+ # Load configuration from file
799
+ load_config() {
800
+ if [ -f "$CONFIG_FILE" ]; then
801
+ while IFS='=' read -r key value || [ -n "$key" ]; do
802
+ # Skip comments and empty lines
803
+ case "$key" in
804
+ '#'* | '') continue ;;
805
+ esac
806
+
807
+ # Sanitize key and value
808
+ key=$(echo "$key" | tr -d '[:space:]')
809
+ value=$(echo "$value" | tr -d '"' | tr -d "'" | tr -d '[:space:]')
810
+
811
+ case "$key" in
812
+ token_detail)
813
+ if [ "$value" = "false" ]; then
814
+ TOKEN_DETAIL_ENABLED=false
815
+ fi
816
+ ;;
817
+ esac
818
+ done <"$CONFIG_FILE"
819
+ fi
820
+ }
821
+
822
+ # Render graphs once
823
+ render_once() {
824
+ local state_file=$1
825
+
826
+ # Load data
827
+ load_token_history "$state_file"
828
+ calculate_deltas
829
+
830
+ # Display header
831
+ local session_name project_name
832
+ session_name=$(basename "$state_file" .state | sed 's/statusline\.//')
833
+ # Extract project name from path (last component)
834
+ if [ -n "$LAST_PROJECT_DIR" ]; then
835
+ project_name=$(basename "$LAST_PROJECT_DIR")
836
+ fi
837
+ echo ""
838
+ if [ -n "$project_name" ]; then
839
+ echo -e "${BOLD}${MAGENTA}Context Stats${RESET} ${DIM}(${CYAN}$project_name${DIM} • $session_name)${RESET}"
840
+ else
841
+ echo -e "${BOLD}${MAGENTA}Context Stats${RESET} ${DIM}(Session: $session_name)${RESET}"
842
+ fi
843
+
844
+ # Render graphs (use CURRENT_USED_TOKENS for actual context window usage)
845
+ case "$GRAPH_TYPE" in
846
+ cumulative)
847
+ render_timeseries_graph "Context Usage Over Time" "$CURRENT_USED_TOKENS" "$TIMESTAMPS" "$GREEN"
848
+ ;;
849
+ delta)
850
+ render_timeseries_graph "Context Growth Per Interaction" "$DELTAS" "$DELTA_TIMES" "$CYAN"
851
+ ;;
852
+ io)
853
+ render_timeseries_graph "Input Tokens (per request)" "$CURRENT_INPUT_TOKENS" "$TIMESTAMPS" "$BLUE"
854
+ render_timeseries_graph "Output Tokens (per request)" "$CURRENT_OUTPUT_TOKENS" "$TIMESTAMPS" "$MAGENTA"
855
+ ;;
856
+ both)
857
+ render_timeseries_graph "Context Usage Over Time" "$CURRENT_USED_TOKENS" "$TIMESTAMPS" "$GREEN"
858
+ render_timeseries_graph "Context Growth Per Interaction" "$DELTAS" "$DELTA_TIMES" "$CYAN"
859
+ ;;
860
+ all)
861
+ render_timeseries_graph "Input Tokens (per request)" "$CURRENT_INPUT_TOKENS" "$TIMESTAMPS" "$BLUE"
862
+ render_timeseries_graph "Output Tokens (per request)" "$CURRENT_OUTPUT_TOKENS" "$TIMESTAMPS" "$MAGENTA"
863
+ render_timeseries_graph "Context Usage Over Time" "$CURRENT_USED_TOKENS" "$TIMESTAMPS" "$GREEN"
864
+ render_timeseries_graph "Context Growth Per Interaction" "$DELTAS" "$DELTA_TIMES" "$CYAN"
865
+ ;;
866
+ esac
867
+
868
+ # Render summary
869
+ render_summary
870
+
871
+ # Render footer
872
+ render_footer
873
+ }
874
+
875
+ # Watch mode - continuously refresh the display
876
+ run_watch_mode() {
877
+ local state_file=$1
878
+
879
+ # ANSI escape codes for cursor control (using $'...' for proper interpretation)
880
+ local CURSOR_HOME=$'\033[H'
881
+ local CLEAR_SCREEN=$'\033[2J'
882
+ local HIDE_CURSOR=$'\033[?25l'
883
+ local SHOW_CURSOR=$'\033[?25h'
884
+ local CLEAR_TO_END=$'\033[J'
885
+
886
+ # Set up signal handler for clean exit
887
+ trap 'printf "%s\n" "${SHOW_CURSOR}"; echo -e "${DIM}Watch mode stopped.${RESET}"; exit 0' INT TERM
888
+
889
+ # Hide cursor for cleaner display
890
+ printf "%s" "${HIDE_CURSOR}"
891
+
892
+ # Initial clear
893
+ printf "%s%s" "${CLEAR_SCREEN}" "${CURSOR_HOME}"
894
+
895
+ while true; do
896
+ # Move cursor to home position (top-left) instead of clearing
897
+ # This prevents flickering by overwriting in place
898
+ printf "%s" "${CURSOR_HOME}"
899
+
900
+ # Re-read terminal dimensions in case of resize
901
+ get_terminal_dimensions
902
+
903
+ # Show watch mode indicator with live timestamp
904
+ local current_time
905
+ current_time=$(date +%H:%M:%S)
906
+ echo -e "${DIM}[LIVE ${current_time}] Refresh: ${WATCH_INTERVAL}s | Ctrl+C to exit${RESET}"
907
+
908
+ # Handle case where state_file is empty (no sessions found at all)
909
+ if [ -z "$state_file" ]; then
910
+ show_waiting_message "" "Waiting for session data..."
911
+ # Re-validate and render (file might have new data)
912
+ elif [ -f "$state_file" ]; then
913
+ local line_count
914
+ line_count=$(wc -l <"$state_file" | tr -d ' ')
915
+ if [ "$line_count" -ge 2 ]; then
916
+ render_once "$state_file"
917
+ else
918
+ show_waiting_message "$SESSION_ID" "Waiting for more data points..."
919
+ echo -e " ${DIM}Current: ${line_count} point(s), need at least 2${RESET}"
920
+ fi
921
+ else
922
+ # File doesn't exist yet (new session)
923
+ show_waiting_message "$SESSION_ID" "Waiting for session data..."
924
+ fi
925
+
926
+ # Clear any remaining lines from previous render (in case terminal resized smaller)
927
+ printf "%s" "${CLEAR_TO_END}"
928
+
929
+ sleep "$WATCH_INTERVAL"
930
+ done
931
+ }
932
+
933
+ main() {
934
+ parse_args "$@"
935
+ init_colors
936
+ get_terminal_dimensions
937
+ load_config
938
+
939
+ # Find state file
940
+ local state_file
941
+ if ! state_file=$(find_latest_state_file); then
942
+ # No state files found at all
943
+ if [ "$WATCH_MODE" = "true" ]; then
944
+ # Watch mode - wait for data
945
+ run_watch_mode ""
946
+ else
947
+ # Single run mode - show friendly message
948
+ echo -e "${YELLOW}No session data found.${RESET}"
949
+ echo -e "${DIM}Run Claude Code to generate token usage data.${RESET}"
950
+ exit 0
951
+ fi
952
+ return
953
+ fi
954
+
955
+ if [ "$WATCH_MODE" = "true" ]; then
956
+ # Watch mode - don't exit on validation errors, keep trying
957
+ run_watch_mode "$state_file"
958
+ else
959
+ # Single run mode - check if file exists
960
+ if [ ! -f "$state_file" ]; then
961
+ # Specific session requested but file doesn't exist yet
962
+ show_waiting_message "$SESSION_ID"
963
+ exit 0
964
+ fi
965
+ validate_state_file "$state_file"
966
+ render_once "$state_file"
967
+ fi
968
+ }
969
+
970
+ main "$@"