tokengolf 0.5.0 → 0.5.2

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/CHANGELOG.md CHANGED
@@ -7,6 +7,7 @@ TokenGolf patch notes — what changed, what it measures, and why the mechanic e
7
7
  ## [Unreleased]
8
8
 
9
9
  ### Changed
10
+ - **Design D HUD** — StatusLine HUD redesigned with `██` accent bar, inline `▓░` progress bars for budget and context, no separator lines. 1 line when context <50%, 2 lines when context visible. Accent bar turns red when budget >75%. Matches Design D across all UI surfaces.
10
11
  - **Design D block accent UI** — All bordered boxes replaced with left-only `██` block accent bars. Eliminates persistent right-border misalignment caused by emoji/unicode width differences across terminals. Color-coded: yellow for won, red for died, gray for neutral.
11
12
  - ScoreCard, StatsView, ActiveRun, StartRun components all use custom Ink `borderStyle` with `left: '██'`, no right/top/bottom borders
12
13
  - session-end.js ANSI scorecard uses `██` prefix per line with `─` horizontal separators
package/CLAUDE.md CHANGED
@@ -347,10 +347,13 @@ Nine hooks in `hooks/` directory, installed via `tokengolf install`. Most comple
347
347
  ### `StatusLine` (`statusline.sh`)
348
348
  - Bash script; uses `TG_SESSION_JSON=... python3 - "$STATE_FILE" <<'PYEOF'` pattern to avoid heredoc/stdin conflict
349
349
  - Receives live session JSON (cost, context %, model) via stdin
350
- - Shows: quest/mode | tier emoji + cost [/budget pct%] | [efficiency rating] | [🪶/🎒/📦 ctx%] | model label | [floor]
350
+ - **Design D accent bar**: `██` prefix on each line, color-coded (yellow normal, red when budget >75%)
351
+ - Line 1: `██ ⛳ quest $cost/budget ▓▓▓░░░ pct% RATING model F1/5`
352
+ - Line 2 (always shown when context data available): `██ 🧠 ▓▓▓▓░░░ ctx% 🪶/🎒/📦`
353
+ - Budget progress bar: `▓` filled, `░` empty, 11 chars wide. Red when >75%, yellow otherwise
354
+ - Context progress bar: `▓░` 10 chars wide. Green (50–74%), yellow (75–89%), red (90%+); hidden below 50%
351
355
  - Model label: `⚔️ Sonnet`, `⚔️ Sonnet·High`, `🏹 Haiku`, `🧙 Opus·Max`, etc. Effort appended only when explicitly set in settings.json (medium omitted — it's the default)
352
- - Context load: 🪶 green (50–74%), 🎒 yellow (75–89%), 📦 red (90%+); hidden below 50%
353
- - Separator lines (`───────────────`) above and below HUD row
356
+ - Always 2 lines when context data is available from Claude Code
354
357
  - statusLine config must be an object: `{type:"command", command:"...statusline.sh", padding:1}`
355
358
 
356
359
  ### Hook installation
package/README.md CHANGED
@@ -253,16 +253,13 @@ Budget presets are model-calibrated — Haiku Diamond is $0.15, Opus Diamond is
253
253
  After `tokengolf install`, a status line appears in every Claude Code session showing quest, cost, efficiency, context load, and model class.
254
254
 
255
255
  ```
256
- ───────────────
257
- ⛳ add pagination to /users | 🥈 $0.54/$1.50 36% | EFFICIENT | ⚔️ Sonnet | Floor 2/5
258
- ───────────────
256
+ ██ ⛳ add pagination to /users $0.54/1.50 ▓▓▓▓░░░░░░░ 36% EFFICIENT ⚔️ Sonnet F2/5
259
257
 
260
- ───────────────
261
- refactor auth middleware | 🥈 $0.82/$4.00 21% | LEGENDARY | 🪶 52% | 🧙 Opus | Floor 3/5
262
- ───────────────
258
+ ██ ⛳ refactor auth middleware $0.82/4.00 ▓▓░░░░░░░░░ 21% LEGENDARY 🧙 Opus F3/5
259
+ ██ 🧠 ▓▓▓▓▓░░░░░ 52% 🪶
263
260
  ```
264
261
 
265
- Context indicators: **🪶** green (50–74%) · **🎒** yellow (75–89%) · **📦** red (90%+). **💤** replaces ⛳ if the previous session fainted.
262
+ Budget bar turns red above 75%. Context bar (line 2) appears at 50%+: **🪶** green · **🎒** yellow · **📦** red. Accent `██` turns red in danger. **💤** replaces ⛳ when fainted.
266
263
 
267
264
  ---
268
265
 
package/dist/cli.js CHANGED
@@ -847,7 +847,8 @@ function hudLine({ quest, model, cost, budget, ctxPct, effort, fainted, floor })
847
847
  modelEmoji = "?";
848
848
  }
849
849
  const labelParts = [`${modelEmoji} ${modelName}`];
850
- if (effort) labelParts.push(effort.charAt(0).toUpperCase() + effort.slice(1));
850
+ if (effort && effort !== "medium")
851
+ labelParts.push(effort.charAt(0).toUpperCase() + effort.slice(1));
851
852
  const modelLabel = labelParts.join("\xB7");
852
853
  let tierEmoji;
853
854
  if (cost < 0.1) tierEmoji = "\u{1F48E}";
@@ -855,8 +856,7 @@ function hudLine({ quest, model, cost, budget, ctxPct, effort, fainted, floor })
855
856
  else if (cost < 1) tierEmoji = "\u{1F948}";
856
857
  else if (cost < 3) tierEmoji = "\u{1F949}";
857
858
  else tierEmoji = "\u{1F4B8}";
858
- const sep = ` ${DIM}|${RESET} `;
859
- let costStr, ratingStr;
859
+ let costStr, ratingStr, accent;
860
860
  if (budget) {
861
861
  const pct = cost / budget * 100;
862
862
  let rating, rc;
@@ -876,28 +876,42 @@ function hudLine({ quest, model, cost, budget, ctxPct, effort, fainted, floor })
876
876
  rating = "BUSTED";
877
877
  rc = R;
878
878
  }
879
- costStr = `${tierEmoji} $${cost.toFixed(4)}/$${budget.toFixed(2)} ${pct.toFixed(0)}%`;
880
- ratingStr = `${rc}${rating}${RESET}`;
879
+ accent = pct > 75 ? R : Y;
880
+ const barW = 11;
881
+ const barFilled = Math.min(barW, Math.round(pct / 100 * barW));
882
+ const barEmpty = barW - barFilled;
883
+ const bar = `${accent}${"\u2593".repeat(barFilled)}${"\u2591".repeat(barEmpty)}${RESET}`;
884
+ costStr = `${DIM}$${RESET}${cost.toFixed(2)}${DIM}/${budget.toFixed(2)}${RESET} ${bar} ${pct.toFixed(0)}%`;
885
+ ratingStr = ` ${rc}${rating}${RESET}`;
881
886
  } else {
882
- costStr = `${tierEmoji} $${cost.toFixed(4)}`;
883
- ratingStr = null;
887
+ accent = Y;
888
+ costStr = `${tierEmoji} $${cost.toFixed(2)}`;
889
+ ratingStr = "";
884
890
  }
885
- let ctxStr = null;
886
- if (ctxPct != null) {
887
- if (ctxPct >= 90) ctxStr = `${R}\u{1F4E6} ${ctxPct}%${RESET}`;
888
- else if (ctxPct >= 75) ctxStr = `${Y}\u{1F392} ${ctxPct}%${RESET}`;
889
- else if (ctxPct >= 50) ctxStr = `${G}\u{1FAB6} ${ctxPct}%${RESET}`;
891
+ let ctxLine = null;
892
+ if (ctxPct != null && ctxPct >= 50) {
893
+ const ctxW = 10;
894
+ const ctxFilled = Math.min(ctxW, Math.round(ctxPct / 100 * ctxW));
895
+ const ctxEmpty = ctxW - ctxFilled;
896
+ let ctxColor, ctxIcon;
897
+ if (ctxPct >= 90) {
898
+ ctxColor = R;
899
+ ctxIcon = "\u{1F4E6}";
900
+ } else if (ctxPct >= 75) {
901
+ ctxColor = Y;
902
+ ctxIcon = "\u{1F392}";
903
+ } else {
904
+ ctxColor = G;
905
+ ctxIcon = "\u{1FAB6}";
906
+ }
907
+ const ctxBar = `${ctxColor}${"\u2593".repeat(ctxFilled)}${"\u2591".repeat(ctxEmpty)}${RESET}`;
908
+ ctxLine = ` ${accent}\u2588\u2588${RESET} \u{1F9E0} ${ctxBar} ${ctxPct}% ${ctxIcon}`;
890
909
  }
891
910
  const icon = fainted ? "\u{1F4A4}" : "\u26F3";
892
- const prefix = `${BOLD}${C}${icon}${RESET}`;
893
- const parts = [`${prefix} ${quest}`, costStr];
894
- if (ratingStr) parts.push(ratingStr);
895
- if (ctxStr) parts.push(ctxStr);
896
- parts.push(`${C}${modelLabel}${RESET}`);
897
- if (budget && floor) parts.push(`Floor ${floor}`);
898
- return `${DIM} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}
899
- ${parts.join(sep)}
900
- ${DIM} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}`;
911
+ let line1 = ` ${accent}\u2588\u2588${RESET} ${icon} ${quest} ${costStr}${ratingStr} ${modelLabel}`;
912
+ if (budget && floor) line1 += ` ${DIM}F${floor}${RESET}`;
913
+ return ctxLine ? `${line1}
914
+ ${ctxLine}` : line1;
901
915
  }
902
916
  function runDemo() {
903
917
  console.log("");
@@ -1782,8 +1796,50 @@ function installHooks() {
1782
1796
  }
1783
1797
  const existing = settings.statusLine;
1784
1798
  const existingCmd = typeof existing === "string" ? existing : existing?.command ?? null;
1785
- const alreadyOurs = existingCmd === STATUSLINE_PATH || existingCmd === WRAPPER_PATH;
1786
- if (!alreadyOurs && existingCmd) {
1799
+ const isTgStatusline = (cmd) => cmd && (cmd.includes("tokengolf/hooks/statusline") || cmd.includes("tokengolf\\hooks\\statusline"));
1800
+ const alreadyOurs = isTgStatusline(existingCmd);
1801
+ if (alreadyOurs) {
1802
+ let userStatusline = null;
1803
+ if (existingCmd.includes("statusline-wrapper")) {
1804
+ try {
1805
+ const wrapperContent = fs4.readFileSync(existingCmd, "utf8");
1806
+ const lines = wrapperContent.split("\n").filter((l) => l.includes('echo "$SESSION_JSON"'));
1807
+ for (const line of lines) {
1808
+ const match = line.match(/echo "\$SESSION_JSON" \| (.+?)( 2>|$)/);
1809
+ if (match && !isTgStatusline(match[1])) {
1810
+ userStatusline = match[1];
1811
+ break;
1812
+ }
1813
+ }
1814
+ } catch {
1815
+ }
1816
+ }
1817
+ if (userStatusline) {
1818
+ fs4.writeFileSync(
1819
+ WRAPPER_PATH,
1820
+ [
1821
+ "#!/usr/bin/env bash",
1822
+ "SESSION_JSON=$(cat)",
1823
+ `echo "$SESSION_JSON" | ${userStatusline} 2>/dev/null || true`,
1824
+ `echo "$SESSION_JSON" | bash ${STATUSLINE_PATH}`
1825
+ ].join("\n") + "\n"
1826
+ );
1827
+ fs4.chmodSync(WRAPPER_PATH, 493);
1828
+ settings.statusLine = {
1829
+ type: "command",
1830
+ command: WRAPPER_PATH,
1831
+ padding: existing?.padding ?? 1
1832
+ };
1833
+ console.log(" \u2713 statusLine \u2192 updated paths (kept your existing statusline)");
1834
+ } else {
1835
+ settings.statusLine = {
1836
+ type: "command",
1837
+ command: STATUSLINE_PATH,
1838
+ padding: existing?.padding ?? 1
1839
+ };
1840
+ console.log(" \u2713 statusLine \u2192 updated to current install path");
1841
+ }
1842
+ } else if (existingCmd) {
1787
1843
  fs4.writeFileSync(
1788
1844
  WRAPPER_PATH,
1789
1845
  [
@@ -1800,15 +1856,13 @@ function installHooks() {
1800
1856
  padding: 1
1801
1857
  };
1802
1858
  console.log(" \u2713 statusLine \u2192 wrapped your existing statusline + tokengolf HUD");
1803
- } else if (!alreadyOurs) {
1859
+ } else {
1804
1860
  settings.statusLine = {
1805
1861
  type: "command",
1806
1862
  command: STATUSLINE_PATH,
1807
1863
  padding: 1
1808
1864
  };
1809
1865
  console.log(" \u2713 statusLine \u2192 live HUD in every Claude session");
1810
- } else {
1811
- console.log(" \u2713 statusLine \u2192 already installed");
1812
1866
  }
1813
1867
  fs4.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
1814
1868
  console.log(" \u2713 SessionStart \u2192 injects run context into Claude");
package/docs/index.html CHANGED
@@ -1145,33 +1145,22 @@
1145
1145
  <div class="term" data-glow="cyan">
1146
1146
  <div class="term-header">tokengolf demo hud</div>
1147
1147
  <pre>
1148
- <span class="t-dim"> ───────────────</span>
1149
- <span class="t-b t-cyan">⛳</span> Flow <span class="t-dim">|</span> 💎 $0.0034 <span class="t-dim">|</span> <span class="t-cyan">⚔️ Sonnet</span>
1150
- <span class="t-dim"> ───────────────</span>
1148
+ <span class="border-yellow">██</span> ⛳ Flow 💎 $0.0034 ⚔️ Sonnet
1151
1149
 
1152
- <span class="t-dim"> ───────────────</span>
1153
- <span class="t-b t-cyan">⛳</span> add pagination to /users <span class="t-dim">|</span> 🥈 $0.5400/$1.50 36% <span class="t-dim">|</span> <span class="t-cyan">EFFICIENT</span> <span class="t-dim">|</span> <span class="t-cyan">⚔️ Sonnet</span> <span class="t-dim">|</span> Floor 2/5
1154
- <span class="t-dim"> ───────────────</span>
1150
+ <span class="border-yellow">██</span> ⛳ add pagination to /users <span class="t-dim">$</span>0.54<span class="t-dim">/1.50</span> <span class="t-yellow">▓▓▓▓░░░░░░░</span> 36% <span class="t-cyan">EFFICIENT</span> ⚔️ Sonnet <span class="t-dim">F2/5</span>
1155
1151
 
1156
- <span class="t-dim"> ───────────────</span>
1157
- <span class="t-b t-cyan">⛳</span> implement SSO with SAML <span class="t-dim">|</span> 🥈 $0.4100/$2.00 21% <span class="t-dim">|</span> <span class="t-magenta">LEGENDARY</span> <span class="t-dim">|</span> <span class="t-cyan">⚔️ Sonnet·High</span> <span class="t-dim">|</span> Floor 1/5
1158
- <span class="t-dim"> ───────────────</span>
1152
+ <span class="border-yellow">██</span> ⛳ implement SSO with SAML <span class="t-dim">$</span>0.41<span class="t-dim">/2.00</span> <span class="t-yellow">▓▓░░░░░░░░░</span> 21% <span class="t-magenta">LEGENDARY</span> ⚔️ Sonnet·High <span class="t-dim">F1/5</span>
1159
1153
 
1160
- <span class="t-dim"> ───────────────</span>
1161
- <span class="t-b t-cyan">⛳</span> refactor auth middleware <span class="t-dim">|</span> 🥈 $0.8200/$4.00 21% <span class="t-dim">|</span> <span class="t-magenta">LEGENDARY</span> <span class="t-dim">|</span> <span class="t-green">🪶 52%</span> <span class="t-dim">|</span> <span class="t-cyan">🧙 Opus</span> <span class="t-dim">|</span> Floor 3/5
1162
- <span class="t-dim"> ───────────────</span>
1154
+ <span class="border-yellow">██</span> ⛳ refactor auth middleware <span class="t-dim">$</span>0.82<span class="t-dim">/4.00</span> <span class="t-yellow">▓▓░░░░░░░░░</span> 21% <span class="t-magenta">LEGENDARY</span> 🧙 Opus <span class="t-dim">F3/5</span>
1155
+ <span class="border-yellow">██</span> 🧠 <span class="t-green">▓▓▓▓▓░░░░░</span> 52% 🪶
1163
1156
 
1164
- <span class="t-dim"> ───────────────</span>
1165
- <span class="t-b t-cyan">⛳</span> fix N+1 query in dashboard <span class="t-dim">|</span> 🥈 $0.4600/$0.50 92% <span class="t-dim">|</span> <span class="t-yellow">CLOSE CALL</span> <span class="t-dim">|</span> <span class="t-yellow">🎒 78%</span> <span class="t-dim">|</span> <span class="t-cyan">🏹 Haiku</span> <span class="t-dim">|</span> Floor 4/5
1166
- <span class="t-dim"> ───────────────</span>
1157
+ <span class="border-red">██</span> ⛳ fix N+1 query in dashboard <span class="t-dim">$</span>0.46<span class="t-dim">/0.50</span> <span class="t-red">▓▓▓▓▓▓▓▓▓▓░</span> 92% <span class="t-yellow">CLOSE CALL</span> 🏹 Haiku <span class="t-dim">F4/5</span>
1158
+ <span class="border-red">██</span> 🧠 <span class="t-yellow">▓▓▓▓▓▓▓▓░░</span> 78% 🎒
1167
1159
 
1168
- <span class="t-dim"> ───────────────</span>
1169
- <span class="t-b t-cyan">⛳</span> migrate postgres schema <span class="t-dim">|</span> 🥉 $2.4100/$2.00 121% <span class="t-dim">|</span> <span class="t-red">BUSTED</span> <span class="t-dim">|</span> <span class="t-cyan">⚔️ Sonnet</span> <span class="t-dim">|</span> Floor 2/5
1170
- <span class="t-dim"> ───────────────</span>
1160
+ <span class="border-red">██</span> ⛳ migrate postgres schema <span class="t-dim">$</span>2.41<span class="t-dim">/2.00</span> <span class="t-red">▓▓▓▓▓▓▓▓▓▓▓</span> 121% <span class="t-red">BUSTED</span> ⚔️ Sonnet <span class="t-dim">F2/5</span>
1171
1161
 
1172
- <span class="t-dim"> ───────────────</span>
1173
- <span class="t-b t-cyan">💤</span> write test suite for payments <span class="t-dim">|</span> 🥉 $1.2200/$3.00 41% <span class="t-dim">|</span> <span class="t-cyan">EFFICIENT</span> <span class="t-dim">|</span> <span class="t-green">🪶 67%</span> <span class="t-dim">|</span> <span class="t-cyan">⚔️ Sonnet</span> <span class="t-dim">|</span> Floor 2/5
1174
- <span class="t-dim"> ───────────────</span></pre>
1162
+ <span class="border-yellow">██</span> 💤 write test suite for payments <span class="t-dim">$</span>1.22<span class="t-dim">/3.00</span> <span class="t-yellow">▓▓▓▓▓░░░░░░</span> 41% <span class="t-cyan">EFFICIENT</span> ⚔️ Sonnet <span class="t-dim">F2/5</span>
1163
+ <span class="border-yellow">██</span> 🧠 <span class="t-green">▓▓▓▓▓▓▓░░░</span> 67% 🪶</pre>
1175
1164
  </div>
1176
1165
  <div class="demo-label">
1177
1166
  <strong>Live HUD</strong> appears in every Claude Code session. Shows quest, cost, efficiency rating, context load, and model class at a glance.
@@ -22,7 +22,7 @@ except: pass
22
22
  ctx_pct = (session.get('context_window') or {}).get('used_percentage') or None
23
23
  quest = (run.get('quest') or 'Flow')[:32]
24
24
  budget = run.get('budget')
25
- floor = f"{run.get('floor',1)}/{run.get('totalFloors',5)}"
25
+ floor = f"F{run.get('floor',1)}/{run.get('totalFloors',5)}"
26
26
  sm = session.get('model') or {}; m = (sm.get('id','') or run.get('model','') if isinstance(sm,dict) else sm or run.get('model','')).lower()
27
27
  # opusplan must be checked before opus (opusplan contains 'opus' as substring)
28
28
  if 'opusplan' in m: model, model_emoji = 'Paladin', '⚜️'
@@ -38,11 +38,11 @@ fast = run.get('fastMode', False)
38
38
  fainted = run.get('fainted', False)
39
39
 
40
40
  label_parts = [f'{model_emoji} {model}']
41
- if effort: label_parts.append(effort.capitalize())
41
+ if effort and effort != 'medium': label_parts.append(effort.capitalize())
42
42
  if fast: label_parts.append('⚡Fast')
43
43
  model_label = '·'.join(label_parts)
44
44
 
45
- R, B, G, Y, M, C, DIM, RESET = '\033[31m','\033[34m','\033[32m','\033[33m','\033[35m','\033[36m','\033[2m','\033[0m'
45
+ R, G, Y, M, C, DIM, RESET = '\033[31m','\033[32m','\033[33m','\033[35m','\033[36m','\033[2m','\033[0m'
46
46
  BOLD = '\033[1m'
47
47
 
48
48
  if cost < 0.10: tier_emoji = '💎'
@@ -51,6 +51,7 @@ elif cost < 1.00: tier_emoji = '🥈'
51
51
  elif cost < 3.00: tier_emoji = '🥉'
52
52
  else: tier_emoji = '💸'
53
53
 
54
+ # Accent bar color: red in danger, yellow otherwise
54
55
  if budget:
55
56
  pct = cost / budget * 100
56
57
  if pct <= 25: rating, rc = 'LEGENDARY', M
@@ -58,24 +59,39 @@ if budget:
58
59
  elif pct <= 75: rating, rc = 'SOLID', G
59
60
  elif pct <= 100: rating, rc = 'CLOSE CALL', Y
60
61
  else: rating, rc = 'BUSTED', R
61
- cost_str = f"{tier_emoji} ${cost:.4f}/${budget:.2f} {pct:.0f}%"
62
- rating_str = f"{rc}{rating}{RESET}"
62
+ accent = R if pct > 75 else Y
63
+ # Budget progress bar
64
+ bar_w = 11
65
+ bar_filled = min(bar_w, int(pct / 100 * bar_w + 0.5))
66
+ bar_empty = bar_w - bar_filled
67
+ bar = f"{accent}{'▓' * bar_filled}{'░' * bar_empty}{RESET}"
68
+ cost_str = f"{DIM}${RESET}{cost:.2f}{DIM}/{budget:.2f}{RESET} {bar} {pct:.0f}%"
69
+ rating_str = f" {rc}{rating}{RESET}"
63
70
  else:
64
- cost_str = f"{tier_emoji} ${cost:.4f}"
65
- rating_str = None
71
+ accent = Y
72
+ cost_str = f"{tier_emoji} ${cost:.2f}"
73
+ rating_str = ''
66
74
 
67
- sep = f" {DIM}|{RESET} "
68
- ctx_str = None
75
+ # Context bar (line 2, always shown)
76
+ ctx_line = None
69
77
  if ctx_pct is not None:
70
- if ctx_pct >= 90: ctx_str = f"{R}📦 {ctx_pct:.0f}%{RESET}"
71
- elif ctx_pct >= 75: ctx_str = f"{Y}🎒 {ctx_pct:.0f}%{RESET}"
72
- elif ctx_pct >= 50: ctx_str = f"{G}🪶 {ctx_pct:.0f}%{RESET}"
78
+ ctx_w = 10
79
+ ctx_filled = min(ctx_w, int(ctx_pct / 100 * ctx_w + 0.5))
80
+ ctx_empty = ctx_w - ctx_filled
81
+ if ctx_pct >= 90: ctx_color, ctx_icon = R, '📦'
82
+ elif ctx_pct >= 75: ctx_color, ctx_icon = Y, '🎒'
83
+ else: ctx_color, ctx_icon = G, '🪶'
84
+ ctx_bar = f"{ctx_color}{'▓' * ctx_filled}{'░' * ctx_empty}{RESET}"
85
+ ctx_line = f" {accent}██{RESET} 🧠 {ctx_bar} {ctx_pct:.0f}% {ctx_icon}"
73
86
 
74
- prefix = f"{BOLD}{C}{'💤' if fainted else '⛳'}{RESET}"
75
- parts = [f"{prefix} {quest}", cost_str]
76
- if rating_str: parts.append(rating_str)
77
- if ctx_str: parts.append(ctx_str)
78
- parts.append(f"{C}{model_label}{RESET}")
79
- if budget: parts.append(f"Floor {floor}")
80
- print('\n' + f'{DIM} ───────────────{RESET}' + '\n' + sep.join(parts) + '\n' + f'{DIM} ───────────────{RESET}')
87
+ # Line 1: accent bar + quest + cost bar + rating + model + floor
88
+ icon = '💤' if fainted else '⛳'
89
+ parts = [f" {accent}██{RESET} {icon} {quest} {cost_str}{rating_str} {model_label}"]
90
+ if budget: parts.append(f" {DIM}{floor}{RESET}")
91
+ line1 = ''.join(parts)
92
+
93
+ # Output (leading blank line separates from any existing statusline above)
94
+ print()
95
+ print(line1)
96
+ if ctx_line: print(ctx_line)
81
97
  PYEOF
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokengolf",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Gamify your Claude Code sessions. Flow mode tracks you. Roguelike mode trains you.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,8 @@
17
17
  "format": "prettier --write src/ hooks/",
18
18
  "format:check": "prettier --check src/ hooks/",
19
19
  "version": "git add CHANGELOG.md",
20
- "postversion": "git push && git push --tags"
20
+ "postversion": "git push && git push --tags",
21
+ "postpublish": "./scripts/update-homebrew.sh"
21
22
  },
22
23
  "dependencies": {
23
24
  "@inkjs/ui": "^2.0.0",
package/src/lib/demo.js CHANGED
@@ -25,7 +25,8 @@ function hudLine({ quest, model, cost, budget, ctxPct, effort, fainted, floor })
25
25
  }
26
26
 
27
27
  const labelParts = [`${modelEmoji} ${modelName}`];
28
- if (effort) labelParts.push(effort.charAt(0).toUpperCase() + effort.slice(1));
28
+ if (effort && effort !== 'medium')
29
+ labelParts.push(effort.charAt(0).toUpperCase() + effort.slice(1));
29
30
  const modelLabel = labelParts.join('·');
30
31
 
31
32
  let tierEmoji;
@@ -35,8 +36,7 @@ function hudLine({ quest, model, cost, budget, ctxPct, effort, fainted, floor })
35
36
  else if (cost < 3.0) tierEmoji = '🥉';
36
37
  else tierEmoji = '💸';
37
38
 
38
- const sep = ` ${DIM}|${RESET} `;
39
- let costStr, ratingStr;
39
+ let costStr, ratingStr, accent;
40
40
 
41
41
  if (budget) {
42
42
  const pct = (cost / budget) * 100;
@@ -57,29 +57,47 @@ function hudLine({ quest, model, cost, budget, ctxPct, effort, fainted, floor })
57
57
  rating = 'BUSTED';
58
58
  rc = R;
59
59
  }
60
- costStr = `${tierEmoji} $${cost.toFixed(4)}/$${budget.toFixed(2)} ${pct.toFixed(0)}%`;
61
- ratingStr = `${rc}${rating}${RESET}`;
60
+ accent = pct > 75 ? R : Y;
61
+ // Budget progress bar
62
+ const barW = 11;
63
+ const barFilled = Math.min(barW, Math.round((pct / 100) * barW));
64
+ const barEmpty = barW - barFilled;
65
+ const bar = `${accent}${'▓'.repeat(barFilled)}${'░'.repeat(barEmpty)}${RESET}`;
66
+ costStr = `${DIM}$${RESET}${cost.toFixed(2)}${DIM}/${budget.toFixed(2)}${RESET} ${bar} ${pct.toFixed(0)}%`;
67
+ ratingStr = ` ${rc}${rating}${RESET}`;
62
68
  } else {
63
- costStr = `${tierEmoji} $${cost.toFixed(4)}`;
64
- ratingStr = null;
69
+ accent = Y;
70
+ costStr = `${tierEmoji} $${cost.toFixed(2)}`;
71
+ ratingStr = '';
65
72
  }
66
73
 
67
- let ctxStr = null;
68
- if (ctxPct != null) {
69
- if (ctxPct >= 90) ctxStr = `${R}📦 ${ctxPct}%${RESET}`;
70
- else if (ctxPct >= 75) ctxStr = `${Y}🎒 ${ctxPct}%${RESET}`;
71
- else if (ctxPct >= 50) ctxStr = `${G}🪶 ${ctxPct}%${RESET}`;
74
+ // Context bar (line 2, only shown when >= 50%)
75
+ let ctxLine = null;
76
+ if (ctxPct != null && ctxPct >= 50) {
77
+ const ctxW = 10;
78
+ const ctxFilled = Math.min(ctxW, Math.round((ctxPct / 100) * ctxW));
79
+ const ctxEmpty = ctxW - ctxFilled;
80
+ let ctxColor, ctxIcon;
81
+ if (ctxPct >= 90) {
82
+ ctxColor = R;
83
+ ctxIcon = '📦';
84
+ } else if (ctxPct >= 75) {
85
+ ctxColor = Y;
86
+ ctxIcon = '🎒';
87
+ } else {
88
+ ctxColor = G;
89
+ ctxIcon = '🪶';
90
+ }
91
+ const ctxBar = `${ctxColor}${'▓'.repeat(ctxFilled)}${'░'.repeat(ctxEmpty)}${RESET}`;
92
+ ctxLine = ` ${accent}██${RESET} 🧠 ${ctxBar} ${ctxPct}% ${ctxIcon}`;
72
93
  }
73
94
 
95
+ // Line 1: accent bar + quest + cost bar + rating + model + floor
74
96
  const icon = fainted ? '💤' : '⛳';
75
- const prefix = `${BOLD}${C}${icon}${RESET}`;
76
- const parts = [`${prefix} ${quest}`, costStr];
77
- if (ratingStr) parts.push(ratingStr);
78
- if (ctxStr) parts.push(ctxStr);
79
- parts.push(`${C}${modelLabel}${RESET}`);
80
- if (budget && floor) parts.push(`Floor ${floor}`);
97
+ let line1 = ` ${accent}██${RESET} ${icon} ${quest} ${costStr}${ratingStr} ${modelLabel}`;
98
+ if (budget && floor) line1 += ` ${DIM}F${floor}${RESET}`;
81
99
 
82
- return `${DIM} ───────────────${RESET}\n${parts.join(sep)}\n${DIM} ───────────────${RESET}`;
100
+ return ctxLine ? `${line1}\n${ctxLine}` : line1;
83
101
  }
84
102
 
85
103
  const SCENARIOS = [
@@ -149,9 +149,59 @@ export function installHooks() {
149
149
 
150
150
  const existing = settings.statusLine;
151
151
  const existingCmd = typeof existing === 'string' ? existing : (existing?.command ?? null);
152
- const alreadyOurs = existingCmd === STATUSLINE_PATH || existingCmd === WRAPPER_PATH;
152
+ // Detect any tokengolf statusline (from any install path — npm, homebrew, project dir)
153
+ const isTgStatusline = (cmd) =>
154
+ cmd &&
155
+ (cmd.includes('tokengolf/hooks/statusline') || cmd.includes('tokengolf\\hooks\\statusline'));
156
+ const alreadyOurs = isTgStatusline(existingCmd);
157
+
158
+ if (alreadyOurs) {
159
+ // Check if existing wrapper has a non-TG statusline we should preserve
160
+ let userStatusline = null;
161
+ if (existingCmd.includes('statusline-wrapper')) {
162
+ try {
163
+ const wrapperContent = fs.readFileSync(existingCmd, 'utf8');
164
+ const lines = wrapperContent.split('\n').filter((l) => l.includes('echo "$SESSION_JSON"'));
165
+ // Find the line that calls a non-tokengolf statusline
166
+ for (const line of lines) {
167
+ const match = line.match(/echo "\$SESSION_JSON" \| (.+?)( 2>|$)/);
168
+ if (match && !isTgStatusline(match[1])) {
169
+ userStatusline = match[1];
170
+ break;
171
+ }
172
+ }
173
+ } catch {}
174
+ }
153
175
 
154
- if (!alreadyOurs && existingCmd) {
176
+ if (userStatusline) {
177
+ // Re-wrap: preserve user's statusline + update tokengolf path
178
+ fs.writeFileSync(
179
+ WRAPPER_PATH,
180
+ [
181
+ '#!/usr/bin/env bash',
182
+ 'SESSION_JSON=$(cat)',
183
+ `echo "$SESSION_JSON" | ${userStatusline} 2>/dev/null || true`,
184
+ `echo "$SESSION_JSON" | bash ${STATUSLINE_PATH}`,
185
+ ].join('\n') + '\n'
186
+ );
187
+ fs.chmodSync(WRAPPER_PATH, 0o755);
188
+ settings.statusLine = {
189
+ type: 'command',
190
+ command: WRAPPER_PATH,
191
+ padding: existing?.padding ?? 1,
192
+ };
193
+ console.log(' ✓ statusLine → updated paths (kept your existing statusline)');
194
+ } else {
195
+ // Direct install — no user statusline to preserve
196
+ settings.statusLine = {
197
+ type: 'command',
198
+ command: STATUSLINE_PATH,
199
+ padding: existing?.padding ?? 1,
200
+ };
201
+ console.log(' ✓ statusLine → updated to current install path');
202
+ }
203
+ } else if (existingCmd) {
204
+ // User has a non-TG statusline — wrap it
155
205
  fs.writeFileSync(
156
206
  WRAPPER_PATH,
157
207
  [
@@ -168,15 +218,14 @@ export function installHooks() {
168
218
  padding: 1,
169
219
  };
170
220
  console.log(' ✓ statusLine → wrapped your existing statusline + tokengolf HUD');
171
- } else if (!alreadyOurs) {
221
+ } else {
222
+ // No existing statusline — install directly
172
223
  settings.statusLine = {
173
224
  type: 'command',
174
225
  command: STATUSLINE_PATH,
175
226
  padding: 1,
176
227
  };
177
228
  console.log(' ✓ statusLine → live HUD in every Claude session');
178
- } else {
179
- console.log(' ✓ statusLine → already installed');
180
229
  }
181
230
 
182
231
  fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));