coder-config 0.49.2-beta → 0.49.4-beta

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.
@@ -19,8 +19,8 @@
19
19
 
20
20
  <!-- PWA Manifest -->
21
21
  <link rel="manifest" href="/manifest.json">
22
- <script type="module" crossorigin src="/assets/index-zVvhh8v5.js"></script>
23
- <link rel="stylesheet" crossorigin href="/assets/index-C0x-uedt.css">
22
+ <script type="module" crossorigin src="/assets/index-DyuQWsYT.js"></script>
23
+ <link rel="stylesheet" crossorigin href="/assets/index-C0-_yjwc.css">
24
24
  </head>
25
25
  <body>
26
26
  <div id="root"></div>
@@ -1,181 +1,309 @@
1
1
  /**
2
- * Statuslines Routes - Library of Claude Code statusCommand presets
2
+ * Statuslines Routes
3
+ *
4
+ * Claude Code's statusLine feature sends JSON session data to a script via stdin.
5
+ * Scripts parse it with jq and print text for the status bar.
6
+ *
7
+ * Settings format in ~/.claude/settings.json:
8
+ * { "statusLine": { "type": "command", "command": "~/.claude/statuslines/<id>.sh" } }
9
+ *
10
+ * JSON fields available: model.display_name, context_window.used_percentage,
11
+ * context_window.context_window_size, context_window.current_usage.*,
12
+ * cost.total_cost_usd, cost.total_duration_ms, cost.total_lines_added,
13
+ * cost.total_lines_removed, workspace.current_dir, session_id, version
3
14
  */
4
15
 
5
16
  const fs = require('fs');
6
17
  const path = require('path');
7
18
  const os = require('os');
19
+ const { execSync } = require('child_process');
20
+
21
+ const STATUSLINES_DIR = path.join(os.homedir(), '.claude', 'statuslines');
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Script templates
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const SCRIPTS = {
28
+ minimal: `#!/bin/bash
29
+ # Minimal: model name and context percentage
30
+ input=$(cat)
31
+ MODEL=$(echo "$input" | jq -r '.model.display_name // "?"')
32
+ PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
33
+ echo "* $MODEL $PCT% ctx"
34
+ `,
35
+
36
+ 'context-bar': `#!/bin/bash
37
+ # Context Bar: model with dot-gauge context usage
38
+ input=$(cat)
39
+ MODEL=$(echo "$input" | jq -r '.model.display_name // "?"')
40
+ PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
41
+ FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
42
+ BAR=""; for ((i=0; i<FILLED; i++)); do BAR="$BAR●"; done
43
+ for ((i=0; i<EMPTY; i++)); do BAR="$BAR○"; done
44
+ echo "* $MODEL ctx $BAR $PCT%"
45
+ `,
46
+
47
+ 'git-context': `#!/bin/bash
48
+ # Git + Context: model, context bar, git branch, lines changed
49
+ input=$(cat)
50
+ MODEL=$(echo "$input" | jq -r '.model.display_name // "?"')
51
+ PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
52
+ LINES_ADD=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
53
+ LINES_REM=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
54
+ FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
55
+ BAR=""; for ((i=0; i<FILLED; i++)); do BAR="$BAR●"; done
56
+ for ((i=0; i<EMPTY; i++)); do BAR="$BAR○"; done
57
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')
58
+ OUT="* $MODEL | ctx $BAR $PCT%"
59
+ [ -n "$BRANCH" ] && OUT="$OUT | $BRANCH"
60
+ [ "$LINES_ADD" != "0" ] || [ "$LINES_REM" != "0" ] && OUT="$OUT | +$LINES_ADD -$LINES_REM"
61
+ echo "$OUT"
62
+ `,
63
+
64
+ full: `#!/bin/bash
65
+ # Full: model, context with token counts, lines changed, git branch, duration, cost
66
+ input=$(cat)
67
+ MODEL=$(echo "$input" | jq -r '.model.display_name // "?"')
68
+ PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
69
+ CTX_USED=$(echo "$input" | jq -r '((.context_window.current_usage.input_tokens // 0) + (.context_window.current_usage.cache_creation_input_tokens // 0) + (.context_window.current_usage.cache_read_input_tokens // 0))')
70
+ CTX_MAX=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
71
+ LINES_ADD=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
72
+ LINES_REM=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
73
+ DUR_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
74
+ COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
75
+
76
+ FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
77
+ BAR=""; for ((i=0; i<FILLED; i++)); do BAR="$BAR●"; done
78
+ for ((i=0; i<EMPTY; i++)); do BAR="$BAR○"; done
79
+
80
+ CTX_K=$(awk "BEGIN {printf \\"%.1fK\\", $CTX_USED/1000}")
81
+ MAX_K=$(awk "BEGIN {printf \\"%.1fK\\", $CTX_MAX/1000}")
82
+ HOURS=$((DUR_MS / 3600000)); MINS=$(((DUR_MS % 3600000) / 60000))
83
+ COST_FMT=$(printf '$%.3f' $COST)
84
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')
85
+
86
+ OUT="* $MODEL | ctx $BAR $CTX_K/$MAX_K"
87
+ [ "$LINES_ADD" != "0" ] || [ "$LINES_REM" != "0" ] && OUT="$OUT | +$LINES_ADD -$LINES_REM"
88
+ [ -n "$BRANCH" ] && OUT="$OUT | $BRANCH"
89
+ [ "$HOURS" -gt 0 ] && OUT="$OUT | \${HOURS}h \${MINS}m" || OUT="$OUT | \${MINS}m"
90
+ OUT="$OUT | $COST_FMT"
91
+ echo "$OUT"
92
+ `,
93
+
94
+ 'cost-tracker': `#!/bin/bash
95
+ # Cost Tracker: model, session cost, duration
96
+ input=$(cat)
97
+ MODEL=$(echo "$input" | jq -r '.model.display_name // "?"')
98
+ COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
99
+ DUR_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
100
+ COST_FMT=$(printf '$%.3f' $COST)
101
+ MINS=$((DUR_MS / 60000)); SECS=$(((DUR_MS % 60000) / 1000))
102
+ echo "* $MODEL | $COST_FMT | \${MINS}m \${SECS}s"
103
+ `,
104
+
105
+ multiline: `#!/bin/bash
106
+ # Multiline: line 1 = model + git, line 2 = color context bar + cost
107
+ input=$(cat)
108
+ MODEL=$(echo "$input" | jq -r '.model.display_name // "?"')
109
+ PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
110
+ COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
111
+ DUR_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
112
+
113
+ GREEN='\\033[32m'; YELLOW='\\033[33m'; RED='\\033[31m'
114
+ CYAN='\\033[36m'; RESET='\\033[0m'
115
+ [ "$PCT" -ge 90 ] && BAR_COLOR="$RED" || { [ "$PCT" -ge 70 ] && BAR_COLOR="$YELLOW" || BAR_COLOR="$GREEN"; }
116
+
117
+ FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
118
+ BAR=""; for ((i=0; i<FILLED; i++)); do BAR="$BAR█"; done
119
+ for ((i=0; i<EMPTY; i++)); do BAR="$BAR░"; done
120
+
121
+ MINS=$((DUR_MS / 60000)); SECS=$(((DUR_MS % 60000) / 1000))
122
+ COST_FMT=$(printf '$%.3f' $COST)
123
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')
124
+ [ -n "$BRANCH" ] && BRANCH_STR=" | $BRANCH" || BRANCH_STR=""
125
+
126
+ echo -e "$CYAN* $MODEL$RESET$BRANCH_STR"
127
+ echo -e "$BAR_COLOR$BAR$RESET $PCT% | $YELLOW$COST_FMT$RESET | \${MINS}m \${SECS}s"
128
+ `,
129
+ };
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Preset metadata (no script content here — see SCRIPTS above)
133
+ // ---------------------------------------------------------------------------
8
134
 
9
- /**
10
- * Preset statusline library.
11
- * Each preset's `command` is set as `statusCommand` in ~/.claude/settings.json.
12
- * Empty command string = remove statusCommand (use Claude Code built-in).
13
- */
14
135
  const PRESETS = [
15
136
  {
16
- id: 'default',
17
- name: 'Claude Code Default',
18
- description: 'Built-in statusline from Claude Code (model, context, git, cost)',
19
- preview: '* opus-4-6 | ctx ●●●●○○○○○○ 74.4K/200.0K | +146 -13 | main | 5h 7% | 7d 11% • 6d left',
20
- command: '',
137
+ id: 'disabled',
138
+ name: 'Disabled',
139
+ description: 'No status bar shown',
140
+ preview: '',
21
141
  category: 'Built-in',
22
142
  },
23
143
  {
24
- id: 'git-branch',
25
- name: 'Git Branch',
26
- description: 'Shows current branch and dirty file count',
27
- preview: ' main 2 changed',
28
- command: "branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) && dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') && echo \"${branch} ${dirty} changed\"",
29
- category: 'Git',
144
+ id: 'minimal',
145
+ name: 'Minimal',
146
+ description: 'Model name and context percentage',
147
+ preview: '* opus-4-6 37% ctx',
148
+ category: 'Simple',
30
149
  },
31
150
  {
32
- id: 'git-extended',
33
- name: 'Git Extended',
34
- description: 'Branch, staged/unstaged counts, and last commit message',
35
- preview: ' main +3 ~1 "fix: auth bug"',
36
- command: "branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?') && staged=$(git diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ') && unstaged=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ') && msg=$(git log -1 --pretty=%s 2>/dev/null | cut -c1-40) && echo \" ${branch} +${staged} ~${unstaged} \\\"${msg}\\\"\"",
37
- category: 'Git',
151
+ id: 'context-bar',
152
+ name: 'Context Bar',
153
+ description: 'Model with a ●○ dot gauge showing context usage',
154
+ preview: '* opus-4-6 ctx ●●●●○○○○○○ 37%',
155
+ category: 'Simple',
38
156
  },
39
157
  {
40
- id: 'git-sync',
41
- name: 'Git Sync Status',
42
- description: 'Branch with ahead/behind counts relative to remote',
43
- preview: ' main ↑2 ↓0',
44
- command: "branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?') && ahead=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo 0) && behind=$(git rev-list --count HEAD..@{u} 2>/dev/null || echo 0) && echo \" ${branch} ↑${ahead} ↓${behind}\"",
158
+ id: 'git-context',
159
+ name: 'Git + Context',
160
+ description: 'Model, context bar, git branch, and lines changed this session',
161
+ preview: '* opus-4-6 | ctx ●●●●○○○○○○ 37% | main | +146 -13',
45
162
  category: 'Git',
46
163
  },
47
164
  {
48
- id: 'project-context',
49
- name: 'Project Context',
50
- description: 'Repo name, branch, and uncommitted file count',
51
- preview: 'coder-config main 3 pending',
52
- command: "repo=$(basename $(git rev-parse --show-toplevel 2>/dev/null) 2>/dev/null || basename $PWD) && branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '') && count=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') && echo \"${repo} ${branch} ${count} pending\"",
165
+ id: 'full',
166
+ name: 'Full',
167
+ description: 'Everything: model, context with token counts, lines, branch, duration, cost',
168
+ preview: '* opus-4-6 | ctx ●●●●○○○○○○ 74.4K/200.0K | +146 -13 | main | 5h 2m | $0.142',
53
169
  category: 'Git',
54
170
  },
55
171
  {
56
- id: 'clock',
57
- name: 'Clock',
58
- description: 'Current time (HH:MM)',
59
- preview: ' 14:32',
60
- command: "date +' %H:%M'",
61
- category: 'System',
62
- },
63
- {
64
- id: 'datetime',
65
- name: 'Date & Time',
66
- description: 'Full date and time',
67
- preview: ' Mon Mar 04 14:32',
68
- command: "date +' %a %b %d %H:%M'",
69
- category: 'System',
172
+ id: 'cost-tracker',
173
+ name: 'Cost Tracker',
174
+ description: 'Model, total session cost, and elapsed time',
175
+ preview: '* opus-4-6 | $0.142 | 32m 15s',
176
+ category: 'Cost',
70
177
  },
71
178
  {
72
- id: 'system-load',
73
- name: 'System Load',
74
- description: 'CPU load average and memory usage',
75
- preview: ' load 1.2 mem 8.4G/16G',
76
- command: "load=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}' | tr -d ',') && mem=$(vm_stat 2>/dev/null | awk '/Pages free/{free=$3} /Pages active/{active=$3} /Pages inactive/{inact=$3} END{total=(free+active+inact)*4096/1073741824; used=active*4096/1073741824; printf \"%.1fG/%.1fG\", used, total}' 2>/dev/null || echo '?') && echo \" load ${load} mem ${mem}\"",
77
- category: 'System',
78
- },
79
- {
80
- id: 'git-clock',
81
- name: 'Git + Clock',
82
- description: 'Branch and current time',
83
- preview: ' main 14:32',
84
- command: "branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '') && echo \" ${branch} $(date +'%H:%M')\"",
85
- category: 'Combo',
86
- },
87
- {
88
- id: 'git-project-clock',
89
- name: 'Git + Project + Clock',
90
- description: 'Repo name, branch with diff stats, and time',
91
- preview: 'coder-config main +3 ~1 14:32',
92
- command: "repo=$(basename $(git rev-parse --show-toplevel 2>/dev/null) 2>/dev/null || basename $PWD) && branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '') && added=$(git diff --stat HEAD 2>/dev/null | grep -E '^.*\\+' | tail -1 | grep -o '[0-9]* insertion' | grep -o '[0-9]*' || echo 0) && deleted=$(git diff --stat HEAD 2>/dev/null | grep -E 'deletion' | tail -1 | grep -o '[0-9]* deletion' | grep -o '[0-9]*' || echo 0) && echo \"${repo} ${branch} +${added} ~${deleted} $(date +'%H:%M')\"",
93
- category: 'Combo',
94
- },
95
- {
96
- id: 'minimal-branch',
97
- name: 'Minimal',
98
- description: 'Just the git branch, nothing else',
99
- preview: 'main',
100
- command: "git rev-parse --abbrev-ref HEAD 2>/dev/null || echo ''",
101
- category: 'Minimal',
179
+ id: 'multiline',
180
+ name: 'Multiline',
181
+ description: 'Two rows: model + branch on top, color-coded context bar + cost below',
182
+ preview: '* opus-4-6 | main\n█████░░░░░ 37% | $0.142 | 32m 15s',
183
+ category: 'Cost',
102
184
  },
103
185
  {
104
186
  id: 'custom',
105
- name: 'Custom',
106
- description: 'Write your own shell command',
107
- preview: '(your output here)',
108
- command: null, // null = user-defined
187
+ name: 'Custom Script',
188
+ description: 'Write your own bash script — receives Claude Code JSON via stdin',
189
+ preview: null,
109
190
  category: 'Custom',
110
191
  },
111
192
  ];
112
193
 
113
- /**
114
- * Returns the full preset library
115
- */
194
+ // ---------------------------------------------------------------------------
195
+ // Helpers
196
+ // ---------------------------------------------------------------------------
197
+
198
+ function scriptPath(presetId) {
199
+ return path.join(STATUSLINES_DIR, `${presetId}.sh`);
200
+ }
201
+
202
+ function expandHome(p) {
203
+ return p.replace(/^~/, os.homedir());
204
+ }
205
+
206
+ function settingsPath() {
207
+ return path.join(os.homedir(), '.claude', 'settings.json');
208
+ }
209
+
210
+ function readSettings() {
211
+ const p = settingsPath();
212
+ if (!fs.existsSync(p)) return {};
213
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; }
214
+ }
215
+
216
+ function writeSettings(settings) {
217
+ const p = settingsPath();
218
+ const dir = path.dirname(p);
219
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
220
+ fs.writeFileSync(p, JSON.stringify(settings, null, 2) + '\n', 'utf8');
221
+ }
222
+
223
+ function ensureScriptDir() {
224
+ if (!fs.existsSync(STATUSLINES_DIR)) fs.mkdirSync(STATUSLINES_DIR, { recursive: true });
225
+ }
226
+
227
+ function writeScript(presetId, content) {
228
+ ensureScriptDir();
229
+ const p = scriptPath(presetId);
230
+ fs.writeFileSync(p, content, 'utf8');
231
+ fs.chmodSync(p, 0o755);
232
+ return p;
233
+ }
234
+
235
+ function commandPathInSettings(settings) {
236
+ return settings?.statusLine?.command || null;
237
+ }
238
+
239
+ function matchPresetFromCommand(cmd) {
240
+ if (!cmd) return 'disabled';
241
+ // Match against known script paths
242
+ for (const preset of PRESETS) {
243
+ if (preset.id === 'disabled' || preset.id === 'custom') continue;
244
+ const expected = scriptPath(preset.id);
245
+ const expectedHome = expected.replace(os.homedir(), '~');
246
+ if (cmd === expected || cmd === expectedHome) return preset.id;
247
+ }
248
+ return 'custom';
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Route handlers
253
+ // ---------------------------------------------------------------------------
254
+
116
255
  function getStatuslinePresets() {
117
256
  return { presets: PRESETS };
118
257
  }
119
258
 
120
- /**
121
- * Reads current statusCommand from ~/.claude/settings.json
122
- */
123
259
  function getCurrentStatusline() {
124
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
260
+ const settings = readSettings();
261
+ const cmd = commandPathInSettings(settings);
262
+ const presetId = matchPresetFromCommand(cmd);
125
263
 
126
- try {
127
- if (!fs.existsSync(settingsPath)) {
128
- return { command: '', presetId: 'default' };
264
+ // For custom, also return the current script content
265
+ let scriptContent = '';
266
+ if (presetId === 'custom' && cmd) {
267
+ const resolved = expandHome(cmd);
268
+ if (fs.existsSync(resolved)) {
269
+ scriptContent = fs.readFileSync(resolved, 'utf8');
129
270
  }
130
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
131
- const command = settings.statusCommand || '';
132
- const matched = PRESETS.find(p => p.command !== null && p.command === command);
133
- return {
134
- command,
135
- presetId: matched ? matched.id : (command ? 'custom' : 'default'),
136
- };
137
- } catch (e) {
138
- return { command: '', presetId: 'default', error: e.message };
139
271
  }
272
+
273
+ return { command: cmd || '', presetId, scriptContent };
140
274
  }
141
275
 
142
276
  /**
143
- * Saves statusCommand to ~/.claude/settings.json.
144
- * Pass command='' to remove (revert to Claude Code built-in).
277
+ * Apply a preset or custom script.
278
+ * body: { presetId: string, scriptContent?: string }
145
279
  */
146
280
  function setStatusline(body) {
147
- const { command } = body;
148
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
281
+ const { presetId, scriptContent } = body;
149
282
 
150
283
  try {
151
- const claudeDir = path.dirname(settingsPath);
152
- if (!fs.existsSync(claudeDir)) {
153
- fs.mkdirSync(claudeDir, { recursive: true });
154
- }
284
+ const settings = readSettings();
155
285
 
156
- let settings = {};
157
- if (fs.existsSync(settingsPath)) {
158
- try {
159
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
160
- } catch (e) {
161
- settings = {};
162
- }
286
+ if (presetId === 'disabled') {
287
+ delete settings.statusLine;
288
+ writeSettings(settings);
289
+ return { success: true, presetId: 'disabled', command: '' };
163
290
  }
164
291
 
165
- if (command === '' || command == null) {
166
- delete settings.statusCommand;
292
+ let cmd;
293
+
294
+ if (presetId === 'custom') {
295
+ if (!scriptContent) return { success: false, error: 'scriptContent required for custom preset' };
296
+ cmd = writeScript('custom', scriptContent);
167
297
  } else {
168
- settings.statusCommand = command;
298
+ const template = SCRIPTS[presetId];
299
+ if (!template) return { success: false, error: `Unknown preset: ${presetId}` };
300
+ cmd = writeScript(presetId, template);
169
301
  }
170
302
 
171
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
303
+ settings.statusLine = { type: 'command', command: cmd };
304
+ writeSettings(settings);
172
305
 
173
- const matched = PRESETS.find(p => p.command !== null && p.command === (command || ''));
174
- return {
175
- success: true,
176
- command: command || '',
177
- presetId: matched ? matched.id : (command ? 'custom' : 'default'),
178
- };
306
+ return { success: true, presetId, command: cmd };
179
307
  } catch (e) {
180
308
  return { success: false, error: e.message };
181
309
  }