coder-config 0.54.3-beta → 0.54.5

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/lib/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Constants and tool path configurations
3
3
  */
4
4
 
5
- const VERSION = '0.54.3-beta';
5
+ const VERSION = '0.54.5';
6
6
 
7
7
  // Tool-specific path configurations
8
8
  const TOOL_PATHS = {
@@ -1675,7 +1675,18 @@ _coder_workstream_cd() {
1675
1675
  [ -z "$result" ] && return 0
1676
1676
  count=$(echo "$result" | grep -o '"count":[0-9]*' | cut -d: -f2)
1677
1677
  current=$(echo "$result" | grep -o '"current":true' || echo "")
1678
- [ -n "$current" ] && return 0
1678
+ if [ -n "$current" ]; then
1679
+ # Already on this workstream — silently sync the color env var if it drifted
1680
+ # (e.g. workstream activated before color was added, or color edited since).
1681
+ local cur_color
1682
+ cur_color=$(echo "$result" | grep -o '"color":"[^"]*"' | head -1 | cut -d'"' -f4)
1683
+ if [ -n "$cur_color" ]; then
1684
+ [ "$CODER_WORKSTREAM_COLOR" != "$cur_color" ] && export CODER_WORKSTREAM_COLOR="$cur_color"
1685
+ else
1686
+ [ -n "$CODER_WORKSTREAM_COLOR" ] && unset CODER_WORKSTREAM_COLOR
1687
+ fi
1688
+ return 0
1689
+ fi
1679
1690
  [ "$count" = "0" ] && return 0
1680
1691
  if [ "$count" = "1" ]; then
1681
1692
  local name id color
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-config",
3
- "version": "0.54.3-beta",
3
+ "version": "0.54.5",
4
4
  "description": "Configuration manager for AI coding tools - Claude Code, Gemini CLI, Codex CLI, Antigravity. Manage MCPs, rules, permissions, memory, and workstreams.",
5
5
  "author": "regression.io",
6
6
  "main": "config-loader.js",
@@ -62,7 +62,115 @@ echo "$OUT"
62
62
  `,
63
63
 
64
64
  full: `#!/bin/bash
65
- # Full: model, colored context bar, token counts, lines, branch, duration, cost, workstream
65
+ # Full: model, context bar, 5H/7D rate-limit bars, lines, branch, cost, workstream
66
+ input=$(cat)
67
+ MODEL=$(echo "$input" | jq -r '.model.display_name // "?"')
68
+ MODEL_SHORT=$(echo "$MODEL" | cut -c1-12)
69
+ [ "\${#MODEL}" -gt 12 ] && MODEL_SHORT="\${MODEL_SHORT}…"
70
+ PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
71
+ 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))')
72
+ CTX_MAX=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
73
+ LINES_ADD=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
74
+ LINES_REM=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
75
+ COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
76
+ RL_5H_PCT=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty' | cut -d. -f1)
77
+ RL_5H_RESET=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
78
+ RL_7D_PCT=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty' | cut -d. -f1)
79
+
80
+ GREEN='\\033[32m'; YELLOW='\\033[33m'; RED='\\033[31m'
81
+ CYAN='\\033[36m'; DIM='\\033[2m'; RESET='\\033[0m'
82
+ [ "$PCT" -ge 90 ] && BAR_COLOR="$RED" || { [ "$PCT" -ge 70 ] && BAR_COLOR="$YELLOW" || BAR_COLOR="$GREEN"; }
83
+
84
+ FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
85
+ BAR=""; [ "$FILLED" -gt 0 ] && BAR="$BAR$(printf '●%.0s' $(seq 1 $FILLED))"
86
+ [ "$EMPTY" -gt 0 ] && BAR="$BAR$(printf '○%.0s' $(seq 1 $EMPTY))"
87
+
88
+ make_block_bar() {
89
+ local pct=\$1 color=\$2 width=8
90
+ [ -z "\$pct" ] && pct=0
91
+ local filled=\$((pct * width / 100))
92
+ [ "\$filled" -gt "\$width" ] && filled=\$width
93
+ [ "\$filled" -eq 0 ] && [ "\$pct" -gt 0 ] && filled=1
94
+ local empty=\$((width - filled))
95
+ local out=""
96
+ [ "\$filled" -gt 0 ] && out="\${color}\$(printf '▰%.0s' \$(seq 1 \$filled))\${RESET}"
97
+ [ "\$empty" -gt 0 ] && out="\$out\${DIM}\$(printf '▰%.0s' \$(seq 1 \$empty))\${RESET}"
98
+ printf '%s' "\$out"
99
+ }
100
+ time_until_hm() {
101
+ local resets=\$1
102
+ local now; now=\$(date +%s)
103
+ local diff=\$((resets - now))
104
+ [ "\$diff" -lt 0 ] && diff=0
105
+ local h=\$((diff / 3600))
106
+ local m=\$(((diff % 3600) / 60))
107
+ printf '%dH %dM' "\$h" "\$m"
108
+ }
109
+ rl_color() {
110
+ local pct=\$1
111
+ [ -z "\$pct" ] && { printf '%s' "$GREEN"; return; }
112
+ if [ "\$pct" -ge 90 ]; then printf '%s' "$RED"
113
+ elif [ "\$pct" -ge 70 ]; then printf '%s' "$YELLOW"
114
+ else printf '%s' "$GREEN"; fi
115
+ }
116
+
117
+ fmt_tokens() {
118
+ local n=\$1
119
+ if [ "\$n" -ge 1000000 ]; then
120
+ awk "BEGIN {v=\$n/1000000; if (v == int(v)) printf \\"%dM\\", v; else printf \\"%.1fM\\", v}"
121
+ else
122
+ local k=\$(( (n + 500) / 1000 ))
123
+ if [ "\$k" -ge 1000 ]; then
124
+ awk "BEGIN {v=\$k/1000; if (v == int(v)) printf \\"%dM\\", v; else printf \\"%.1fM\\", v}"
125
+ else
126
+ printf '%dK' "\$k"
127
+ fi
128
+ fi
129
+ }
130
+ CTX_K=$(fmt_tokens $CTX_USED)
131
+ MAX_K=$(fmt_tokens $CTX_MAX)
132
+ COST_FMT=$(printf '$%.3f' $COST)
133
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')
134
+
135
+ WS_TAG=""
136
+ if [ -n "$CODER_WORKSTREAM" ]; then
137
+ case "$CODER_WORKSTREAM_COLOR" in
138
+ red) WS_COLOR='\\033[38;5;203m' ;;
139
+ orange) WS_COLOR='\\033[38;5;208m' ;;
140
+ yellow) WS_COLOR='\\033[38;5;221m' ;;
141
+ green) WS_COLOR='\\033[38;5;120m' ;;
142
+ cyan) WS_COLOR='\\033[38;5;87m' ;;
143
+ blue) WS_COLOR='\\033[38;5;111m' ;;
144
+ purple) WS_COLOR='\\033[38;5;177m' ;;
145
+ pink) WS_COLOR='\\033[38;5;213m' ;;
146
+ gray) WS_COLOR='\\033[38;5;245m' ;;
147
+ *) WS_COLOR="$CYAN" ;;
148
+ esac
149
+ WS_TAG=" | \${WS_COLOR}◆ \${CODER_WORKSTREAM}\${RESET}"
150
+ fi
151
+
152
+ OUT="\${CYAN}\${MODEL_SHORT}\${RESET} \${BAR_COLOR}\${BAR}\${RESET} \${DIM}\${CTX_K}/\${MAX_K}\${RESET} (\${PCT}%)"
153
+
154
+ if [ -n "$RL_5H_PCT" ]; then
155
+ C5=$(rl_color "$RL_5H_PCT")
156
+ B5=$(make_block_bar "$RL_5H_PCT" "$C5")
157
+ OUT="$OUT | \${DIM}5H:\${RESET} \${B5} \${RL_5H_PCT}%"
158
+ [ -n "$RL_5H_RESET" ] && OUT="$OUT \${DIM}$(time_until_hm "$RL_5H_RESET")\${RESET}"
159
+ fi
160
+ if [ -n "$RL_7D_PCT" ]; then
161
+ C7=$(rl_color "$RL_7D_PCT")
162
+ B7=$(make_block_bar "$RL_7D_PCT" "$C7")
163
+ OUT="$OUT | \${DIM}7D:\${RESET} \${B7} \${RL_7D_PCT}%"
164
+ fi
165
+
166
+ [ "$LINES_ADD" != "0" ] || [ "$LINES_REM" != "0" ] && OUT="$OUT | \${GREEN}+\${LINES_ADD}\${RESET} \${RED}-\${LINES_REM}\${RESET}"
167
+ [ -n "$BRANCH" ] && OUT="$OUT | \${CYAN}\${BRANCH}\${RESET}"
168
+ OUT="$OUT | \${YELLOW}\${COST_FMT}\${RESET}\${WS_TAG}"
169
+ echo -e "$OUT"
170
+ `,
171
+
172
+ classic: `#!/bin/bash
173
+ # Classic: model, colored context bar, token counts, lines, branch, duration, cost, workstream
66
174
  input=$(cat)
67
175
  MODEL=$(echo "$input" | jq -r '.model.display_name // "?"')
68
176
  PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
@@ -88,8 +196,6 @@ HOURS=$((DUR_MS / 3600000)); MINS=$(((DUR_MS % 3600000) / 60000))
88
196
  COST_FMT=$(printf '$%.3f' $COST)
89
197
  BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')
90
198
 
91
- # Workstream tag (if active). CODER_WORKSTREAM and CODER_WORKSTREAM_COLOR
92
- # are set by coder-config when a workstream is activated.
93
199
  WS_TAG=""
94
200
  if [ -n "$CODER_WORKSTREAM" ]; then
95
201
  case "$CODER_WORKSTREAM_COLOR" in
@@ -188,8 +294,15 @@ const PRESETS = [
188
294
  {
189
295
  id: 'full',
190
296
  name: 'Full',
191
- description: 'Everything with colors: model, context bar, token counts, lines, branch, duration, cost, workstream',
192
- preview: '* opus-4-6 | ●●●●○○○○○○ 74.4K/200.0K | 37% left | +146 -13 | main | 5h 2m | $0.142 | ◆ coder-config',
297
+ description: 'Model, context bar, 5H/7D rate-limit bars with reset timers, lines, branch, cost, workstream',
298
+ preview: 'Opus 4.7 ●●●○○○○○○○ 74K/1M (37%) | 5H: ▰▰▰▰▰▰▰▰ 42% 2H 29M | 7D: ▰▰▰▰▰▰▰▰ 15% | +146 -13 | main | $0.142',
299
+ category: 'Git',
300
+ },
301
+ {
302
+ id: 'classic',
303
+ name: 'Classic',
304
+ description: 'Original full layout: model, context bar, token counts, lines, branch, duration, cost, workstream',
305
+ preview: '* Opus 4.7 | ●●●●○○○○○○ 74.4K/200.0K | 63% left | +146 -13 | main | 5h 2m | $0.142 | ◆ coder-config',
193
306
  category: 'Git',
194
307
  },
195
308
  {
@@ -251,11 +364,114 @@ function ensureScriptDir() {
251
364
  function writeScript(presetId, content) {
252
365
  ensureScriptDir();
253
366
  const p = scriptPath(presetId);
367
+ // Backup any existing script before overwriting (timestamped, never collides).
368
+ if (fs.existsSync(p)) {
369
+ const existing = fs.readFileSync(p, 'utf8');
370
+ if (existing !== content) {
371
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
372
+ fs.writeFileSync(`${p}.${ts}.bak`, existing, 'utf8');
373
+ }
374
+ }
254
375
  fs.writeFileSync(p, content, 'utf8');
255
376
  fs.chmodSync(p, 0o755);
377
+ // Track this exact content as a "known shipped version" so future
378
+ // auto-migrations can tell user edits apart from old templates.
379
+ recordShippedHash(presetId, content);
256
380
  return p;
257
381
  }
258
382
 
383
+ // ---------------------------------------------------------------------------
384
+ // Auto-migration: refresh installed preset scripts when the bundled template
385
+ // changes between versions, but only if the on-disk script matches one of the
386
+ // previously-shipped versions (i.e. the user hasn't customized it).
387
+ // ---------------------------------------------------------------------------
388
+
389
+ const SHIPPED_HASHES_FILE = path.join(STATUSLINES_DIR, '.shipped-hashes.json');
390
+
391
+ // Historical content hashes for each preset, seeded so users who installed a
392
+ // preset before the hash-tracking system was added still get auto-migrated.
393
+ // Append (never edit) when a preset's bundled template changes.
394
+ const LEGACY_HASHES = {
395
+ full: [
396
+ '0ac48c7d993dd0c6e49b1f537934e418a5e65b0838afc587bafde31cc45877da', // pre-2026-04 model+duration variant
397
+ ],
398
+ minimal: ['d818c74b391732f5aae948fbc9c154b6cba8e56952a1727d3dfa78602823c601'],
399
+ 'context-bar': ['643bf0f54baa70ec72d85f8800dee24712c31eae8b6b0030e597d0c1e4ed0ae0'],
400
+ 'git-context': ['b186ea21e4d14fb905f8682e5dfe7b3b2050a93b7801226002052aaa7bafd451'],
401
+ 'cost-tracker': ['83364c860744a6cf7fb5f07b061ae13a1919fc7e72f35597dd383e2de3371895'],
402
+ multiline: ['49939b21f4691171bcfbe5c0ca252bcf9ffb772f335a14712bb9beccf67920b9'],
403
+ };
404
+
405
+ function sha256(s) {
406
+ return require('crypto').createHash('sha256').update(s).digest('hex');
407
+ }
408
+
409
+ function loadShippedHashes() {
410
+ if (!fs.existsSync(SHIPPED_HASHES_FILE)) return {};
411
+ try { return JSON.parse(fs.readFileSync(SHIPPED_HASHES_FILE, 'utf8')); } catch { return {}; }
412
+ }
413
+
414
+ function saveShippedHashes(data) {
415
+ ensureScriptDir();
416
+ fs.writeFileSync(SHIPPED_HASHES_FILE, JSON.stringify(data, null, 2), 'utf8');
417
+ }
418
+
419
+ function recordShippedHash(presetId, content) {
420
+ const data = loadShippedHashes();
421
+ const list = data[presetId] || [];
422
+ const h = sha256(content);
423
+ if (!list.includes(h)) {
424
+ list.push(h);
425
+ data[presetId] = list;
426
+ try { saveShippedHashes(data); } catch {}
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Run on server startup. For each preset whose script is installed AND whose
432
+ * settings.json points at it, refresh the file from the latest bundled template
433
+ * if the on-disk hash matches a previously-shipped version. User-edited scripts
434
+ * (unknown hash) are left alone.
435
+ */
436
+ function autoMigratePresets() {
437
+ try {
438
+ const settings = readSettings();
439
+ const cmd = commandPathInSettings(settings);
440
+ if (!cmd) return;
441
+ const presetId = matchPresetFromCommand(cmd);
442
+ if (presetId === 'disabled' || presetId === 'custom') return;
443
+
444
+ const latest = SCRIPTS[presetId];
445
+ if (!latest) return;
446
+ const p = scriptPath(presetId);
447
+ if (!fs.existsSync(p)) return;
448
+
449
+ const onDisk = fs.readFileSync(p, 'utf8');
450
+ if (onDisk === latest) {
451
+ // Already up-to-date — make sure its hash is recorded.
452
+ recordShippedHash(presetId, latest);
453
+ return;
454
+ }
455
+
456
+ const shipped = [
457
+ ...(loadShippedHashes()[presetId] || []),
458
+ ...(LEGACY_HASHES[presetId] || []),
459
+ ];
460
+ const onDiskHash = sha256(onDisk);
461
+ if (!shipped.includes(onDiskHash)) {
462
+ // User-edited — record latest as known but don't overwrite.
463
+ recordShippedHash(presetId, latest);
464
+ return;
465
+ }
466
+
467
+ // Safe to refresh: writeScript backs up + records new hash.
468
+ writeScript(presetId, latest);
469
+ console.log(`[statuslines] auto-migrated preset "${presetId}" to latest template`);
470
+ } catch (e) {
471
+ // Never let migration crash startup.
472
+ }
473
+ }
474
+
259
475
  function commandPathInSettings(settings) {
260
476
  return settings?.statusLine?.command || null;
261
477
  }
@@ -357,4 +573,5 @@ module.exports = {
357
573
  getCurrentStatusline,
358
574
  getPresetScript,
359
575
  setStatusline,
576
+ autoMigratePresets,
360
577
  };
package/ui/server.cjs CHANGED
@@ -303,6 +303,13 @@ class ConfigUIServer {
303
303
  } catch (e) {
304
304
  // Ignore migration errors
305
305
  }
306
+ try {
307
+ // Refresh installed statusline preset scripts when bundled templates
308
+ // change between versions (skipped if user has customized the script).
309
+ routes.statuslines.autoMigratePresets();
310
+ } catch (e) {
311
+ // Ignore migration errors
312
+ }
306
313
  }
307
314
 
308
315
  async handleRequest(req, res) {