@buckits/claude-statusline 2.3.3 β†’ 3.0.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/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  <br>
11
11
 
12
- ![Statusline Preview](assets/statusline-preview.svg)
12
+ ![Statusline Preview](assets/50-bars.png)
13
13
 
14
14
  <br>
15
15
 
@@ -34,7 +34,7 @@ Claude Code's default statusline is... minimal. You deserve better.
34
34
 
35
35
  ### 🎨 Gradient Progress Bar
36
36
 
37
- 50 segments that smoothly transition through colors as your context fills up. The gradient is calculated relative to the auto-compact threshold, not total capacityβ€”so you always know how close you are to summarization.
37
+ Smoothly transitions through colors as your context fills up. Choose your width during installβ€”Compact (25), Medium (38), Full (50), or Custom (any number 10-100). The gradient is calculated relative to the auto-compact threshold, not total capacityβ€”so you always know how close you are to summarization.
38
38
 
39
39
  ### ⚑ Auto-Compact Threshold
40
40
 
@@ -68,8 +68,9 @@ npx @buckits/claude-statusline
68
68
 
69
69
  That's it. The installer will:
70
70
  1. Ask where to install (global or local)
71
- 2. Copy the statusline script
72
- 3. Configure your settings
71
+ 2. Let you choose your progress bar width
72
+ 3. Copy the statusline script
73
+ 4. Configure your settings
73
74
 
74
75
  ### Options
75
76
 
@@ -92,23 +93,17 @@ npx @buckits/claude-statusline --global --uninstall
92
93
 
93
94
  ## What It Looks Like
94
95
 
95
- ### 🟒 Normal Usage (Green Zone)
96
+ ### Compact (25 bars)
96
97
 
97
- ![Green Zone](assets/statusline-green.svg)
98
+ ![Compact](assets/25-bars.png)
98
99
 
99
- Safe and sound. Plenty of context remaining.
100
+ ### Medium (38 bars)
100
101
 
101
- ### 🟑 Getting Busy (Yellow Zone)
102
+ ![Medium](assets/38-bars.png)
102
103
 
103
- ![Yellow Zone](assets/statusline-yellow.svg)
104
+ ### Full (50 bars)
104
105
 
105
- Making progress. Keep an eye on that threshold.
106
-
107
- ### πŸ”΄ Approaching Limit (Red Zone)
108
-
109
- ![Red Zone](assets/statusline-red.svg)
110
-
111
- Getting close to auto-compact. Consider wrapping up or starting fresh.
106
+ ![Full](assets/50-bars.png)
112
107
 
113
108
  ## 🀝 GSD Compatible
114
109
 
@@ -125,36 +120,22 @@ npx get-shit-done-cc
125
120
  ## Requirements
126
121
 
127
122
  - **Claude Code CLI** (obviously)
128
- - **jq** - for JSON parsing ([install guide](https://stedolan.github.io/jq/download/))
129
- - **Bash** - ships with macOS/Linux
123
+ - **Node.js** >= 14
130
124
  - **Git** - for git status features (optional)
131
125
 
132
- ### Installing jq
133
-
134
- ```bash
135
- # macOS
136
- brew install jq
137
-
138
- # Ubuntu/Debian
139
- sudo apt install jq
140
-
141
- # Windows (via chocolatey)
142
- choco install jq
143
- ```
144
-
145
126
  ## Manual Installation
146
127
 
147
128
  If you prefer to install manually:
148
129
 
149
- 1. Copy `statusline.sh` to `~/.claude/statusline.sh`
150
- 2. Make it executable: `chmod +x ~/.claude/statusline.sh`
130
+ 1. Copy `statusline.js` to `~/.claude/statusline.js`
131
+ 2. Make it executable: `chmod +x ~/.claude/statusline.js`
151
132
  3. Add to `~/.claude/settings.json`:
152
133
 
153
134
  ```json
154
135
  {
155
136
  "statusLine": {
156
137
  "type": "command",
157
- "command": "/Users/YOUR_USERNAME/.claude/statusline.sh"
138
+ "command": "/Users/YOUR_USERNAME/.claude/statusline.js --width 50"
158
139
  }
159
140
  }
160
141
  ```
@@ -164,12 +145,11 @@ If you prefer to install manually:
164
145
  ### Statusline not showing?
165
146
 
166
147
  1. Make sure you restarted Claude Code after installation
167
- 2. Check that `jq` is installed: `which jq`
168
- 3. Verify the script is executable: `ls -la ~/.claude/statusline.sh`
148
+ 2. Verify the script is executable: `ls -la ~/.claude/statusline.js`
169
149
 
170
150
  ### Wrong colors?
171
151
 
172
- Your terminal needs to support 256 colors. Most modern terminals do.
152
+ Your terminal needs to support true color (24-bit). Most modern terminals do.
173
153
 
174
154
  ### Git status not showing?
175
155
 
package/bin/install.js CHANGED
@@ -134,10 +134,10 @@ function uninstall(isGlobal) {
134
134
  let removed = false;
135
135
 
136
136
  // Remove statusline.sh
137
- const statuslinePath = path.join(targetDir, 'statusline.sh');
137
+ const statuslinePath = path.join(targetDir, 'statusline.js');
138
138
  if (fs.existsSync(statuslinePath)) {
139
139
  fs.unlinkSync(statuslinePath);
140
- console.log(` ${green}βœ“${reset} Removed statusline.sh`);
140
+ console.log(` ${green}βœ“${reset} Removed statusline.js`);
141
141
  removed = true;
142
142
  }
143
143
 
@@ -147,7 +147,7 @@ function uninstall(isGlobal) {
147
147
  const settings = readSettings(settingsPath);
148
148
 
149
149
  if (settings.statusLine && settings.statusLine.command &&
150
- settings.statusLine.command.includes('statusline.sh')) {
150
+ settings.statusLine.command.includes('statusline.js')) {
151
151
  delete settings.statusLine;
152
152
  writeSettings(settingsPath, settings);
153
153
  console.log(` ${green}βœ“${reset} Removed statusline from settings.json`);
@@ -175,7 +175,8 @@ function uninstall(isGlobal) {
175
175
  /**
176
176
  * Install statusline
177
177
  */
178
- function install(isGlobal) {
178
+ function install(isGlobal, barWidth) {
179
+ barWidth = barWidth || 50;
179
180
  const targetDir = isGlobal ? getGlobalDir() : getLocalDir();
180
181
  const locationLabel = isGlobal
181
182
  ? targetDir.replace(os.homedir(), '~')
@@ -190,11 +191,11 @@ function install(isGlobal) {
190
191
  }
191
192
 
192
193
  // Copy statusline.sh
193
- const statuslineSrc = path.join(__dirname, '..', 'statusline.sh');
194
- const statuslineDest = path.join(targetDir, 'statusline.sh');
194
+ const statuslineSrc = path.join(__dirname, '..', 'statusline.js');
195
+ const statuslineDest = path.join(targetDir, 'statusline.js');
195
196
  fs.copyFileSync(statuslineSrc, statuslineDest);
196
197
  fs.chmodSync(statuslineDest, '755');
197
- console.log(` ${green}βœ“${reset} Installed statusline.sh`);
198
+ console.log(` ${green}βœ“${reset} Installed statusline.js`);
198
199
 
199
200
  // Update settings.json
200
201
  const settingsPath = path.join(targetDir, 'settings.json');
@@ -221,7 +222,7 @@ function install(isGlobal) {
221
222
  // Set new format
222
223
  settings.statusLine = {
223
224
  type: 'command',
224
- command: statuslineDest
225
+ command: statuslineDest + ' --width ' + barWidth
225
226
  };
226
227
 
227
228
  writeSettings(settingsPath, settings);
@@ -249,7 +250,7 @@ function install(isGlobal) {
249
250
  function promptLocation() {
250
251
  if (!process.stdin.isTTY) {
251
252
  console.log(` ${dim}Non-interactive mode, defaulting to global install${reset}\n`);
252
- install(true);
253
+ install(true, 50);
253
254
  return;
254
255
  }
255
256
 
@@ -279,12 +280,47 @@ function promptLocation() {
279
280
  This project only
280
281
  `);
281
282
 
282
- rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
283
- answered = true;
284
- rl.close();
283
+ rl.question(` Choice ${dim}[1]${reset}: `, (locAnswer) => {
284
+ const isGlobal = (locAnswer.trim() || '1') !== '2';
285
285
  console.log('');
286
- const choice = answer.trim() || '1';
287
- install(choice !== '2');
286
+ promptWidth(rl, (barWidth) => {
287
+ answered = true;
288
+ rl.close();
289
+ console.log('');
290
+ install(isGlobal, barWidth);
291
+ });
292
+ });
293
+ }
294
+
295
+ /**
296
+ * Interactive prompt for bar width
297
+ */
298
+ function promptWidth(rl, callback) {
299
+ console.log(` ${yellow}Progress bar width?${reset}
300
+
301
+ ${cyan}1${reset}) ${bold}Compact${reset} ${dim}[${green}${'β–ˆβ–ˆ'.repeat(3)}${reset}${dim}${'β–‘'.repeat(19)}${red}ϟ${reset}${dim}${'β–‘'.repeat(3)}]${reset} ${dim}25 bars${reset}
302
+ ${cyan}2${reset}) ${bold}Medium${reset} ${dim}[${green}${'β–ˆβ–ˆ'.repeat(5)}${reset}${dim}${'β–‘'.repeat(24)}${red}ϟ${reset}${dim}${'β–‘'.repeat(5)}]${reset} ${dim}38 bars${reset}
303
+ ${cyan}3${reset}) ${bold}Full${reset} ${dim}[${green}${'β–ˆβ–ˆ'.repeat(7)}${reset}${dim}${'β–‘'.repeat(28)}${red}ϟ${reset}${dim}${'β–‘'.repeat(8)}]${reset} ${dim}50 bars${reset}
304
+ ${cyan}4${reset}) ${bold}Custom${reset} ${dim}Enter your own number${reset}
305
+ `);
306
+
307
+ rl.question(` Choice ${dim}[3]${reset}: `, (answer) => {
308
+ const choice = answer.trim() || '3';
309
+ const widths = { '1': 25, '2': 38, '3': 50 };
310
+
311
+ if (choice === '4') {
312
+ rl.question(` Number of bars ${dim}(10-100)${reset}: `, (numAnswer) => {
313
+ const num = parseInt(numAnswer.trim(), 10);
314
+ if (num >= 10 && num <= 100) {
315
+ callback(num);
316
+ } else {
317
+ console.log(` ${yellow}⚠${reset} Invalid number, using default (50)\n`);
318
+ callback(50);
319
+ }
320
+ });
321
+ } else {
322
+ callback(widths[choice] || 50);
323
+ }
288
324
  });
289
325
  }
290
326
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@buckits/claude-statusline",
3
- "version": "2.3.3",
3
+ "version": "3.0.0",
4
4
  "description": "The statusline Claude Code deserves - gradient progress bar, auto-compact threshold marker, git status, cost tracking, 2-line dashboard",
5
5
  "bin": {
6
6
  "claude-statusline": "bin/install.js"
7
7
  },
8
8
  "files": [
9
9
  "bin/",
10
- "statusline.sh"
10
+ "statusline.js"
11
11
  ],
12
12
  "keywords": [
13
13
  "claude",
package/statusline.js ADDED
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const { execFileSync } = require('child_process');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ // Parse --width arg (default 50)
11
+ const widthArgIdx = process.argv.indexOf('--width');
12
+ const BAR_WIDTH = widthArgIdx !== -1 && process.argv[widthArgIdx + 1]
13
+ ? parseInt(process.argv[widthArgIdx + 1], 10) || 50
14
+ : 50;
15
+
16
+ // Read JSON input from stdin
17
+ let inputData = '';
18
+ process.stdin.setEncoding('utf8');
19
+ process.stdin.on('data', (chunk) => { inputData += chunk; });
20
+ process.stdin.on('end', () => {
21
+ try {
22
+ main(JSON.parse(inputData));
23
+ } catch (e) {
24
+ process.exit(1);
25
+ }
26
+ });
27
+
28
+ function main(input) {
29
+ // Extract values from JSON
30
+ const cwd = (input.workspace && input.workspace.current_dir) || input.cwd || '';
31
+ const model = (input.model && input.model.display_name) || '';
32
+
33
+ // Extract token information from context_window
34
+ const cw = input.context_window || {};
35
+ const cu = cw.current_usage || {};
36
+ const totalInput = Number(cw.total_input_tokens) || 0;
37
+ const totalOutput = Number(cw.total_output_tokens) || 0;
38
+ const cacheRead = Number(cu.cache_read_input_tokens) || 0;
39
+
40
+ // Total used = input + output + cached tokens being used
41
+ let usedTokens = totalInput + totalOutput + cacheRead;
42
+ const maxTokens = Number(cw.context_window_size) || 200000;
43
+
44
+ // Use the pre-calculated percentage if available (more accurate)
45
+ let percent;
46
+ const percentPrecalc = cw.used_percentage;
47
+ if (percentPrecalc != null && percentPrecalc !== '') {
48
+ percent = Math.trunc(Number(percentPrecalc));
49
+ // Recalculate used_tokens from percentage for display accuracy
50
+ usedTokens = Math.trunc(maxTokens * percent / 100);
51
+ } else {
52
+ // Fallback: calculate percentage from tokens
53
+ percent = maxTokens > 0 ? Math.trunc(usedTokens * 100 / maxTokens) : 0;
54
+ }
55
+
56
+ // Clamp percent to 0-100
57
+ if (percent < 0) percent = 0;
58
+ if (percent > 100) percent = 100;
59
+
60
+ // Progress bar settings
61
+ const barWidth = BAR_WIDTH;
62
+ const filled = Math.trunc(percent * barWidth / 100);
63
+
64
+ // ── Git information ──────────────────────────────────
65
+ let gitBranch = '';
66
+ let gitRemote = '';
67
+ let gitAhead = '';
68
+ let gitBehind = '';
69
+ let gitStatusIndicator = '';
70
+
71
+ if (cwd) {
72
+ try {
73
+ execFileSync('git', ['rev-parse', '--git-dir'], { cwd, stdio: 'pipe' });
74
+
75
+ // Branch name
76
+ try {
77
+ gitBranch = execFileSync('git', ['branch', '--show-current'],
78
+ { cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
79
+ } catch (e) {}
80
+
81
+ // Upstream tracking branch
82
+ let upstream = '';
83
+ try {
84
+ upstream = execFileSync('git',
85
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
86
+ { cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
87
+ gitRemote = upstream;
88
+ } catch (e) {}
89
+
90
+ // Ahead/behind counts
91
+ if (upstream) {
92
+ try {
93
+ const ab = execFileSync('git',
94
+ ['rev-list', '--left-right', '--count', 'HEAD...' + upstream],
95
+ { cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
96
+ const parts = ab.split(/\s+/);
97
+ const ahead = parseInt(parts[0], 10);
98
+ const behind = parseInt(parts[1], 10);
99
+ if (ahead > 0) gitAhead = String(ahead);
100
+ if (behind > 0) gitBehind = String(behind);
101
+ } catch (e) {}
102
+ }
103
+
104
+ // Git status (dirty/staged indicators)
105
+ try {
106
+ const porcelain = execFileSync('git', ['status', '--porcelain'],
107
+ { cwd, encoding: 'utf8', stdio: 'pipe' });
108
+
109
+ let hasUnstaged = false;
110
+ let hasStaged = false;
111
+
112
+ if (porcelain) {
113
+ const lines = porcelain.split('\n');
114
+ for (const line of lines) {
115
+ if (!line) continue;
116
+ // Check for staged changes (first column not space)
117
+ if (/^[MADRC]/.test(line)) hasStaged = true;
118
+ // Check for unstaged changes (second column not space, or untracked files)
119
+ if (/^\?\?/.test(line) || /^.[MD]/.test(line)) hasUnstaged = true;
120
+ }
121
+ }
122
+
123
+ // Build status indicator
124
+ if (hasUnstaged && hasStaged) {
125
+ gitStatusIndicator = '\x1b[1;33m●\x1b[1;32m✚\x1b[0m'; // Both
126
+ } else if (hasUnstaged) {
127
+ gitStatusIndicator = '\x1b[1;33m●\x1b[0m'; // Unstaged (yellow)
128
+ } else if (hasStaged) {
129
+ gitStatusIndicator = '\x1b[1;32m✚\x1b[0m'; // Staged (green)
130
+ } else {
131
+ gitStatusIndicator = '\x1b[1;32mβœ“\x1b[0m'; // Clean (green)
132
+ }
133
+ } catch (e) {}
134
+ } catch (e) {
135
+ // Not a git repo β€” skip git info
136
+ }
137
+ }
138
+
139
+ // Project name from directory
140
+ const projectName = cwd ? path.basename(cwd) : '';
141
+
142
+ // ── Token formatting ─────────────────────────────────
143
+
144
+ // Format tokens for display (e.g., 45.2k/200k)
145
+ function formatTokens(tokens) {
146
+ if (tokens >= 1000000) {
147
+ const millions = Math.trunc(tokens / 1000000);
148
+ const decimal = Math.trunc((tokens % 1000000) / 100000);
149
+ return millions + '.' + decimal + 'M';
150
+ } else if (tokens >= 1000) {
151
+ const thousands = Math.trunc(tokens / 1000);
152
+ const decimal = Math.trunc((tokens % 1000) / 100);
153
+ return thousands + '.' + decimal + 'k';
154
+ }
155
+ return String(tokens);
156
+ }
157
+
158
+ // Format tokens without decimals (e.g., 72k/200k)
159
+ function formatTokensInt(tokens) {
160
+ if (tokens >= 1000000) {
161
+ return Math.trunc(tokens / 1000000) + 'M';
162
+ } else if (tokens >= 1000) {
163
+ return Math.trunc(tokens / 1000) + 'k';
164
+ }
165
+ return String(tokens);
166
+ }
167
+
168
+ const usedFmtInt = formatTokensInt(usedTokens);
169
+ const maxFmtInt = formatTokensInt(maxTokens);
170
+
171
+ // ── Cost ──────────────────────────────────────────────
172
+ let costDisplay = '';
173
+ const costUsd = input.cost && input.cost.total_cost_usd;
174
+ if (costUsd != null && costUsd !== '' && costUsd !== null) {
175
+ costDisplay = '$' + Number(costUsd).toFixed(2);
176
+ }
177
+
178
+ // ── Tools / Skills / Agents counts ────────────────────
179
+ const ctx = input.context || {};
180
+ const skillsCount = Array.isArray(ctx.skills) ? ctx.skills.length : 0;
181
+ const agentsCount = Array.isArray(ctx.agents) ? ctx.agents.length : 0;
182
+ const mcpToolsCount = Array.isArray(ctx.mcp_tools) ? ctx.mcp_tools.length : 0;
183
+ const totalTools = skillsCount + agentsCount + mcpToolsCount;
184
+ let toolsDisplay = '';
185
+ if (totalTools > 0) {
186
+ toolsDisplay = '\uD83D\uDD27 ' + totalTools; // πŸ”§
187
+ }
188
+
189
+ // ── Background tasks ──────────────────────────────────
190
+ const bgTasksArr = input.background_tasks || input.running_agents || input.active_tasks;
191
+ let bgTasksCount = 0;
192
+ if (bgTasksArr != null) {
193
+ bgTasksCount = Array.isArray(bgTasksArr) ? bgTasksArr.length : 0;
194
+ }
195
+ let bgDisplay = '';
196
+ if (bgTasksCount > 0) {
197
+ bgDisplay = '\u231B ' + bgTasksCount; // ⏳
198
+ }
199
+
200
+ // ── Auto-compact threshold ────────────────────────────
201
+ const AUTO_COMPACT_THRESHOLD = 22;
202
+ let untilCompact = cw.until_compact != null ? Number(cw.until_compact)
203
+ : cw.until_auto_compact != null ? Number(cw.until_auto_compact)
204
+ : null;
205
+
206
+ if (untilCompact == null) {
207
+ const remainingPct = Number(cw.remaining_percentage) || 0;
208
+ if (remainingPct > AUTO_COMPACT_THRESHOLD) {
209
+ untilCompact = remainingPct - AUTO_COMPACT_THRESHOLD;
210
+ } else {
211
+ untilCompact = 0;
212
+ }
213
+ }
214
+
215
+ let compactColor = '38;5;46';
216
+ let compactIndicatorText = '';
217
+ let compactIndicatorPct = '';
218
+
219
+ if (untilCompact && untilCompact !== 0) {
220
+ if (untilCompact >= 25) compactColor = '38;5;46';
221
+ else if (untilCompact >= 20) compactColor = '38;5;154';
222
+ else if (untilCompact >= 15) compactColor = '38;5;226';
223
+ else if (untilCompact >= 10) compactColor = '38;5;220';
224
+ else if (untilCompact >= 7) compactColor = '38;5;214';
225
+ else if (untilCompact >= 4) compactColor = '38;5;208';
226
+ else compactColor = '38;5;196';
227
+
228
+ // Calculate tokens until compact threshold
229
+ const tokensUntilCompact = Math.trunc(untilCompact * maxTokens / 100);
230
+ const tokensUntilCompactFmt = formatTokens(tokensUntilCompact);
231
+
232
+ // Build indicator with tokens primary (gradient color) and percentage in parens (dim)
233
+ compactIndicatorText = '\u26A1' + tokensUntilCompactFmt + ' until compact'; // ⚑
234
+ compactIndicatorPct = '(' + untilCompact + '%)';
235
+ }
236
+
237
+ // ══════════════════════════════════════════════════════
238
+ // 2-LINE DASHBOARD LAYOUT
239
+ // ══════════════════════════════════════════════════════
240
+
241
+ // LINE 1: Model + Context
242
+ // Format: Opus 4.5 ($12.01) [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘βš‘β–‘β–‘β–‘β–‘β–‘] 74k/200k
243
+
244
+ const compactThresholdPct = 100 - AUTO_COMPACT_THRESHOLD; // 78%
245
+ const thresholdPosition = Math.trunc(compactThresholdPct * barWidth / 100);
246
+
247
+ // Build unified progress bar with per-bar gradient toward threshold
248
+ let unifiedBar = '[';
249
+ let unifiedRightmostColor = '38;2;0;255;0'; // Default green
250
+
251
+ // Smooth RGB gradient: green(0,255,0) β†’ yellow(255,255,0) β†’ red(255,0,0)
252
+ // t = 0.0 β†’ green, t = 0.5 β†’ yellow, t = 1.0 β†’ red
253
+ function gradientRGB(i) {
254
+ const t = thresholdPosition > 0 ? i / thresholdPosition : 0;
255
+ let r, g;
256
+ if (t <= 0.5) {
257
+ // Green β†’ Yellow: R ramps up, G stays max
258
+ r = Math.round(t * 2 * 255);
259
+ g = 255;
260
+ } else {
261
+ // Yellow β†’ Red: R stays max, G ramps down
262
+ r = 255;
263
+ g = Math.round((1 - t) * 2 * 255);
264
+ }
265
+ return { r, g, b: 0 };
266
+ }
267
+
268
+ for (let i = 0; i < barWidth; i++) {
269
+ if (i === thresholdPosition) {
270
+ // Threshold marker: bolt replaces this bar segment
271
+ if (i < filled) {
272
+ // Filled: gradient background with contrasting bold white bolt
273
+ const { r, g, b } = gradientRGB(i);
274
+ unifiedRightmostColor = '38;2;' + r + ';' + g + ';' + b;
275
+ unifiedBar += '\x1b[48;2;' + r + ';' + g + ';' + b + ';1;37m\u03DF\x1b[0m';
276
+ } else {
277
+ // Not filled: bright red bolt on theme-adaptive dark background
278
+ unifiedBar += '\x1b[100;38;2;255;0;0m\u03DF\x1b[0m';
279
+ }
280
+ } else if (i < filled) {
281
+ if (i > thresholdPosition) {
282
+ // Past threshold: always red
283
+ unifiedRightmostColor = '38;2;255;0;0';
284
+ unifiedBar += '\x1b[38;2;255;0;0m\u2588\x1b[0m';
285
+ } else {
286
+ const { r, g, b } = gradientRGB(i);
287
+ const color = '38;2;' + r + ';' + g + ';' + b;
288
+ unifiedRightmostColor = color;
289
+ unifiedBar += '\x1b[' + color + 'm\u2588\x1b[0m';
290
+ }
291
+ } else {
292
+ unifiedBar += '\u2591'; // β–‘
293
+ }
294
+ }
295
+
296
+ unifiedBar += ']';
297
+
298
+ // Assemble Line 1
299
+ let line1 = '';
300
+
301
+ // Robot icon and model name in cyan
302
+ if (model) {
303
+ line1 += '\uD83E\uDD16 \x1b[1;36m' + model + '\x1b[0m'; // πŸ€–
304
+ }
305
+
306
+ // Add cost after model name (bright green)
307
+ if (costDisplay) {
308
+ line1 += ' \x1b[1;32m(' + costDisplay + ')\x1b[0m';
309
+ }
310
+
311
+ // Add subtle separator between cost and progress bar
312
+ line1 += ' \x1b[2;37m\u2502\x1b[0m'; // β”‚
313
+
314
+ // Add progress bar and tokens to line 1
315
+ line1 += ' ' + unifiedBar
316
+ + ' \x1b[' + unifiedRightmostColor + 'm' + usedFmtInt + '\x1b[0m'
317
+ + '\x1b[1;36m/' + maxFmtInt + '\x1b[0m';
318
+
319
+ // LINE 2: Project/Git Info
320
+ // Format: πŸ“ myproject main βœ“ β†’ origin/main ↑11
321
+ let line2 = '';
322
+
323
+ // Folder icon and project name in cyan
324
+ if (projectName) {
325
+ line2 += '\uD83D\uDCC1 \x1b[1;36m' + projectName + '\x1b[0m'; // πŸ“
326
+ }
327
+
328
+ // Git branch and tracking
329
+ if (gitBranch) {
330
+ if (line2) line2 += ' ';
331
+
332
+ // Branch in magenta
333
+ line2 += '\x1b[1;35m' + gitBranch + '\x1b[0m';
334
+
335
+ // Git status indicator (after branch name)
336
+ if (gitStatusIndicator) {
337
+ line2 += ' ' + gitStatusIndicator;
338
+ }
339
+
340
+ // Arrow and tracking
341
+ line2 += ' \x1b[2;37m\u2192\x1b[0m'; // β†’
342
+
343
+ if (gitRemote) {
344
+ // Tracking branch in blue
345
+ line2 += ' \x1b[1;34m' + gitRemote + '\x1b[0m';
346
+
347
+ // Ahead/behind indicators
348
+ if (gitAhead || gitBehind) {
349
+ line2 += ' ';
350
+ if (gitAhead) line2 += '\x1b[0;32m\u2191' + gitAhead + '\x1b[0m'; // ↑
351
+ if (gitBehind) {
352
+ if (gitAhead) line2 += ' ';
353
+ line2 += '\x1b[0;31m\u2193' + gitBehind + '\x1b[0m'; // ↓
354
+ }
355
+ }
356
+ } else {
357
+ line2 += ' \x1b[2;37m(no upstream)\x1b[0m';
358
+ }
359
+ }
360
+
361
+ // ── GSD Update check ─────────────────────────────────
362
+ let gsdUpdateSuffix = '';
363
+ const gsdCacheFile = path.join(os.homedir(), '.claude', 'cache', 'gsd-update-check.json');
364
+
365
+ // Check for GSD in project first, then global
366
+ let gsdVersionFile = '';
367
+ if (cwd) {
368
+ const localGsd = path.join(cwd, '.claude', 'get-shit-done', 'VERSION');
369
+ if (fs.existsSync(localGsd)) {
370
+ gsdVersionFile = localGsd;
371
+ }
372
+ }
373
+ if (!gsdVersionFile) {
374
+ const globalGsd = path.join(os.homedir(), '.claude', 'get-shit-done', 'VERSION');
375
+ if (fs.existsSync(globalGsd)) {
376
+ gsdVersionFile = globalGsd;
377
+ }
378
+ }
379
+
380
+ if (gsdVersionFile) {
381
+ let installedVer = '';
382
+ try {
383
+ installedVer = fs.readFileSync(gsdVersionFile, 'utf8').trim();
384
+ } catch (e) {}
385
+ if (!installedVer) installedVer = '0.0.0';
386
+
387
+ // Get latest version from cache (populated by SessionStart hook)
388
+ if (fs.existsSync(gsdCacheFile)) {
389
+ try {
390
+ const cache = JSON.parse(fs.readFileSync(gsdCacheFile, 'utf8'));
391
+ const latestVer = cache.latest || 'unknown';
392
+
393
+ // Compare versions - append to line2 if they differ
394
+ if (latestVer !== 'unknown' && installedVer !== latestVer) {
395
+ gsdUpdateSuffix = ' | GSD ' + installedVer + '>' + latestVer;
396
+ }
397
+ } catch (e) {}
398
+ }
399
+ }
400
+
401
+ // Append GSD update to line2 if available
402
+ line2 += gsdUpdateSuffix;
403
+
404
+ // Output the dashboard (2 lines, no trailing newline)
405
+ process.stdout.write(line1 + '\x1b[K\n' + line2 + '\x1b[K');
406
+ }
package/statusline.sh DELETED
@@ -1,437 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Claude Code Status Line Script
4
- # Read JSON input from stdin
5
-
6
- input=$(cat)
7
-
8
- # Extract values from JSON
9
- cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // empty')
10
- model=$(echo "$input" | jq -r '.model.display_name // empty')
11
-
12
- # Extract token information from context_window
13
- # The actual context usage includes cache tokens from prompt caching
14
- total_input=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
15
- total_output=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0')
16
- cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
17
-
18
- # Total used = input + output + cached tokens being used
19
- used_tokens=$((total_input + total_output + cache_read))
20
- max_tokens=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
21
-
22
- # Use the pre-calculated percentage if available (more accurate)
23
- percent_precalc=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
24
- if [ -n "$percent_precalc" ]; then
25
- percent=$percent_precalc
26
- # Recalculate used_tokens from percentage for display accuracy
27
- used_tokens=$((max_tokens * percent / 100))
28
- else
29
- # Fallback: calculate percentage from tokens
30
- if [ "$max_tokens" -gt 0 ] 2>/dev/null; then
31
- percent=$((used_tokens * 100 / max_tokens))
32
- else
33
- percent=0
34
- fi
35
- fi
36
-
37
- # Clamp percent to 0-100
38
- [ "$percent" -lt 0 ] && percent=0
39
- [ "$percent" -gt 100 ] && percent=100
40
-
41
- # Progress bar settings - 50 squares (4k tokens each for 200k context)
42
- bar_width=50
43
- filled=$((percent * bar_width / 100))
44
- empty=$((bar_width - filled))
45
-
46
- # Get color for a specific bar position (0 to bar_width-1)
47
- # Each bar position gets its own color based on progression
48
- get_bar_color_for_position() {
49
- local pos=$1
50
- local total=$2
51
- # Calculate percentage for this specific bar position
52
- local bar_pct=$((pos * 100 / total))
53
-
54
- if [ "$bar_pct" -le 33 ]; then
55
- # Green range: 0-33%
56
- # Bright green (46) β†’ yellow-green (154) β†’ green (34)
57
- if [ "$bar_pct" -le 16 ]; then
58
- echo "38;5;46" # Bright green
59
- elif [ "$bar_pct" -le 25 ]; then
60
- echo "38;5;118" # Green-yellow
61
- else
62
- echo "38;5;154" # Yellow-green
63
- fi
64
- elif [ "$bar_pct" -le 66 ]; then
65
- # Orange range: 34-66%
66
- # Yellow (226) β†’ orange (214) β†’ dark orange (208)
67
- local adj_pct=$((bar_pct - 33))
68
- if [ "$adj_pct" -le 11 ]; then
69
- echo "38;5;226" # Yellow
70
- elif [ "$adj_pct" -le 22 ]; then
71
- echo "38;5;220" # Light orange
72
- else
73
- echo "38;5;214" # Orange
74
- fi
75
- else
76
- # Red range: 67-100%
77
- # Orange-red (208) β†’ red (196) β†’ dark red (160)
78
- local adj_pct=$((bar_pct - 66))
79
- if [ "$adj_pct" -le 11 ]; then
80
- echo "38;5;208" # Orange-red
81
- elif [ "$adj_pct" -le 22 ]; then
82
- echo "38;5;202" # Red-orange
83
- else
84
- echo "38;5;196" # Bright red
85
- fi
86
- fi
87
- }
88
-
89
- # Build progress bar with gradient colors
90
- # Track the color of the rightmost filled bar for the percentage text
91
- bar="["
92
- rightmost_color="38;5;46" # Default to bright green
93
- for ((i=0; i<filled; i++)); do
94
- bar_color=$(get_bar_color_for_position $i $bar_width)
95
- rightmost_color=$bar_color # Keep updating to track the last one
96
- bar+=$(printf "\033[${bar_color}mβ–ˆ\033[0m")
97
- done
98
- for ((i=0; i<empty; i++)); do
99
- bar+="β–‘"
100
- done
101
- bar+="]"
102
-
103
- # Get git information if in a git repo
104
- git_branch=""
105
- git_remote=""
106
- if [ -n "$cwd" ] && git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
107
- git_branch=$(git -C "$cwd" branch --show-current 2>/dev/null)
108
-
109
- # Get the upstream tracking branch
110
- upstream=$(git -C "$cwd" rev-parse --abbrev-ref --symbolic-full-name @{upstream} 2>/dev/null)
111
- if [ -n "$upstream" ]; then
112
- # upstream will be in format like "origin/POC" or "origin/main"
113
- git_remote="$upstream"
114
- fi
115
-
116
- # Get ahead/behind counts
117
- git_ahead=""
118
- git_behind=""
119
- if [ -n "$upstream" ]; then
120
- # Get number of commits ahead/behind
121
- ahead_behind=$(git -C "$cwd" rev-list --left-right --count HEAD...$upstream 2>/dev/null)
122
- if [ -n "$ahead_behind" ]; then
123
- ahead=$(echo "$ahead_behind" | awk '{print $1}')
124
- behind=$(echo "$ahead_behind" | awk '{print $2}')
125
- [ "$ahead" -gt 0 ] && git_ahead="$ahead"
126
- [ "$behind" -gt 0 ] && git_behind="$behind"
127
- fi
128
- fi
129
-
130
- # Get git status (dirty/staged indicators)
131
- git_status_indicator=""
132
- if git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
133
- porcelain=$(git -C "$cwd" status --porcelain 2>/dev/null)
134
-
135
- has_unstaged=false
136
- has_staged=false
137
-
138
- if [ -n "$porcelain" ]; then
139
- # Check for staged changes (first column not space)
140
- if echo "$porcelain" | grep -q '^[MADRC]'; then
141
- has_staged=true
142
- fi
143
-
144
- # Check for unstaged changes (second column not space, or untracked files)
145
- if echo "$porcelain" | grep -q '^\?\?' || echo "$porcelain" | grep -q '^.[MD]'; then
146
- has_unstaged=true
147
- fi
148
- fi
149
-
150
- # Build status indicator
151
- if [ "$has_unstaged" = true ] && [ "$has_staged" = true ]; then
152
- git_status_indicator=$(printf "\033[1;33m●\033[1;32m✚\033[0m") # Both
153
- elif [ "$has_unstaged" = true ]; then
154
- git_status_indicator=$(printf "\033[1;33m●\033[0m") # Unstaged (yellow)
155
- elif [ "$has_staged" = true ]; then
156
- git_status_indicator=$(printf "\033[1;32m✚\033[0m") # Staged (green)
157
- else
158
- git_status_indicator=$(printf "\033[1;32mβœ“\033[0m") # Clean (green)
159
- fi
160
- fi
161
- fi
162
-
163
- # Get project name from directory
164
- project_name=""
165
- if [ -n "$cwd" ]; then
166
- project_name=$(basename "$cwd")
167
- fi
168
-
169
- # Format tokens for display (e.g., 45.2k/200k)
170
- format_tokens() {
171
- local tokens=$1
172
- # Use pure bash arithmetic to avoid bc dependency
173
- if [ "$tokens" -ge 1000000 ]; then
174
- local millions=$((tokens / 1000000))
175
- local remainder=$((tokens % 1000000))
176
- local decimal=$((remainder / 100000))
177
- printf "%d.%dM" "$millions" "$decimal"
178
- elif [ "$tokens" -ge 1000 ]; then
179
- local thousands=$((tokens / 1000))
180
- local remainder=$((tokens % 1000))
181
- local decimal=$((remainder / 100))
182
- printf "%d.%dk" "$thousands" "$decimal"
183
- else
184
- echo "$tokens"
185
- fi
186
- }
187
-
188
- # Format tokens without decimals (e.g., 72k/200k)
189
- format_tokens_int() {
190
- local tokens=$1
191
- if [ "$tokens" -ge 1000000 ]; then
192
- local millions=$((tokens / 1000000))
193
- printf "%dM" "$millions"
194
- elif [ "$tokens" -ge 1000 ]; then
195
- local thousands=$((tokens / 1000))
196
- printf "%dk" "$thousands"
197
- else
198
- echo "$tokens"
199
- fi
200
- }
201
-
202
- used_fmt=$(format_tokens $used_tokens)
203
- max_fmt=$(format_tokens $max_tokens)
204
- used_fmt_int=$(format_tokens_int $used_tokens)
205
- max_fmt_int=$(format_tokens_int $max_tokens)
206
-
207
- # Extract cost information
208
- cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty')
209
- cost_display=""
210
- if [ -n "$cost_usd" ] && [ "$cost_usd" != "null" ]; then
211
- # Format cost to 2 decimal places
212
- cost_display=$(printf "\$%.2f" "$cost_usd")
213
- fi
214
-
215
- # Extract tools/skills/agents counts
216
- skills_count=$(echo "$input" | jq -r '.context.skills // [] | length')
217
- agents_count=$(echo "$input" | jq -r '.context.agents // [] | length')
218
- mcp_tools_count=$(echo "$input" | jq -r '.context.mcp_tools // [] | length')
219
- total_tools=$((skills_count + agents_count + mcp_tools_count))
220
- tools_display=""
221
- if [ "$total_tools" -gt 0 ]; then
222
- tools_display=$(printf "πŸ”§ %d" "$total_tools")
223
- fi
224
-
225
- # Extract background tasks (check for various possible field names)
226
- bg_tasks=$(echo "$input" | jq -r '.background_tasks // .running_agents // .active_tasks // empty | length')
227
- bg_display=""
228
- if [ -n "$bg_tasks" ] && [ "$bg_tasks" != "null" ] && [ "$bg_tasks" -gt 0 ]; then
229
- bg_display=$(printf "⏳ %d" "$bg_tasks")
230
- fi
231
-
232
- # Extract "until compact" percentage
233
- # Auto-compact triggers at a threshold (appears to be around 78% usage / 22% remaining)
234
- # The "until compact" value = remaining_percentage - threshold_remaining
235
- # If threshold is 22%, and you have 24% remaining, then until_compact = 24% - 22% = 2%
236
-
237
- # First check if there's an explicit field (unlikely but worth checking)
238
- until_compact=$(echo "$input" | jq -r '.context_window.until_compact // .context_window.until_auto_compact // empty')
239
-
240
- if [ -z "$until_compact" ]; then
241
- # Calculate based on auto-compact threshold
242
- # Auto-compact threshold appears to be 22% remaining (78% used)
243
- AUTO_COMPACT_THRESHOLD=22
244
-
245
- remaining_pct=$(echo "$input" | jq -r '.context_window.remaining_percentage // 0')
246
-
247
- if [ "$remaining_pct" -gt "$AUTO_COMPACT_THRESHOLD" ]; then
248
- until_compact=$((remaining_pct - AUTO_COMPACT_THRESHOLD))
249
- else
250
- # Already past threshold or at it
251
- until_compact=0
252
- fi
253
- fi
254
-
255
- compact_indicator=""
256
- compact_color="38;5;46"
257
- if [ -n "$until_compact" ] && [ "$until_compact" != "null" ] && [ "$until_compact" != "0" ]; then
258
- if [ "$until_compact" -ge 25 ]; then
259
- compact_color="38;5;46"
260
- elif [ "$until_compact" -ge 20 ]; then
261
- compact_color="38;5;154"
262
- elif [ "$until_compact" -ge 15 ]; then
263
- compact_color="38;5;226"
264
- elif [ "$until_compact" -ge 10 ]; then
265
- compact_color="38;5;220"
266
- elif [ "$until_compact" -ge 7 ]; then
267
- compact_color="38;5;214"
268
- elif [ "$until_compact" -ge 4 ]; then
269
- compact_color="38;5;208"
270
- else
271
- compact_color="38;5;196"
272
- fi
273
-
274
- # Calculate tokens until compact threshold
275
- # Auto-compact threshold is 22% remaining (78% used)
276
- # Tokens until compact = until_compact * context_window_size / 100
277
- remaining_pct=$(echo "$input" | jq -r '.context_window.remaining_percentage // 0')
278
- tokens_until_compact=$((until_compact * max_tokens / 100))
279
- tokens_until_compact_fmt=$(format_tokens $tokens_until_compact)
280
-
281
- # Build indicator with tokens primary (gradient color) and percentage in parens (dim)
282
- compact_indicator_text=$(printf "⚑%s until compact" "$tokens_until_compact_fmt")
283
- compact_indicator_pct=$(printf "(%d%%)" "$until_compact")
284
- fi
285
-
286
- # ==========================================
287
- # 2-LINE DASHBOARD LAYOUT
288
- # ==========================================
289
-
290
- # LINE 1: Model + Context
291
- # Format: Opus 4.5 ($12.01) [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘βš‘β–‘β–‘β–‘β–‘β–‘] 74k/200k
292
- line1=""
293
-
294
- # Robot icon and model name in cyan
295
- if [ -n "$model" ]; then
296
- line1+=$(printf "πŸ€– \033[1;36m%s\033[0m" "$model")
297
- fi
298
-
299
- # Add cost after model name (bright green)
300
- if [ -n "$cost_display" ]; then
301
- line1+=$(printf " \033[1;32m(%s)\033[0m" "$cost_display")
302
- fi
303
-
304
- # Add subtle separator between cost and progress bar
305
- line1+=$(printf " \033[2;37mβ”‚\033[0m")
306
-
307
- # Build progress bar for line 1
308
- # Calculate compact threshold position (78% of bar = 22% remaining)
309
- AUTO_COMPACT_THRESHOLD=22
310
- compact_threshold_pct=$((100 - AUTO_COMPACT_THRESHOLD)) # 78%
311
- threshold_position=$((compact_threshold_pct * bar_width / 100))
312
-
313
- # Build unified progress bar with per-bar gradient toward threshold
314
- unified_bar="["
315
- unified_rightmost_color="38;5;46" # Default green
316
-
317
- for ((i=0; i<bar_width; i++)); do
318
- if [ "$i" -eq "$threshold_position" ]; then
319
- # Insert red lightning bolt threshold marker
320
- unified_bar+=$(printf "\033[0;31m⚑\033[0m")
321
- fi
322
-
323
- if [ "$i" -lt "$filled" ]; then
324
- # Calculate color for this specific bar position
325
- # Gradient from green (position 0) to red (threshold position)
326
- if [ "$threshold_position" -gt 0 ]; then
327
- bar_position_pct=$((i * 100 / threshold_position))
328
- else
329
- bar_position_pct=0
330
- fi
331
-
332
- # Map position percentage to gradient colors
333
- if [ "$bar_position_pct" -le 20 ]; then
334
- bar_color="38;5;46" # Bright green
335
- elif [ "$bar_position_pct" -le 40 ]; then
336
- bar_color="38;5;118" # Green-yellow
337
- elif [ "$bar_position_pct" -le 60 ]; then
338
- bar_color="38;5;154" # Yellow-green
339
- elif [ "$bar_position_pct" -le 70 ]; then
340
- bar_color="38;5;226" # Yellow
341
- elif [ "$bar_position_pct" -le 80 ]; then
342
- bar_color="38;5;220" # Light orange
343
- elif [ "$bar_position_pct" -le 90 ]; then
344
- bar_color="38;5;214" # Orange
345
- elif [ "$bar_position_pct" -le 95 ]; then
346
- bar_color="38;5;208" # Orange-red
347
- else
348
- bar_color="38;5;196" # Red
349
- fi
350
-
351
- unified_rightmost_color=$bar_color
352
- unified_bar+=$(printf "\033[${bar_color}mβ–ˆ\033[0m")
353
- else
354
- unified_bar+="β–‘"
355
- fi
356
- done
357
-
358
- unified_bar+="]"
359
-
360
- # Add progress bar and tokens to line 1
361
- line1+=$(printf " %s \033[${unified_rightmost_color}m%s\033[0m\033[1;36m/%s\033[0m" "$unified_bar" "$used_fmt_int" "$max_fmt_int")
362
-
363
- # LINE 2: Project/Git Info
364
- # Format: πŸ“ trellis POC β†’ origin/POC ↑11
365
- line2=""
366
-
367
- # Folder icon and project name in cyan
368
- if [ -n "$project_name" ]; then
369
- line2+=$(printf "πŸ“ \033[1;36m%s\033[0m" "$project_name")
370
- fi
371
-
372
- # Git branch and tracking
373
- if [ -n "$git_branch" ]; then
374
- [ -n "$line2" ] && line2+=" "
375
-
376
- # Branch in magenta
377
- line2+=$(printf "\033[1;35m%s\033[0m" "$git_branch")
378
-
379
- # Git status indicator (after branch name)
380
- if [ -n "$git_status_indicator" ]; then
381
- line2+=$(printf " %s" "$git_status_indicator")
382
- fi
383
-
384
- # Arrow and tracking
385
- line2+=$(printf " \033[2;37m→\033[0m")
386
-
387
- if [ -n "$git_remote" ]; then
388
- # Tracking branch in blue
389
- line2+=$(printf " \033[1;34m%s\033[0m" "$git_remote")
390
-
391
- # Ahead/behind indicators
392
- if [ -n "$git_ahead" ] || [ -n "$git_behind" ]; then
393
- line2+=" "
394
- [ -n "$git_ahead" ] && line2+=$(printf "\033[0;32m↑%s\033[0m" "$git_ahead")
395
- if [ -n "$git_behind" ]; then
396
- [ -n "$git_ahead" ] && line2+=" "
397
- line2+=$(printf "\033[0;31m↓%s\033[0m" "$git_behind")
398
- fi
399
- fi
400
- else
401
- line2+=$(printf " \033[2;37m(no upstream)\033[0m")
402
- fi
403
- fi
404
-
405
- # GSD Update check (appended to line 2 if update available)
406
- gsd_update_suffix=""
407
- gsd_cache_file="$HOME/.claude/cache/gsd-update-check.json"
408
-
409
- # Check for GSD in project first, then global
410
- gsd_version_file=""
411
- if [ -n "$cwd" ] && [ -f "$cwd/.claude/get-shit-done/VERSION" ]; then
412
- gsd_version_file="$cwd/.claude/get-shit-done/VERSION"
413
- elif [ -f "$HOME/.claude/get-shit-done/VERSION" ]; then
414
- gsd_version_file="$HOME/.claude/get-shit-done/VERSION"
415
- fi
416
-
417
- if [ -n "$gsd_version_file" ]; then
418
- # Read installed version directly from the VERSION file we found
419
- installed_ver=$(cat "$gsd_version_file" 2>/dev/null | tr -d '[:space:]')
420
- [ -z "$installed_ver" ] && installed_ver="0.0.0"
421
-
422
- # Get latest version from cache (populated by SessionStart hook)
423
- if [ -f "$gsd_cache_file" ]; then
424
- latest_ver=$(jq -r '.latest // "unknown"' "$gsd_cache_file" 2>/dev/null)
425
-
426
- # Compare versions - append to line2 if they differ
427
- if [ "$latest_ver" != "unknown" ] && [ "$installed_ver" != "$latest_ver" ]; then
428
- gsd_update_suffix=$(printf " | GSD %s>%s" "$installed_ver" "$latest_ver")
429
- fi
430
- fi
431
- fi
432
-
433
- # Append GSD update to line2 if available
434
- line2+="$gsd_update_suffix"
435
-
436
- # Output the dashboard (2 lines, no trailing newline)
437
- printf "%s\033[K\n%s\033[K" "$line1" "$line2"