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 +1 -0
- package/CLAUDE.md +6 -3
- package/README.md +4 -7
- package/dist/cli.js +80 -26
- package/docs/index.html +10 -21
- package/hooks/statusline.sh +35 -19
- package/package.json +3 -2
- package/src/lib/demo.js +37 -19
- package/src/lib/install.js +54 -5
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
262
|
-
───────────────
|
|
258
|
+
██ ⛳ refactor auth middleware $0.82/4.00 ▓▓░░░░░░░░░ 21% LEGENDARY 🧙 Opus F3/5
|
|
259
|
+
██ 🧠 ▓▓▓▓▓░░░░░ 52% 🪶
|
|
263
260
|
```
|
|
264
261
|
|
|
265
|
-
Context
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
880
|
-
|
|
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
|
-
|
|
883
|
-
|
|
887
|
+
accent = Y;
|
|
888
|
+
costStr = `${tierEmoji} $${cost.toFixed(2)}`;
|
|
889
|
+
ratingStr = "";
|
|
884
890
|
}
|
|
885
|
-
let
|
|
886
|
-
if (ctxPct != null) {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
|
1786
|
-
|
|
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
|
|
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="
|
|
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">
|
|
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">
|
|
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">
|
|
1161
|
-
<span class="
|
|
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">
|
|
1165
|
-
<span class="
|
|
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">
|
|
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">
|
|
1173
|
-
<span class="
|
|
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.
|
package/hooks/statusline.sh
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
71
|
+
accent = Y
|
|
72
|
+
cost_str = f"{tier_emoji} ${cost:.2f}"
|
|
73
|
+
rating_str = ''
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
# Context bar (line 2, always shown)
|
|
76
|
+
ctx_line = None
|
|
69
77
|
if ctx_pct is not None:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
69
|
+
accent = Y;
|
|
70
|
+
costStr = `${tierEmoji} $${cost.toFixed(2)}`;
|
|
71
|
+
ratingStr = '';
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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 `${
|
|
100
|
+
return ctxLine ? `${line1}\n${ctxLine}` : line1;
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
const SCENARIOS = [
|
package/src/lib/install.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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));
|