upfynai-code 0.1.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 (65) hide show
  1. package/LICENSE +22 -0
  2. package/bin/cli.js +86 -0
  3. package/dist/assets/CanvasPanel-B48gAKVY.js +538 -0
  4. package/dist/assets/CanvasPanel-B48gAKVY.js.map +1 -0
  5. package/dist/assets/CanvasPanel-BsOG3EVs.css +1 -0
  6. package/dist/assets/index-CEhTwG68.css +1 -0
  7. package/dist/assets/index-GqAGWpJI.js +70 -0
  8. package/dist/assets/index-GqAGWpJI.js.map +1 -0
  9. package/dist/index.html +18 -0
  10. package/index.html +17 -0
  11. package/package.json +67 -0
  12. package/src/App.tsx +226 -0
  13. package/src/components/canvas/CanvasPanel.tsx +62 -0
  14. package/src/components/canvas/layout/graph-builder.ts +136 -0
  15. package/src/components/canvas/shapes/CompactionNodeShape.tsx +76 -0
  16. package/src/components/canvas/shapes/SessionNodeShape.tsx +93 -0
  17. package/src/components/canvas/shapes/StatuslineWidgetShape.tsx +125 -0
  18. package/src/components/canvas/shapes/TextResponseNodeShape.tsx +86 -0
  19. package/src/components/canvas/shapes/ToolCallNodeShape.tsx +107 -0
  20. package/src/components/canvas/shapes/ToolResultNodeShape.tsx +87 -0
  21. package/src/components/canvas/shapes/shared-styles.ts +35 -0
  22. package/src/components/chat/ChatPanel.tsx +96 -0
  23. package/src/components/chat/InputBar.tsx +81 -0
  24. package/src/components/chat/MessageList.tsx +130 -0
  25. package/src/components/chat/PermissionDialog.tsx +70 -0
  26. package/src/components/layout/FolderSelector.tsx +152 -0
  27. package/src/components/layout/ModelSelector.tsx +65 -0
  28. package/src/components/layout/SessionManager.tsx +115 -0
  29. package/src/components/statusline/StatuslineBar.tsx +114 -0
  30. package/src/main.tsx +10 -0
  31. package/src/server/claude-session.ts +156 -0
  32. package/src/server/index.ts +149 -0
  33. package/src/services/stream-consumer.ts +330 -0
  34. package/src/statusline-core/bin/statusline.sh +121 -0
  35. package/src/statusline-core/commands/sls-config.md +42 -0
  36. package/src/statusline-core/commands/sls-doctor.md +35 -0
  37. package/src/statusline-core/commands/sls-help.md +48 -0
  38. package/src/statusline-core/commands/sls-layout.md +38 -0
  39. package/src/statusline-core/commands/sls-preview.md +34 -0
  40. package/src/statusline-core/commands/sls-theme.md +40 -0
  41. package/src/statusline-core/installer.js +228 -0
  42. package/src/statusline-core/layouts/compact.sh +21 -0
  43. package/src/statusline-core/layouts/full.sh +62 -0
  44. package/src/statusline-core/layouts/standard.sh +39 -0
  45. package/src/statusline-core/lib/core.sh +389 -0
  46. package/src/statusline-core/lib/helpers.sh +81 -0
  47. package/src/statusline-core/lib/json-parser.sh +71 -0
  48. package/src/statusline-core/themes/catppuccin.sh +32 -0
  49. package/src/statusline-core/themes/default.sh +37 -0
  50. package/src/statusline-core/themes/gruvbox.sh +32 -0
  51. package/src/statusline-core/themes/nord.sh +32 -0
  52. package/src/statusline-core/themes/tokyo-night.sh +32 -0
  53. package/src/store/canvas-store.ts +50 -0
  54. package/src/store/chat-store.ts +60 -0
  55. package/src/store/permission-store.ts +29 -0
  56. package/src/store/session-store.ts +52 -0
  57. package/src/store/statusline-store.ts +160 -0
  58. package/src/styles/global.css +117 -0
  59. package/src/themes/index.ts +149 -0
  60. package/src/types/canvas-graph.ts +24 -0
  61. package/src/types/sdk-messages.ts +156 -0
  62. package/src/types/statusline-fields.ts +67 -0
  63. package/src/vite-env.d.ts +1 -0
  64. package/tsconfig.json +26 -0
  65. package/vite.config.ts +24 -0
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env bash
2
+ # skill-statusline v2.0 — Core engine
3
+ # Reads Claude Code JSON from stdin, computes all fields, renders via layout
4
+
5
+ STATUSLINE_DIR="${HOME}/.claude/statusline"
6
+ CONFIG_FILE="${HOME}/.claude/statusline-config.json"
7
+
8
+ # ── 0. Read stdin JSON ──
9
+ input=$(cat)
10
+
11
+ # ── 1. Source modules ──
12
+ source "${STATUSLINE_DIR}/json-parser.sh"
13
+ source "${STATUSLINE_DIR}/helpers.sh"
14
+
15
+ # ── 2. Load config ──
16
+ active_theme="default"
17
+ active_layout="standard"
18
+ cfg_warn_threshold=85
19
+ cfg_bar_width=40
20
+ cfg_show_burn_rate="false"
21
+ cfg_show_vim="true"
22
+ cfg_show_agent="true"
23
+
24
+ if [ -f "$CONFIG_FILE" ]; then
25
+ _cfg=$(cat "$CONFIG_FILE" 2>/dev/null)
26
+ _t=$(echo "$_cfg" | grep -o '"theme"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/')
27
+ _l=$(echo "$_cfg" | grep -o '"layout"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/')
28
+ [ -n "$_t" ] && active_theme="$_t"
29
+ [ -n "$_l" ] && active_layout="$_l"
30
+ _w=$(echo "$_cfg" | grep -o '"compaction_warning_threshold"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
31
+ _bw=$(echo "$_cfg" | grep -o '"bar_width"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
32
+ _br=$(echo "$_cfg" | grep -o '"show_burn_rate"[[:space:]]*:[[:space:]]*true' | head -1)
33
+ _sv=$(echo "$_cfg" | grep -o '"show_vim_mode"[[:space:]]*:[[:space:]]*false' | head -1)
34
+ _sa=$(echo "$_cfg" | grep -o '"show_agent_name"[[:space:]]*:[[:space:]]*false' | head -1)
35
+ [ -n "$_w" ] && cfg_warn_threshold="$_w"
36
+ [ -n "$_bw" ] && cfg_bar_width="$_bw"
37
+ [ -n "$_br" ] && cfg_show_burn_rate="true"
38
+ [ -n "$_sv" ] && cfg_show_vim="false"
39
+ [ -n "$_sa" ] && cfg_show_agent="false"
40
+ fi
41
+
42
+ # Allow env override (for ccsl preview)
43
+ [ -n "$STATUSLINE_THEME_OVERRIDE" ] && active_theme="$STATUSLINE_THEME_OVERRIDE"
44
+ [ -n "$STATUSLINE_LAYOUT_OVERRIDE" ] && active_layout="$STATUSLINE_LAYOUT_OVERRIDE"
45
+
46
+ # ── 3. Source theme ──
47
+ theme_file="${STATUSLINE_DIR}/themes/${active_theme}.sh"
48
+ if [ -f "$theme_file" ]; then
49
+ source "$theme_file"
50
+ else
51
+ source "${STATUSLINE_DIR}/themes/default.sh"
52
+ fi
53
+
54
+ # ── 4. Terminal width detection ──
55
+ SL_TERM_WIDTH=${COLUMNS:-0}
56
+ if [ "$SL_TERM_WIDTH" -eq 0 ] 2>/dev/null; then
57
+ _tw=$(tput cols 2>/dev/null)
58
+ [ -n "$_tw" ] && [ "$_tw" -gt 0 ] && SL_TERM_WIDTH="$_tw"
59
+ fi
60
+ [ "$SL_TERM_WIDTH" -eq 0 ] && SL_TERM_WIDTH=80
61
+
62
+ # Auto-downgrade layout for narrow terminals
63
+ if [ "$SL_TERM_WIDTH" -lt 60 ]; then
64
+ active_layout="compact"
65
+ elif [ "$SL_TERM_WIDTH" -lt 80 ] && [ "$active_layout" = "full" ]; then
66
+ active_layout="standard"
67
+ fi
68
+
69
+ # Dynamic bar width
70
+ BAR_WIDTH="$cfg_bar_width"
71
+ if [ "$SL_TERM_WIDTH" -gt 100 ]; then
72
+ _dyn=$(( SL_TERM_WIDTH - 20 ))
73
+ [ "$_dyn" -gt 60 ] && _dyn=60
74
+ [ "$_dyn" -gt "$BAR_WIDTH" ] && BAR_WIDTH="$_dyn"
75
+ elif [ "$SL_TERM_WIDTH" -lt 70 ]; then
76
+ BAR_WIDTH=20
77
+ fi
78
+
79
+ # ── 5. Initialize cache ──
80
+ _sl_cache_init
81
+
82
+ # ── 6. Parse ALL JSON fields ──
83
+
84
+ # --- 6a. Directory ---
85
+ SL_CWD=$(json_nested_val "workspace" "current_dir")
86
+ [ -z "$SL_CWD" ] && SL_CWD=$(json_val "cwd")
87
+ if [ -z "$SL_CWD" ]; then
88
+ SL_DIR="~"
89
+ clean_cwd=""
90
+ else
91
+ clean_cwd=$(to_fwd "$SL_CWD")
92
+ SL_DIR=$(echo "$clean_cwd" | awk -F'/' '{if(NF>3) print $(NF-2)"/"$(NF-1)"/"$NF; else if(NF>2) print $(NF-1)"/"$NF; else print $0}')
93
+ [ -z "$SL_DIR" ] && SL_DIR="~"
94
+ fi
95
+
96
+ # --- 6b. Model ---
97
+ SL_MODEL_DISPLAY=$(json_nested_val "model" "display_name")
98
+ SL_MODEL_ID=$(json_nested_val "model" "id")
99
+ # Flat fallback
100
+ [ -z "$SL_MODEL_DISPLAY" ] && SL_MODEL_DISPLAY=$(json_val "display_name")
101
+ [ -z "$SL_MODEL_ID" ] && SL_MODEL_ID=$(json_val "id")
102
+ [ -z "$SL_MODEL_DISPLAY" ] && SL_MODEL_DISPLAY="unknown"
103
+
104
+ # Parse model from ID for reliable display (handles "Claude claude-opus-4-6" display_name)
105
+ SL_MODEL=""
106
+ if [ -n "$SL_MODEL_ID" ]; then
107
+ _model_family=$(echo "$SL_MODEL_ID" | sed -n 's/^claude-\([a-z]*\)-.*/\1/p')
108
+ _model_ver=$(echo "$SL_MODEL_ID" | sed -n 's/.*-\([0-9]\)-\([0-9]\)$/\1.\2/p')
109
+ [ -z "$_model_ver" ] && _model_ver=$(echo "$SL_MODEL_ID" | sed -n 's/.*-\([0-9]\)-[0-9]\{8\}$/\1/p')
110
+ if [ -n "$_model_family" ]; then
111
+ _family_cap="$(echo "$_model_family" | sed 's/^\(.\)/\U\1/')"
112
+ if [ -n "$_model_ver" ]; then
113
+ SL_MODEL="${_family_cap} ${_model_ver}"
114
+ else
115
+ SL_MODEL="${_family_cap}"
116
+ fi
117
+ fi
118
+ fi
119
+ [ -z "$SL_MODEL" ] && SL_MODEL="$SL_MODEL_DISPLAY"
120
+
121
+ # --- 6c. Context — ACCURATE computation from current_usage ---
122
+ ctx_size=$(json_nested "context_window" "context_window_size")
123
+ cur_input=$(json_deep "context_window" "current_usage" "input_tokens")
124
+ cur_output=$(json_deep "context_window" "current_usage" "output_tokens")
125
+ cur_cache_create=$(json_deep "context_window" "current_usage" "cache_creation_input_tokens")
126
+ cur_cache_read=$(json_deep "context_window" "current_usage" "cache_read_input_tokens")
127
+
128
+ [ -z "$ctx_size" ] && ctx_size=200000
129
+ [ -z "$cur_input" ] && cur_input=0
130
+ [ -z "$cur_output" ] && cur_output=0
131
+ [ -z "$cur_cache_create" ] && cur_cache_create=0
132
+ [ -z "$cur_cache_read" ] && cur_cache_read=0
133
+
134
+ # Claude's formula: input + cache_creation + cache_read (output excluded from context %)
135
+ ctx_used=$(awk -v a="$cur_input" -v b="$cur_cache_create" -v c="$cur_cache_read" \
136
+ 'BEGIN { printf "%d", a + b + c }')
137
+
138
+ # Self-calculated percentage
139
+ calc_pct=0
140
+ if [ "$cur_input" -gt 0 ] 2>/dev/null; then
141
+ calc_pct=$(awk -v used="$ctx_used" -v total="$ctx_size" \
142
+ 'BEGIN { if (total > 0) printf "%d", (used * 100) / total; else print 0 }')
143
+ fi
144
+
145
+ # Reported percentage as fallback
146
+ reported_pct=$(json_nested "context_window" "used_percentage")
147
+
148
+ # Use self-calculated if we have current_usage data, else fallback
149
+ if [ "$cur_input" -gt 0 ] 2>/dev/null; then
150
+ SL_CTX_PCT="$calc_pct"
151
+ elif [ -n "$reported_pct" ] && [ "$reported_pct" != "null" ]; then
152
+ SL_CTX_PCT=$(echo "$reported_pct" | cut -d. -f1)
153
+ else
154
+ SL_CTX_PCT=0
155
+ fi
156
+
157
+ SL_CTX_REMAINING=$(( 100 - SL_CTX_PCT ))
158
+ [ "$SL_CTX_REMAINING" -lt 0 ] && SL_CTX_REMAINING=0
159
+
160
+ # Context color
161
+ if [ "$SL_CTX_PCT" -gt 90 ] 2>/dev/null; then
162
+ CTX_CLR="$CLR_CTX_CRIT"
163
+ elif [ "$SL_CTX_PCT" -gt 75 ] 2>/dev/null; then
164
+ CTX_CLR="$CLR_CTX_HIGH"
165
+ elif [ "$SL_CTX_PCT" -gt 40 ] 2>/dev/null; then
166
+ CTX_CLR="$CLR_CTX_MED"
167
+ else
168
+ CTX_CLR="$CLR_CTX_LOW"
169
+ fi
170
+
171
+ # Build context bar
172
+ filled=$(( SL_CTX_PCT * BAR_WIDTH / 100 ))
173
+ [ "$filled" -gt "$BAR_WIDTH" ] && filled=$BAR_WIDTH
174
+ empty=$(( BAR_WIDTH - filled ))
175
+ bar_filled=""; bar_empty=""
176
+ i=0; while [ $i -lt $filled ]; do bar_filled="${bar_filled}${BAR_FILLED}"; i=$((i+1)); done
177
+ i=0; while [ $i -lt $empty ]; do bar_empty="${bar_empty}${BAR_EMPTY}"; i=$((i+1)); done
178
+ SL_CTX_BAR="${CTX_CLR}${bar_filled}${CLR_RST}${CLR_BAR_EMPTY}${bar_empty}${CLR_RST} ${CTX_CLR}${SL_CTX_PCT}%${CLR_RST}"
179
+
180
+ # Compaction warning
181
+ SL_COMPACT_WARNING=""
182
+ if [ "$SL_CTX_PCT" -ge 95 ] 2>/dev/null; then
183
+ SL_COMPACT_WARNING=" ${CLR_CTX_CRIT}${CLR_BOLD}COMPACTING${CLR_RST}"
184
+ elif [ "$SL_CTX_PCT" -ge "$cfg_warn_threshold" ] 2>/dev/null; then
185
+ SL_COMPACT_WARNING=" ${CLR_CTX_HIGH}${SL_CTX_REMAINING}% left${CLR_RST}"
186
+ fi
187
+
188
+ # --- 6d. GitHub (with caching) ---
189
+ SL_BRANCH="no-git"
190
+ SL_GIT_DIRTY=""
191
+ SL_GITHUB=""
192
+ gh_user=""
193
+ gh_repo=""
194
+
195
+ if [ -n "$clean_cwd" ]; then
196
+ SL_BRANCH=$(cache_get "git-branch" "git --no-optional-locks -C '$clean_cwd' symbolic-ref --short HEAD 2>/dev/null || git --no-optional-locks -C '$clean_cwd' rev-parse --short HEAD 2>/dev/null" 5)
197
+ [ -z "$SL_BRANCH" ] && SL_BRANCH="no-git"
198
+
199
+ if [ "$SL_BRANCH" != "no-git" ]; then
200
+ remote_url=$(cache_get "git-remote" "git --no-optional-locks -C '$clean_cwd' remote get-url origin" 10)
201
+ if [ -n "$remote_url" ]; then
202
+ gh_user=$(echo "$remote_url" | sed 's|.*github\.com[:/]\([^/]*\)/.*|\1|')
203
+ [ "$gh_user" = "$remote_url" ] && gh_user=""
204
+ gh_repo=$(echo "$remote_url" | sed 's|.*/\([^/]*\)\.git$|\1|; s|.*/\([^/]*\)$|\1|')
205
+ [ "$gh_repo" = "$remote_url" ] && gh_repo=""
206
+ fi
207
+
208
+ # Dirty check (shorter cache — changes more often)
209
+ _staged=$(cache_get "git-staged" "git --no-optional-locks -C '$clean_cwd' diff --cached --quiet 2>/dev/null && echo clean || echo dirty" 3)
210
+ _unstaged=$(cache_get "git-unstaged" "git --no-optional-locks -C '$clean_cwd' diff --quiet 2>/dev/null && echo clean || echo dirty" 3)
211
+ [ "$_staged" = "dirty" ] && SL_GIT_DIRTY="${CLR_GIT_STAGED}+${CLR_RST}"
212
+ [ "$_unstaged" = "dirty" ] && SL_GIT_DIRTY="${SL_GIT_DIRTY}${CLR_GIT_UNSTAGED}~${CLR_RST}"
213
+ fi
214
+ fi
215
+
216
+ if [ -n "$gh_repo" ]; then
217
+ SL_GITHUB="${gh_user}/${gh_repo}/${SL_BRANCH}"
218
+ else
219
+ SL_GITHUB="$SL_BRANCH"
220
+ fi
221
+
222
+ # --- 6e. Cost ---
223
+ cost_raw=$(json_nested "cost" "total_cost_usd")
224
+ [ -z "$cost_raw" ] && cost_raw=$(json_num "total_cost_usd")
225
+ if [ -z "$cost_raw" ] || [ "$cost_raw" = "0" ]; then
226
+ SL_COST='$0.00'
227
+ else
228
+ SL_COST=$(awk -v c="$cost_raw" 'BEGIN { if (c < 0.01) printf "$%.4f", c; else printf "$%.2f", c }')
229
+ fi
230
+
231
+ # --- 6f. Tokens (window vs cumulative) ---
232
+ # Current window tokens (what's actually loaded — accurate)
233
+ [ -z "$cur_input" ] && cur_input=0
234
+ [ -z "$cur_output" ] && cur_output=0
235
+ SL_TOKENS_WIN_IN=$(fmt_tok "$cur_input")
236
+ SL_TOKENS_WIN_OUT=$(fmt_tok "$cur_output")
237
+
238
+ # Cumulative session tokens (grows forever, for reference)
239
+ cum_input=$(json_nested "context_window" "total_input_tokens")
240
+ cum_output=$(json_nested "context_window" "total_output_tokens")
241
+ # Flat fallback
242
+ [ -z "$cum_input" ] && cum_input=$(json_num "total_input_tokens")
243
+ [ -z "$cum_output" ] && cum_output=$(json_num "total_output_tokens")
244
+ [ -z "$cum_input" ] && cum_input=0
245
+ [ -z "$cum_output" ] && cum_output=0
246
+ SL_TOKENS_CUM_IN=$(fmt_tok "$cum_input")
247
+ SL_TOKENS_CUM_OUT=$(fmt_tok "$cum_output")
248
+
249
+ # --- 6g. Skill detection (with caching) ---
250
+ SL_SKILL="Idle"
251
+
252
+ _detect_skill() {
253
+ local cwd="$1"
254
+ local tpath="" search_path="$cwd" proj_hash proj_dir
255
+
256
+ while [ -n "$search_path" ] && [ "$search_path" != "/" ]; do
257
+ proj_hash=$(echo "$search_path" | sed 's|^/\([a-zA-Z]\)/|\U\1--|; s|^[A-Z]:/|&|; s|:/|--|; s|/|-|g')
258
+ proj_dir="$HOME/.claude/projects/${proj_hash}"
259
+ if [ -d "$proj_dir" ]; then
260
+ tpath=$(ls -t "$proj_dir"/*.jsonl 2>/dev/null | head -1)
261
+ [ -n "$tpath" ] && break
262
+ fi
263
+ search_path=$(echo "$search_path" | sed 's|/[^/]*$||')
264
+ done
265
+
266
+ if [ -n "$tpath" ] && [ -f "$tpath" ]; then
267
+ local recent_block last_tool
268
+ recent_block=$(tail -200 "$tpath" 2>/dev/null)
269
+ last_tool=$(echo "$recent_block" | grep -o '"type":"tool_use","id":"[^"]*","name":"[^"]*"' | tail -1 | sed 's/.*"name":"\([^"]*\)".*/\1/')
270
+
271
+ if [ -n "$last_tool" ]; then
272
+ case "$last_tool" in
273
+ Task)
274
+ local agent_count
275
+ agent_count=$(echo "$recent_block" | grep -c '"type":"tool_use","id":"[^"]*","name":"Task"')
276
+ if [ "$agent_count" -gt 1 ]; then
277
+ echo "${agent_count} Agents"
278
+ else
279
+ local agent_desc
280
+ agent_desc=$(echo "$recent_block" | grep -o '"description":"[^"]*"' | tail -1 | sed 's/"description":"//;s/"$//')
281
+ if [ -n "$agent_desc" ]; then
282
+ echo "Agent($(echo "$agent_desc" | cut -c1-20))"
283
+ else
284
+ echo "Agent"
285
+ fi
286
+ fi ;;
287
+ Read) echo "Read" ;;
288
+ Write) echo "Write" ;;
289
+ Edit) echo "Edit" ;;
290
+ MultiEdit) echo "Multi Edit" ;;
291
+ Glob) echo "Search(Files)" ;;
292
+ Grep) echo "Search(Content)" ;;
293
+ Bash) echo "Terminal" ;;
294
+ WebSearch) echo "Web Search" ;;
295
+ WebFetch) echo "Web Fetch" ;;
296
+ Skill) echo "Skill" ;;
297
+ AskUserQuestion) echo "Asking..." ;;
298
+ EnterPlanMode) echo "Planning" ;;
299
+ ExitPlanMode) echo "Plan Ready" ;;
300
+ TaskCreate) echo "Task Create" ;;
301
+ TaskUpdate) echo "Task Update" ;;
302
+ TaskGet) echo "Task Get" ;;
303
+ TaskList) echo "Task List" ;;
304
+ TaskStop) echo "Task Stop" ;;
305
+ TaskOutput) echo "Task Output" ;;
306
+ NotebookEdit) echo "Notebook" ;;
307
+ *) echo "$last_tool" ;;
308
+ esac
309
+ return
310
+ fi
311
+ fi
312
+
313
+ # Fallback: check .ccs/task.md
314
+ local task_file="${cwd}/.ccs/task.md"
315
+ if [ -f "$task_file" ]; then
316
+ local last_skill
317
+ last_skill=$(grep -oE '/ccs-[a-z]+' "$task_file" 2>/dev/null | tail -1)
318
+ [ -n "$last_skill" ] && { echo "$last_skill"; return; }
319
+ fi
320
+
321
+ echo "Idle"
322
+ }
323
+
324
+ if [ -n "$clean_cwd" ]; then
325
+ SL_SKILL=$(cache_get "skill-label" "_detect_skill '$clean_cwd'" 2)
326
+ fi
327
+
328
+ # --- 6h. New fields ---
329
+
330
+ # Session duration
331
+ dur_ms=$(json_nested "cost" "total_duration_ms")
332
+ [ -z "$dur_ms" ] && dur_ms=0
333
+ SL_DURATION=$(fmt_duration "$dur_ms")
334
+
335
+ # Lines changed
336
+ SL_LINES_ADDED=$(json_nested "cost" "total_lines_added")
337
+ SL_LINES_REMOVED=$(json_nested "cost" "total_lines_removed")
338
+ [ -z "$SL_LINES_ADDED" ] && SL_LINES_ADDED=0
339
+ [ -z "$SL_LINES_REMOVED" ] && SL_LINES_REMOVED=0
340
+
341
+ # API duration
342
+ api_ms=$(json_nested "cost" "total_api_duration_ms")
343
+ [ -z "$api_ms" ] && api_ms=0
344
+ SL_API_DURATION=$(fmt_duration "$api_ms")
345
+
346
+ # Vim mode (absent when vim is off)
347
+ SL_VIM_MODE=""
348
+ if [ "$cfg_show_vim" = "true" ]; then
349
+ SL_VIM_MODE=$(json_nested_val "vim" "mode")
350
+ fi
351
+
352
+ # Agent name (absent when not in agent mode)
353
+ SL_AGENT_NAME=""
354
+ if [ "$cfg_show_agent" = "true" ]; then
355
+ SL_AGENT_NAME=$(json_nested_val "agent" "name")
356
+ fi
357
+
358
+ # Cache stats (formatted)
359
+ SL_CACHE_CREATE=$(fmt_tok "$cur_cache_create")
360
+ SL_CACHE_READ=$(fmt_tok "$cur_cache_read")
361
+
362
+ # Burn rate (cost per minute)
363
+ SL_BURN_RATE=""
364
+ if [ "$cfg_show_burn_rate" = "true" ] && [ "$dur_ms" -gt 60000 ] 2>/dev/null; then
365
+ SL_BURN_RATE=$(awk -v cost="$cost_raw" -v ms="$dur_ms" \
366
+ 'BEGIN { if (ms > 0 && cost+0 > 0) { rate = cost / (ms / 60000); printf "$%.2f/m", rate } }')
367
+ fi
368
+
369
+ # Exceeds 200k flag
370
+ SL_EXCEEDS_200K=""
371
+ [ -n "$(json_bool "exceeds_200k_tokens")" ] && SL_EXCEEDS_200K="true"
372
+
373
+ # Version
374
+ SL_VERSION=$(json_val "version")
375
+
376
+ # ── 7. Dynamic column widths ──
377
+ SL_C1=$(( SL_TERM_WIDTH / 2 - 4 ))
378
+ [ "$SL_C1" -lt 25 ] && SL_C1=25
379
+ [ "$SL_C1" -gt 42 ] && SL_C1=42
380
+
381
+ # ── 8. Source layout and render ──
382
+ layout_file="${STATUSLINE_DIR}/layouts/${active_layout}.sh"
383
+ if [ -f "$layout_file" ]; then
384
+ source "$layout_file"
385
+ else
386
+ source "${STATUSLINE_DIR}/layouts/standard.sh"
387
+ fi
388
+
389
+ render_layout
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # skill-statusline v2 — Shared helpers
3
+
4
+ # Convert any path to forward slashes (safe on all OS)
5
+ to_fwd() {
6
+ echo "$1" | tr '\\' '/' | sed 's|//\+|/|g'
7
+ }
8
+
9
+ # Right-pad a colored string to a visible width
10
+ rpad() {
11
+ local str="$1" w="$2"
12
+ local plain
13
+ plain=$(printf '%b' "$str" | sed $'s/\033\\[[0-9;]*m//g')
14
+ local vlen=${#plain}
15
+ local need=$(( w - vlen ))
16
+ printf '%b' "$str"
17
+ [ "$need" -gt 0 ] && printf "%${need}s" ""
18
+ }
19
+
20
+ # Format token count with k/M suffixes
21
+ fmt_tok() {
22
+ awk -v t="$1" 'BEGIN {
23
+ if (t >= 1000000) printf "%.1fM", t/1000000
24
+ else if (t >= 1000) printf "%.0fk", t/1000
25
+ else printf "%d", t
26
+ }'
27
+ }
28
+
29
+ # Format duration from milliseconds to human-readable
30
+ fmt_duration() {
31
+ awk -v ms="$1" 'BEGIN {
32
+ s = int(ms / 1000)
33
+ if (s < 60) printf "%ds", s
34
+ else if (s < 3600) printf "%dm%ds", int(s/60), s%60
35
+ else printf "%dh%dm", int(s/3600), int((s%3600)/60)
36
+ }'
37
+ }
38
+
39
+ # ── Filesystem caching with TTL ──
40
+
41
+ CACHE_DIR="/tmp/sl-cache-${USER:-unknown}"
42
+ CACHE_TTL="${SL_CACHE_TTL:-5}"
43
+
44
+ _sl_cache_init() {
45
+ [ -d "$CACHE_DIR" ] || mkdir -p "$CACHE_DIR" 2>/dev/null
46
+ }
47
+
48
+ # cache_get "key" "command" [ttl_seconds]
49
+ # Returns cached result if fresh, otherwise runs command and caches
50
+ cache_get() {
51
+ local key="$1" cmd="$2" ttl="${3:-$CACHE_TTL}"
52
+ local f="${CACHE_DIR}/${key}"
53
+
54
+ if [ -f "$f" ]; then
55
+ local now mtime age
56
+ now=$(date +%s)
57
+ # Cross-platform stat: Linux/Git Bash vs macOS
58
+ if stat -c %Y /dev/null >/dev/null 2>&1; then
59
+ mtime=$(stat -c %Y "$f" 2>/dev/null)
60
+ else
61
+ mtime=$(stat -f %m "$f" 2>/dev/null)
62
+ fi
63
+ if [ -n "$mtime" ]; then
64
+ age=$(( now - mtime ))
65
+ if [ "$age" -lt "$ttl" ]; then
66
+ cat "$f"
67
+ return 0
68
+ fi
69
+ fi
70
+ fi
71
+
72
+ local result
73
+ result=$(eval "$cmd" 2>/dev/null)
74
+ printf '%s' "$result" > "$f" 2>/dev/null
75
+ printf '%s' "$result"
76
+ }
77
+
78
+ # Clear all cached data
79
+ cache_clear() {
80
+ [ -d "$CACHE_DIR" ] && rm -f "${CACHE_DIR}"/* 2>/dev/null
81
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # skill-statusline v2 — JSON parser (no jq, pure grep/sed)
3
+ # Handles both flat and nested Claude Code JSON structures
4
+
5
+ # ── Flat parsers (v1 compat fallback) ──
6
+
7
+ # Extract a quoted string value: json_val "key" → value
8
+ json_val() {
9
+ echo "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:.*"\(.*\)"/\1/'
10
+ }
11
+
12
+ # Extract a numeric value: json_num "key" → number
13
+ json_num() {
14
+ echo "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*[0-9.]*" | head -1 | sed 's/.*:[[:space:]]*//'
15
+ }
16
+
17
+ # ── Nested parsers (v2 — handles Claude Code's real JSON structure) ──
18
+
19
+ # Extract numeric from single-nested object: json_nested "context_window" "used_percentage"
20
+ json_nested() {
21
+ local parent="$1" key="$2"
22
+ local block
23
+ block=$(echo "$input" | sed -n 's/.*"'"$parent"'"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' | head -1)
24
+ if [ -n "$block" ]; then
25
+ echo "$block" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*[0-9.]*" | head -1 | sed 's/.*:[[:space:]]*//'
26
+ fi
27
+ }
28
+
29
+ # Extract string from single-nested object: json_nested_val "model" "display_name"
30
+ json_nested_val() {
31
+ local parent="$1" key="$2"
32
+ local block
33
+ block=$(echo "$input" | sed -n 's/.*"'"$parent"'"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' | head -1)
34
+ if [ -n "$block" ]; then
35
+ echo "$block" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:.*"\(.*\)"/\1/'
36
+ fi
37
+ }
38
+
39
+ # Extract numeric from double-nested: json_deep "context_window" "current_usage" "input_tokens"
40
+ # Handles: {"context_window":{..."current_usage":{"input_tokens":8500}...}}
41
+ json_deep() {
42
+ local p1="$1" p2="$2" key="$3"
43
+ local outer inner
44
+ # Get everything inside the outer object (greedy — captures nested braces)
45
+ outer=$(echo "$input" | sed -n 's/.*"'"$p1"'"[[:space:]]*:[[:space:]]*{\(.*\)}/\1/p' | head -1)
46
+ if [ -n "$outer" ]; then
47
+ # Now extract the inner object
48
+ inner=$(echo "$outer" | sed -n 's/.*"'"$p2"'"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' | head -1)
49
+ if [ -n "$inner" ]; then
50
+ echo "$inner" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*[0-9.]*" | head -1 | sed 's/.*:[[:space:]]*//'
51
+ fi
52
+ fi
53
+ }
54
+
55
+ # Extract string from double-nested: json_deep_val "context_window" "current_usage" "mode"
56
+ json_deep_val() {
57
+ local p1="$1" p2="$2" key="$3"
58
+ local outer inner
59
+ outer=$(echo "$input" | sed -n 's/.*"'"$p1"'"[[:space:]]*:[[:space:]]*{\(.*\)}/\1/p' | head -1)
60
+ if [ -n "$outer" ]; then
61
+ inner=$(echo "$outer" | sed -n 's/.*"'"$p2"'"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' | head -1)
62
+ if [ -n "$inner" ]; then
63
+ echo "$inner" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:.*"\(.*\)"/\1/'
64
+ fi
65
+ fi
66
+ }
67
+
68
+ # Extract boolean: json_bool "exceeds_200k_tokens" → "true" or ""
69
+ json_bool() {
70
+ echo "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*true" | head -1 | sed 's/.*:[[:space:]]*//'
71
+ }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # Theme: Catppuccin Mocha (warm pastels)
3
+ THEME_NAME="Catppuccin"
4
+
5
+ CLR_RST='\033[0m'
6
+ CLR_BOLD='\033[1m'
7
+ CLR_DIM='\033[2m'
8
+
9
+ CLR_SKILL='\033[38;2;245;194;231m' # Pink
10
+ CLR_MODEL='\033[38;2;203;166;247m' # Mauve
11
+ CLR_DIR='\033[38;2;137;220;235m' # Sky
12
+ CLR_GITHUB='\033[38;2;205;214;244m' # Text
13
+ CLR_TOKENS='\033[38;2;249;226;175m' # Yellow
14
+ CLR_COST='\033[38;2;166;227;161m' # Green
15
+ CLR_VIM='\033[38;2;148;226;213m' # Teal
16
+ CLR_AGENT='\033[38;2;137;180;250m' # Blue
17
+
18
+ CLR_CTX_LOW='\033[38;2;205;214;244m' # Text
19
+ CLR_CTX_MED='\033[38;2;250;179;135m' # Peach
20
+ CLR_CTX_HIGH='\033[38;2;243;139;168m' # Red
21
+ CLR_CTX_CRIT='\033[38;2;235;111;146m' # Maroon
22
+
23
+ CLR_SEP='\033[38;2;69;71;90m' # Surface 1
24
+ CLR_BAR_EMPTY='\033[38;2;49;50;68m' # Surface 0
25
+ CLR_LABEL='\033[38;2;108;112;134m' # Overlay 0
26
+
27
+ CLR_GIT_STAGED='\033[38;2;166;227;161m'
28
+ CLR_GIT_UNSTAGED='\033[38;2;249;226;175m'
29
+
30
+ BAR_FILLED='█'
31
+ BAR_EMPTY='░'
32
+ SEP_CHAR='│'
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ # Theme: Default (skill-statusline classic)
3
+ THEME_NAME="Default"
4
+
5
+ CLR_RST='\033[0m'
6
+ CLR_BOLD='\033[1m'
7
+ CLR_DIM='\033[2m'
8
+
9
+ # Semantic field colors
10
+ CLR_SKILL='\033[38;2;236;72;153m' # Pink
11
+ CLR_MODEL='\033[38;2;168;85;247m' # Purple
12
+ CLR_DIR='\033[38;2;6;182;212m' # Cyan
13
+ CLR_GITHUB='\033[38;2;228;228;231m' # White
14
+ CLR_TOKENS='\033[38;2;245;158;11m' # Yellow/Amber
15
+ CLR_COST='\033[38;2;34;197;94m' # Green
16
+ CLR_VIM='\033[38;2;20;184;166m' # Teal
17
+ CLR_AGENT='\033[38;2;99;102;241m' # Blue/Indigo
18
+
19
+ # Context bar thresholds
20
+ CLR_CTX_LOW='\033[38;2;228;228;231m' # White (<=40%)
21
+ CLR_CTX_MED='\033[38;2;251;146;60m' # Orange (41-75%)
22
+ CLR_CTX_HIGH='\033[38;2;239;68;68m' # Red (76-90%)
23
+ CLR_CTX_CRIT='\033[38;2;220;38;38m' # Deep red (>90%)
24
+
25
+ # Chrome
26
+ CLR_SEP='\033[38;2;55;55;62m' # Separator pipe
27
+ CLR_BAR_EMPTY='\033[38;2;40;40;45m' # Empty bar segments
28
+ CLR_LABEL='\033[38;2;120;120;130m' # Dimmed secondary text
29
+
30
+ # Git dirty indicators
31
+ CLR_GIT_STAGED='\033[38;2;34;197;94m' # Green +
32
+ CLR_GIT_UNSTAGED='\033[38;2;245;158;11m' # Yellow ~
33
+
34
+ # Bar characters
35
+ BAR_FILLED='█'
36
+ BAR_EMPTY='░'
37
+ SEP_CHAR='│'
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # Theme: Gruvbox Dark (retro groovy)
3
+ THEME_NAME="Gruvbox"
4
+
5
+ CLR_RST='\033[0m'
6
+ CLR_BOLD='\033[1m'
7
+ CLR_DIM='\033[2m'
8
+
9
+ CLR_SKILL='\033[38;2;211;134;155m' # Red
10
+ CLR_MODEL='\033[38;2;177;98;134m' # Purple
11
+ CLR_DIR='\033[38;2;131;165;152m' # Aqua
12
+ CLR_GITHUB='\033[38;2;235;219;178m' # FG
13
+ CLR_TOKENS='\033[38;2;250;189;47m' # Yellow
14
+ CLR_COST='\033[38;2;184;187;38m' # Green
15
+ CLR_VIM='\033[38;2;131;165;152m' # Aqua
16
+ CLR_AGENT='\033[38;2;69;133;136m' # Blue
17
+
18
+ CLR_CTX_LOW='\033[38;2;235;219;178m' # FG
19
+ CLR_CTX_MED='\033[38;2;254;128;25m' # Orange
20
+ CLR_CTX_HIGH='\033[38;2;251;73;52m' # Red
21
+ CLR_CTX_CRIT='\033[38;2;204;36;29m' # Dark red
22
+
23
+ CLR_SEP='\033[38;2;80;73;69m' # BG2
24
+ CLR_BAR_EMPTY='\033[38;2;60;56;54m' # BG1
25
+ CLR_LABEL='\033[38;2;124;111;100m' # Gray
26
+
27
+ CLR_GIT_STAGED='\033[38;2;184;187;38m'
28
+ CLR_GIT_UNSTAGED='\033[38;2;250;189;47m'
29
+
30
+ BAR_FILLED='█'
31
+ BAR_EMPTY='░'
32
+ SEP_CHAR='│'
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # Theme: Nord (arctic, blue-tinted)
3
+ THEME_NAME="Nord"
4
+
5
+ CLR_RST='\033[0m'
6
+ CLR_BOLD='\033[1m'
7
+ CLR_DIM='\033[2m'
8
+
9
+ CLR_SKILL='\033[38;2;136;192;208m' # Nord Frost 3
10
+ CLR_MODEL='\033[38;2;180;142;173m' # Nord Aurora purple
11
+ CLR_DIR='\033[38;2;163;190;140m' # Nord Aurora green
12
+ CLR_GITHUB='\033[38;2;216;222;233m' # Nord Snow Storm 1
13
+ CLR_TOKENS='\033[38;2;235;203;139m' # Nord Aurora yellow
14
+ CLR_COST='\033[38;2;163;190;140m' # Nord Aurora green
15
+ CLR_VIM='\033[38;2;143;188;187m' # Nord Frost 1
16
+ CLR_AGENT='\033[38;2;129;161;193m' # Nord Frost 2
17
+
18
+ CLR_CTX_LOW='\033[38;2;216;222;233m' # Snow Storm
19
+ CLR_CTX_MED='\033[38;2;208;135;112m' # Nord Aurora orange
20
+ CLR_CTX_HIGH='\033[38;2;191;97;106m' # Nord Aurora red
21
+ CLR_CTX_CRIT='\033[38;2;191;97;106m'
22
+
23
+ CLR_SEP='\033[38;2;67;76;94m' # Nord Polar Night 3
24
+ CLR_BAR_EMPTY='\033[38;2;46;52;64m' # Nord Polar Night 1
25
+ CLR_LABEL='\033[38;2;76;86;106m' # Nord Polar Night 4
26
+
27
+ CLR_GIT_STAGED='\033[38;2;163;190;140m'
28
+ CLR_GIT_UNSTAGED='\033[38;2;235;203;139m'
29
+
30
+ BAR_FILLED='█'
31
+ BAR_EMPTY='░'
32
+ SEP_CHAR='│'