tokengolf 0.3.0 → 0.4.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/dist/cli.js CHANGED
@@ -9,6 +9,193 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/lib/demo.js
13
+ var demo_exports = {};
14
+ __export(demo_exports, {
15
+ runDemo: () => runDemo
16
+ });
17
+ function hudLine({ quest, model, cost, budget, ctxPct, effort, fainted, floor }) {
18
+ const m = (model || "").toLowerCase();
19
+ let modelName, modelEmoji;
20
+ if (m.includes("haiku")) {
21
+ modelName = "Haiku";
22
+ modelEmoji = "\u{1F3F9}";
23
+ } else if (m.includes("sonnet")) {
24
+ modelName = "Sonnet";
25
+ modelEmoji = "\u2694\uFE0F";
26
+ } else if (m.includes("opus")) {
27
+ modelName = "Opus";
28
+ modelEmoji = "\u{1F9D9}";
29
+ } else {
30
+ modelName = "?";
31
+ modelEmoji = "?";
32
+ }
33
+ const labelParts = [`${modelEmoji} ${modelName}`];
34
+ if (effort) labelParts.push(effort.charAt(0).toUpperCase() + effort.slice(1));
35
+ const modelLabel = labelParts.join("\xB7");
36
+ let tierEmoji;
37
+ if (cost < 0.1) tierEmoji = "\u{1F48E}";
38
+ else if (cost < 0.3) tierEmoji = "\u{1F947}";
39
+ else if (cost < 1) tierEmoji = "\u{1F948}";
40
+ else if (cost < 3) tierEmoji = "\u{1F949}";
41
+ else tierEmoji = "\u{1F4B8}";
42
+ const sep = ` ${DIM}|${RESET} `;
43
+ let costStr, ratingStr;
44
+ if (budget) {
45
+ const pct = cost / budget * 100;
46
+ let rating, rc;
47
+ if (pct <= 25) {
48
+ rating = "LEGENDARY";
49
+ rc = M;
50
+ } else if (pct <= 50) {
51
+ rating = "EFFICIENT";
52
+ rc = C;
53
+ } else if (pct <= 75) {
54
+ rating = "SOLID";
55
+ rc = G;
56
+ } else if (pct <= 100) {
57
+ rating = "CLOSE CALL";
58
+ rc = Y;
59
+ } else {
60
+ rating = "BUSTED";
61
+ rc = R;
62
+ }
63
+ costStr = `${tierEmoji} $${cost.toFixed(4)}/$${budget.toFixed(2)} ${pct.toFixed(0)}%`;
64
+ ratingStr = `${rc}${rating}${RESET}`;
65
+ } else {
66
+ costStr = `${tierEmoji} $${cost.toFixed(4)}`;
67
+ ratingStr = null;
68
+ }
69
+ let ctxStr = null;
70
+ if (ctxPct != null) {
71
+ if (ctxPct >= 90) ctxStr = `${R}\u{1F4E6} ${ctxPct}%${RESET}`;
72
+ else if (ctxPct >= 75) ctxStr = `${Y}\u{1F392} ${ctxPct}%${RESET}`;
73
+ else if (ctxPct >= 50) ctxStr = `${G}\u{1FAB6} ${ctxPct}%${RESET}`;
74
+ }
75
+ const icon = fainted ? "\u{1F4A4}" : "\u26F3";
76
+ const prefix = `${BOLD}${C}${icon}${RESET}`;
77
+ const parts = [`${prefix} ${quest}`, costStr];
78
+ if (ratingStr) parts.push(ratingStr);
79
+ if (ctxStr) parts.push(ctxStr);
80
+ parts.push(`${C}${modelLabel}${RESET}`);
81
+ if (budget && floor) parts.push(`Floor ${floor}`);
82
+ return `${DIM} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}
83
+ ${parts.join(sep)}
84
+ ${DIM} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}`;
85
+ }
86
+ function runDemo() {
87
+ console.log("");
88
+ console.log(`${BOLD}${C}\u26F3 TokenGolf \u2014 HUD Demo${RESET}`);
89
+ console.log(`${DIM}Live statusline shown in every Claude Code session${RESET}`);
90
+ console.log("");
91
+ for (const { title, hud } of SCENARIOS) {
92
+ console.log(`${DIM}${title}${RESET}`);
93
+ console.log(hudLine(hud));
94
+ console.log("");
95
+ }
96
+ console.log(
97
+ `${DIM}Run ${RESET}tokengolf start${DIM} to begin a roguelike run, or just open Claude Code \u2014 flow mode tracks automatically.${RESET}`
98
+ );
99
+ console.log("");
100
+ }
101
+ var R, G, Y, M, C, DIM, BOLD, RESET, SCENARIOS;
102
+ var init_demo = __esm({
103
+ "src/lib/demo.js"() {
104
+ R = "\x1B[31m";
105
+ G = "\x1B[32m";
106
+ Y = "\x1B[33m";
107
+ M = "\x1B[35m";
108
+ C = "\x1B[36m";
109
+ DIM = "\x1B[2m";
110
+ BOLD = "\x1B[1m";
111
+ RESET = "\x1B[0m";
112
+ SCENARIOS = [
113
+ {
114
+ title: "Flow mode (passive \u2014 no quest, no budget)",
115
+ hud: { quest: "Flow", model: "claude-sonnet-4-6", cost: 34e-4 }
116
+ },
117
+ {
118
+ title: "Roguelike \xB7 Sonnet \xB7 EFFICIENT",
119
+ hud: {
120
+ quest: "add pagination to /users",
121
+ model: "claude-sonnet-4-6",
122
+ cost: 0.54,
123
+ budget: 1.5,
124
+ ctxPct: 34,
125
+ floor: "2/5"
126
+ }
127
+ },
128
+ {
129
+ title: "Roguelike \xB7 Sonnet\xB7High \xB7 LEGENDARY",
130
+ hud: {
131
+ quest: "implement SSO with SAML",
132
+ model: "claude-sonnet-4-6",
133
+ cost: 0.41,
134
+ budget: 2,
135
+ ctxPct: 29,
136
+ effort: "high",
137
+ floor: "1/5"
138
+ }
139
+ },
140
+ {
141
+ title: "Roguelike \xB7 Opus \xB7 LEGENDARY \xB7 \u{1FAB6} context",
142
+ hud: {
143
+ quest: "refactor auth middleware",
144
+ model: "claude-opus-4-6",
145
+ cost: 0.82,
146
+ budget: 4,
147
+ ctxPct: 52,
148
+ floor: "3/5"
149
+ }
150
+ },
151
+ {
152
+ title: "Roguelike \xB7 Haiku \xB7 CLOSE CALL \xB7 \u{1F392} context",
153
+ hud: {
154
+ quest: "fix N+1 query in dashboard",
155
+ model: "claude-haiku-4-5-20251001",
156
+ cost: 0.46,
157
+ budget: 0.5,
158
+ ctxPct: 78,
159
+ floor: "4/5"
160
+ }
161
+ },
162
+ {
163
+ title: "Roguelike \xB7 BUSTED \u2014 over budget",
164
+ hud: {
165
+ quest: "migrate postgres schema",
166
+ model: "claude-sonnet-4-6",
167
+ cost: 2.41,
168
+ budget: 2,
169
+ floor: "2/5"
170
+ }
171
+ },
172
+ {
173
+ title: "Roguelike \xB7 Opus \xB7 EFFICIENT \xB7 \u{1F4E6} overencumbered",
174
+ hud: {
175
+ quest: "refactor entire API layer",
176
+ model: "claude-opus-4-6",
177
+ cost: 3.1,
178
+ budget: 10,
179
+ ctxPct: 91,
180
+ floor: "3/5"
181
+ }
182
+ },
183
+ {
184
+ title: "Fainted \u{1F4A4} \u2014 usage limit hit, run resumes next session",
185
+ hud: {
186
+ quest: "write test suite for payments",
187
+ model: "claude-sonnet-4-6",
188
+ cost: 1.22,
189
+ budget: 3,
190
+ ctxPct: 67,
191
+ fainted: true,
192
+ floor: "2/5"
193
+ }
194
+ }
195
+ ];
196
+ }
197
+ });
198
+
12
199
  // src/lib/install.js
13
200
  var install_exports = {};
14
201
  __export(install_exports, {
@@ -36,7 +223,7 @@ function installHooks() {
36
223
  const existing2 = settings.hooks[event] || [];
37
224
  const filtered = existing2.filter(
38
225
  (h) => !h._tg && !h.hooks?.some(
39
- (e) => e.command?.includes("tokengolf") || e.command?.includes("session-start.js") || e.command?.includes("session-stop.js") || e.command?.includes("session-end.js") || e.command?.includes("pre-compact.js") || e.command?.includes("post-tool-use.js") || e.command?.includes("user-prompt-submit.js")
226
+ (e) => e.command?.includes("tokengolf") || e.command?.includes("session-start.js") || e.command?.includes("session-stop.js") || e.command?.includes("session-end.js") || e.command?.includes("pre-compact.js") || e.command?.includes("post-tool-use.js") || e.command?.includes("post-tool-use-failure.js") || e.command?.includes("subagent-start.js") || e.command?.includes("stop.js") || e.command?.includes("user-prompt-submit.js")
40
227
  )
41
228
  );
42
229
  settings.hooks[event] = [...filtered, { _tg: true, ...entry }];
@@ -93,6 +280,34 @@ function installHooks() {
93
280
  }
94
281
  ]
95
282
  });
283
+ upsertHook("PostToolUseFailure", {
284
+ matcher: "",
285
+ hooks: [
286
+ {
287
+ type: "command",
288
+ command: `node ${path4.join(HOOKS_DIR, "post-tool-use-failure.js")}`,
289
+ timeout: 5
290
+ }
291
+ ]
292
+ });
293
+ upsertHook("SubagentStart", {
294
+ hooks: [
295
+ {
296
+ type: "command",
297
+ command: `node ${path4.join(HOOKS_DIR, "subagent-start.js")}`,
298
+ timeout: 5
299
+ }
300
+ ]
301
+ });
302
+ upsertHook("Stop", {
303
+ hooks: [
304
+ {
305
+ type: "command",
306
+ command: `node ${path4.join(HOOKS_DIR, "stop.js")}`,
307
+ timeout: 5
308
+ }
309
+ ]
310
+ });
96
311
  try {
97
312
  fs4.chmodSync(STATUSLINE_PATH, 493);
98
313
  } catch {
@@ -116,9 +331,7 @@ function installHooks() {
116
331
  command: WRAPPER_PATH,
117
332
  padding: 1
118
333
  };
119
- console.log(
120
- " \u2713 statusLine \u2192 wrapped your existing statusline + tokengolf HUD"
121
- );
334
+ console.log(" \u2713 statusLine \u2192 wrapped your existing statusline + tokengolf HUD");
122
335
  } else if (!alreadyOurs) {
123
336
  settings.statusLine = {
124
337
  type: "command",
@@ -134,9 +347,10 @@ function installHooks() {
134
347
  console.log(" \u2713 PostToolUse \u2192 tracks tool calls + 80% budget warning");
135
348
  console.log(" \u2713 UserPromptSubmit \u2192 counts prompts + 50% nudge");
136
349
  console.log(" \u2713 SessionEnd \u2192 auto-displays scorecard on /exit");
137
- console.log(
138
- " \u2713 PreCompact \u2192 tracks compaction events for gear achievements"
139
- );
350
+ console.log(" \u2713 PreCompact \u2192 tracks compaction events for gear achievements");
351
+ console.log(" \u2713 PostToolUseFailure \u2192 tracks failed tool calls");
352
+ console.log(" \u2713 SubagentStart \u2192 tracks subagent spawns");
353
+ console.log(" \u2713 Stop \u2192 tracks turn count");
140
354
  console.log("\n \u2705 Done! Start a run: tokengolf start\n");
141
355
  }
142
356
  var realEntry, HOOKS_DIR, STATUSLINE_PATH, WRAPPER_PATH, CLAUDE_DIR, CLAUDE_SETTINGS;
@@ -211,11 +425,13 @@ function getEffortLevel(effort) {
211
425
  var MODEL_BUDGET_TIERS = {
212
426
  haiku: { diamond: 0.15, gold: 0.4, silver: 1, bronze: 2.5 },
213
427
  sonnet: { diamond: 0.5, gold: 1.5, silver: 4, bronze: 10 },
428
+ opusplan: { diamond: 1.5, gold: 4.5, silver: 12, bronze: 30 },
214
429
  opus: { diamond: 2.5, gold: 7.5, silver: 20, bronze: 50 }
215
430
  };
216
431
  function getModelBudgets(model) {
217
432
  const m = (model || "").toLowerCase();
218
433
  if (m.includes("haiku")) return MODEL_BUDGET_TIERS.haiku;
434
+ if (m.includes("opusplan")) return MODEL_BUDGET_TIERS.opusplan;
219
435
  if (m.includes("opus")) return MODEL_BUDGET_TIERS.opus;
220
436
  return MODEL_BUDGET_TIERS.sonnet;
221
437
  }
@@ -234,6 +450,13 @@ var MODEL_CLASSES = {
234
450
  difficulty: "Normal",
235
451
  color: "cyan"
236
452
  },
453
+ opusplan: {
454
+ name: "Paladin",
455
+ label: "Paladin",
456
+ emoji: "\u269C\uFE0F",
457
+ difficulty: "Calculated",
458
+ color: "yellow"
459
+ },
237
460
  opus: {
238
461
  name: "Opus",
239
462
  label: "Warlock",
@@ -253,9 +476,9 @@ function getTier(spent) {
253
476
  return BUDGET_TIERS.find((t) => spent <= t.max) || BUDGET_TIERS[BUDGET_TIERS.length - 1];
254
477
  }
255
478
  function getModelClass(model = "") {
256
- const key = Object.keys(MODEL_CLASSES).find(
257
- (k) => model.toLowerCase().includes(k)
258
- );
479
+ const m = model.toLowerCase();
480
+ if (m.includes("opusplan")) return MODEL_CLASSES.opusplan;
481
+ const key = Object.keys(MODEL_CLASSES).find((k) => m.includes(k));
259
482
  return MODEL_CLASSES[key] || MODEL_CLASSES.sonnet;
260
483
  }
261
484
  function getEfficiencyRating(spent, budget) {
@@ -271,7 +494,7 @@ function getBudgetPct(spent, budget) {
271
494
  }
272
495
  function formatCost(amount = 0) {
273
496
  if (amount === 0) return "$0.00";
274
- if (amount < 0.01) return `$${(amount * 100).toFixed(3)}\xA2`;
497
+ if (amount < 0.01) return `$${amount.toFixed(5)}`;
275
498
  return `$${amount.toFixed(4)}`;
276
499
  }
277
500
  function formatElapsed(startedAt) {
@@ -284,6 +507,12 @@ function formatElapsed(startedAt) {
284
507
  if (m > 0) return `${m}m ${s % 60}s`;
285
508
  return `${s}s`;
286
509
  }
510
+ function getOpusPct(modelBreakdown, totalSpent) {
511
+ if (!modelBreakdown || !totalSpent) return null;
512
+ const opusCost = Object.entries(modelBreakdown).filter(([m]) => m.toLowerCase().includes("opus")).reduce((sum, [, c]) => sum + c, 0);
513
+ if (opusCost === 0) return null;
514
+ return Math.round(opusCost / totalSpent * 100);
515
+ }
287
516
  function getHaikuPct(modelBreakdown, totalSpent) {
288
517
  if (!modelBreakdown || !totalSpent) return null;
289
518
  const haikuCost = Object.entries(modelBreakdown).filter(([m]) => m.toLowerCase().includes("haiku")).reduce((sum, [, c]) => sum + c, 0);
@@ -295,13 +524,56 @@ function calculateAchievements(run) {
295
524
  const won = run.status === "won";
296
525
  const pct = run.budget ? run.spent / run.budget : null;
297
526
  const mc = getModelClass(run.model);
527
+ const isPaladin = mc === MODEL_CLASSES.opusplan;
528
+ if ((run.modelSwitches ?? 0) >= 3)
529
+ achievements.push({
530
+ key: "indecisive",
531
+ label: `Indecisive \u2014 ${run.modelSwitches} model switches mid-session`,
532
+ emoji: "\u{1F3B2}"
533
+ });
298
534
  if (run.thinkingInvocations > 0 && run.status === "died")
299
535
  achievements.push({
300
536
  key: "hubris",
301
537
  label: "Hubris \u2014 Used ultrathink, busted anyway",
302
538
  emoji: "\u{1F926}"
303
539
  });
304
- if (!won) return achievements;
540
+ if (!won) {
541
+ if (run.budget && run.spent / run.budget >= 2)
542
+ achievements.push({ key: "blowout", label: "Blowout \u2014 Spent 2\xD7 budget", emoji: "\u{1F4A5}" });
543
+ else if (run.budget && run.spent / run.budget > 1 && run.spent / run.budget <= 1.1)
544
+ achievements.push({
545
+ key: "so_close",
546
+ label: "So Close \u2014 Died within 10% of budget",
547
+ emoji: "\u{1F62D}"
548
+ });
549
+ if ((run.totalToolCalls || 0) >= 30)
550
+ achievements.push({
551
+ key: "tool_happy",
552
+ label: `Tool Happy \u2014 Died with ${run.totalToolCalls} tool calls`,
553
+ emoji: "\u{1F528}"
554
+ });
555
+ if ((run.promptCount || 0) <= 2)
556
+ achievements.push({
557
+ key: "silent_death",
558
+ label: "Silent Death \u2014 Died with \u22642 prompts",
559
+ emoji: "\u{1FAA6}"
560
+ });
561
+ if ((run.failedToolCalls ?? 0) >= 5)
562
+ achievements.push({
563
+ key: "fumble",
564
+ label: `Fumble \u2014 Died with ${run.failedToolCalls} failed tool calls`,
565
+ emoji: "\u{1F921}"
566
+ });
567
+ if (run.budget && run.spent / run.budget >= 0.5) {
568
+ if ((run.promptCount || 0) >= 3 && run.spent / (run.promptCount || 1) >= 0.5)
569
+ achievements.push({
570
+ key: "expensive_taste",
571
+ label: "Expensive Taste \u2014 Over $0.50 per prompt",
572
+ emoji: "\u{1F377}"
573
+ });
574
+ }
575
+ return achievements;
576
+ }
305
577
  if (mc === MODEL_CLASSES.haiku) {
306
578
  achievements.push({
307
579
  key: "gold_haiku",
@@ -320,6 +592,18 @@ function calculateAchievements(run) {
320
592
  label: "Silver \u2014 Completed with Sonnet",
321
593
  emoji: "\u{1F948}"
322
594
  });
595
+ } else if (mc === MODEL_CLASSES.opusplan) {
596
+ achievements.push({
597
+ key: "paladin",
598
+ label: "Paladin \u2014 Completed a run as Paladin",
599
+ emoji: "\u269C\uFE0F"
600
+ });
601
+ if (pct !== null && pct <= 0.25)
602
+ achievements.push({
603
+ key: "grand_strategist",
604
+ label: "Grand Strategist \u2014 LEGENDARY efficiency as Paladin",
605
+ emoji: "\u265F\uFE0F"
606
+ });
323
607
  } else if (mc === MODEL_CLASSES.opus) {
324
608
  achievements.push({
325
609
  key: "bronze_opus",
@@ -352,13 +636,13 @@ function calculateAchievements(run) {
352
636
  achievements.push({
353
637
  key: "speedrunner",
354
638
  label: "Speedrunner \u2014 Low effort, completed under budget",
355
- emoji: "\u{1F3AF}"
639
+ emoji: "\u{1F3CE}\uFE0F"
356
640
  });
357
641
  if ((run.effort === "high" || run.effort === "max") && pct !== null && pct <= 0.25)
358
642
  achievements.push({
359
643
  key: "tryhard",
360
644
  label: "Tryhard \u2014 High effort, LEGENDARY efficiency",
361
- emoji: "\u{1F4AA}"
645
+ emoji: "\u{1F3CB}\uFE0F"
362
646
  });
363
647
  if (run.effort === "max" && mc === MODEL_CLASSES.opus)
364
648
  achievements.push({
@@ -372,7 +656,7 @@ function calculateAchievements(run) {
372
656
  achievements.push({
373
657
  key: "lightning",
374
658
  label: "Lightning Run \u2014 Opus fast mode, completed under budget",
375
- emoji: "\u26A1"
659
+ emoji: "\u26C8\uFE0F"
376
660
  });
377
661
  if (pct !== null && pct <= 0.25)
378
662
  achievements.push({
@@ -392,18 +676,16 @@ function calculateAchievements(run) {
392
676
  achievements.push({
393
677
  key: "no_rest",
394
678
  label: "No Rest for the Wicked \u2014 Completed in one session",
395
- emoji: "\u26A1"
679
+ emoji: "\u{1F525}"
396
680
  });
397
681
  if (run.fainted)
398
682
  achievements.push({
399
683
  key: "came_back",
400
684
  label: "Came Back \u2014 Fainted and finished anyway",
401
- emoji: "\u{1F4AA}"
685
+ emoji: "\u{1F9DF}"
402
686
  });
403
687
  const compactionEvents = run.compactionEvents || [];
404
- const manualCompactions = compactionEvents.filter(
405
- (e) => e.trigger === "manual"
406
- );
688
+ const manualCompactions = compactionEvents.filter((e) => e.trigger === "manual");
407
689
  const autoCompactions = compactionEvents.filter((e) => e.trigger === "auto");
408
690
  if (autoCompactions.length > 0)
409
691
  achievements.push({
@@ -412,9 +694,7 @@ function calculateAchievements(run) {
412
694
  emoji: "\u{1F4E6}"
413
695
  });
414
696
  if (manualCompactions.length > 0) {
415
- const minPct = Math.min(
416
- ...manualCompactions.map((e) => e.contextPct ?? 100)
417
- );
697
+ const minPct = Math.min(...manualCompactions.map((e) => e.contextPct ?? 100));
418
698
  if (minPct <= 30)
419
699
  achievements.push({
420
700
  key: "ghost_run",
@@ -445,7 +725,7 @@ function calculateAchievements(run) {
445
725
  achievements.push({
446
726
  key: "calculated_risk",
447
727
  label: "Calculated Risk \u2014 Ultrathink + LEGENDARY efficiency",
448
- emoji: "\u{1F9E0}"
728
+ emoji: "\u{1F9EE}"
449
729
  });
450
730
  if (ti >= 3)
451
731
  achievements.push({
@@ -460,6 +740,77 @@ function calculateAchievements(run) {
460
740
  label: "Silent Run \u2014 No extended thinking, completed under budget",
461
741
  emoji: "\u{1F92B}"
462
742
  });
743
+ if (mc === MODEL_CLASSES.opusplan) {
744
+ const opusPct = getOpusPct(run.modelBreakdown, run.spent);
745
+ if (opusPct !== null) {
746
+ if (opusPct > 60)
747
+ achievements.push({
748
+ key: "architect",
749
+ label: `Architect \u2014 Opus handled ${opusPct}% of cost (heavy planner)`,
750
+ emoji: "\u{1F3DB}\uFE0F"
751
+ });
752
+ if (opusPct < 25)
753
+ achievements.push({
754
+ key: "blitz",
755
+ label: `Blitz \u2014 Opus handled only ${opusPct}% of cost (light plan, fast execution)`,
756
+ emoji: "\u{1F4A8}"
757
+ });
758
+ if (opusPct >= 40 && opusPct <= 60)
759
+ achievements.push({
760
+ key: "equilibrium",
761
+ label: `Equilibrium \u2014 Opus and Sonnet balanced at ${opusPct}% / ${100 - opusPct}%`,
762
+ emoji: "\u2696\uFE0F"
763
+ });
764
+ }
765
+ }
766
+ const switches = run.modelSwitches ?? 0;
767
+ const distinct = run.distinctModels ?? 0;
768
+ if (!isPaladin) {
769
+ if (distinct === 1)
770
+ achievements.push({
771
+ key: "purist",
772
+ label: "Purist \u2014 Single model family throughout",
773
+ emoji: "\u{1F537}"
774
+ });
775
+ if (distinct >= 2 && pct !== null && pct < 1)
776
+ achievements.push({
777
+ key: "chameleon",
778
+ label: `Chameleon \u2014 ${distinct} model families used, completed under budget`,
779
+ emoji: "\u{1F98E}"
780
+ });
781
+ if (switches === 1 && pct !== null && pct < 1)
782
+ achievements.push({
783
+ key: "tactical_switch",
784
+ label: "Tactical Switch \u2014 Exactly 1 model switch, completed under budget",
785
+ emoji: "\u{1F500}"
786
+ });
787
+ if (switches === 0 && distinct <= 1)
788
+ achievements.push({
789
+ key: "committed",
790
+ label: "Committed \u2014 No model switches, one model family",
791
+ emoji: "\u{1F512}"
792
+ });
793
+ if (run.modelBreakdown && run.spent > 0) {
794
+ const declared = (run.model || "").toLowerCase();
795
+ const isHaikuRun = declared.includes("haiku");
796
+ const isSonnetRun = declared.includes("sonnet") && !declared.includes("opus");
797
+ const opusPct2 = getOpusPct(run.modelBreakdown, run.spent) ?? 0;
798
+ const haikuPct2 = getHaikuPct(run.modelBreakdown, run.spent) ?? 0;
799
+ const nonHaikuPct = 100 - haikuPct2;
800
+ if (isHaikuRun && nonHaikuPct > 50)
801
+ achievements.push({
802
+ key: "class_defection",
803
+ label: `Class Defection \u2014 Declared Haiku but ${nonHaikuPct}% cost on heavier models`,
804
+ emoji: "\u26A0\uFE0F"
805
+ });
806
+ else if (isSonnetRun && opusPct2 > 40)
807
+ achievements.push({
808
+ key: "class_defection",
809
+ label: `Class Defection \u2014 Declared Sonnet but ${opusPct2}% cost on Opus`,
810
+ emoji: "\u26A0\uFE0F"
811
+ });
812
+ }
813
+ }
463
814
  const haikuPct = getHaikuPct(run.modelBreakdown, run.spent);
464
815
  if (haikuPct !== null) {
465
816
  if (haikuPct >= 50)
@@ -475,6 +826,142 @@ function calculateAchievements(run) {
475
826
  emoji: "\u{1F3B2}"
476
827
  });
477
828
  }
829
+ const promptCount = run.promptCount || 0;
830
+ const totalToolCalls = run.totalToolCalls || 0;
831
+ if (promptCount === 1)
832
+ achievements.push({
833
+ key: "one_shot",
834
+ label: "One Shot \u2014 Completed in a single prompt",
835
+ emoji: "\u{1F94A}"
836
+ });
837
+ if (promptCount >= 20)
838
+ achievements.push({
839
+ key: "conversationalist",
840
+ label: `Conversationalist \u2014 ${promptCount} prompts`,
841
+ emoji: "\u{1F4AC}"
842
+ });
843
+ if (promptCount <= 3 && totalToolCalls >= 10)
844
+ achievements.push({
845
+ key: "terse",
846
+ label: `Terse \u2014 ${promptCount} prompts, ${totalToolCalls} tool calls`,
847
+ emoji: "\u{1F910}"
848
+ });
849
+ if (promptCount >= 15 && totalToolCalls / promptCount < 1)
850
+ achievements.push({
851
+ key: "backseat_driver",
852
+ label: "Backseat Driver \u2014 Many prompts, few tool calls",
853
+ emoji: "\u{1FA91}"
854
+ });
855
+ if (promptCount >= 2 && totalToolCalls / promptCount >= 5)
856
+ achievements.push({
857
+ key: "high_leverage",
858
+ label: `High Leverage \u2014 ${(totalToolCalls / promptCount).toFixed(1)}\xD7 tools per prompt`,
859
+ emoji: "\u{1F3D7}\uFE0F"
860
+ });
861
+ const toolCalls = run.toolCalls || {};
862
+ const editCount = toolCalls["Edit"] || 0;
863
+ const writeCount = toolCalls["Write"] || 0;
864
+ const readCount = toolCalls["Read"] || 0;
865
+ const bashCount = toolCalls["Bash"] || 0;
866
+ const distinctTools = Object.keys(toolCalls).filter((k) => toolCalls[k] > 0).length;
867
+ if (editCount === 0 && writeCount === 0 && readCount >= 1)
868
+ achievements.push({ key: "read_only", label: "Read Only \u2014 No edits or writes", emoji: "\u{1F441}\uFE0F" });
869
+ if (editCount >= 10)
870
+ achievements.push({ key: "editor", label: `Editor \u2014 ${editCount} Edit calls`, emoji: "\u270F\uFE0F" });
871
+ if (bashCount >= 10 && totalToolCalls >= 1 && bashCount / totalToolCalls >= 0.5)
872
+ achievements.push({
873
+ key: "bash_warrior",
874
+ label: `Bash Warrior \u2014 ${bashCount} Bash calls (${Math.round(bashCount / totalToolCalls * 100)}% of tools)`,
875
+ emoji: "\u{1F41A}"
876
+ });
877
+ if (totalToolCalls >= 5 && readCount / totalToolCalls >= 0.6)
878
+ achievements.push({
879
+ key: "scout",
880
+ label: `Scout \u2014 ${Math.round(readCount / totalToolCalls * 100)}% Read calls`,
881
+ emoji: "\u{1F50D}"
882
+ });
883
+ if (editCount >= 1 && editCount <= 3 && pct !== null && pct < 1)
884
+ achievements.push({
885
+ key: "surgeon",
886
+ label: `Surgeon \u2014 Only ${editCount} Edit call${editCount > 1 ? "s" : ""}, under budget`,
887
+ emoji: "\u{1F52A}"
888
+ });
889
+ if (distinctTools >= 5)
890
+ achievements.push({
891
+ key: "toolbox",
892
+ label: `Toolbox \u2014 ${distinctTools} distinct tools used`,
893
+ emoji: "\u{1F9F0}"
894
+ });
895
+ if (promptCount >= 3) {
896
+ const costPerPrompt = run.spent / promptCount;
897
+ if (costPerPrompt < 0.01)
898
+ achievements.push({
899
+ key: "cheap_shots",
900
+ label: `Cheap Shots \u2014 $${costPerPrompt.toFixed(4)} per prompt`,
901
+ emoji: "\u{1F4B2}"
902
+ });
903
+ if (costPerPrompt >= 0.5)
904
+ achievements.push({
905
+ key: "expensive_taste",
906
+ label: `Expensive Taste \u2014 $${costPerPrompt.toFixed(2)} per prompt`,
907
+ emoji: "\u{1F377}"
908
+ });
909
+ }
910
+ if (run.startedAt && run.endedAt) {
911
+ const elapsedMs = new Date(run.endedAt).getTime() - new Date(run.startedAt).getTime();
912
+ const elapsedMin = elapsedMs / 6e4;
913
+ if (elapsedMin < 5)
914
+ achievements.push({
915
+ key: "speedrun",
916
+ label: `Speedrun \u2014 Completed in ${Math.round(elapsedMin * 60)}s`,
917
+ emoji: "\u23F1\uFE0F"
918
+ });
919
+ if (elapsedMin > 60 && elapsedMin <= 180)
920
+ achievements.push({
921
+ key: "marathon",
922
+ label: `Marathon \u2014 ${Math.round(elapsedMin)}m session`,
923
+ emoji: "\u{1F3C3}"
924
+ });
925
+ if (elapsedMin > 180)
926
+ achievements.push({
927
+ key: "endurance",
928
+ label: `Endurance \u2014 ${Math.round(elapsedMin / 60)}h session`,
929
+ emoji: "\u{1FAE0}"
930
+ });
931
+ }
932
+ const failedToolCalls = run.failedToolCalls ?? 0;
933
+ const subagentSpawns = run.subagentSpawns ?? 0;
934
+ const turnCount = run.turnCount ?? 0;
935
+ if (failedToolCalls === 0 && totalToolCalls >= 5)
936
+ achievements.push({ key: "clean_run", label: "Clean Run \u2014 No tool failures", emoji: "\u2705" });
937
+ if (failedToolCalls >= 10)
938
+ achievements.push({
939
+ key: "stubborn",
940
+ label: `Stubborn \u2014 ${failedToolCalls} failed tool calls, still won`,
941
+ emoji: "\u{1F402}"
942
+ });
943
+ if (subagentSpawns === 0)
944
+ achievements.push({ key: "lone_wolf", label: "Lone Wolf \u2014 No subagents spawned", emoji: "\u{1F43A}" });
945
+ if (subagentSpawns >= 5)
946
+ achievements.push({
947
+ key: "summoner",
948
+ label: `Summoner \u2014 ${subagentSpawns} subagents spawned`,
949
+ emoji: "\u{1F4E1}"
950
+ });
951
+ if (subagentSpawns >= 10 && pct !== null && pct < 0.5)
952
+ achievements.push({
953
+ key: "army",
954
+ label: `Army of One \u2014 ${subagentSpawns} subagents, EFFICIENT cost`,
955
+ emoji: "\u{1FA96}"
956
+ });
957
+ if (promptCount >= 2 && turnCount >= 1 && turnCount / promptCount >= 3)
958
+ achievements.push({
959
+ key: "agentic",
960
+ label: `Agentic \u2014 ${(turnCount / promptCount).toFixed(1)} turns per prompt`,
961
+ emoji: "\u{1F916}"
962
+ });
963
+ if (promptCount >= 3 && turnCount === promptCount)
964
+ achievements.push({ key: "obedient", label: "Obedient \u2014 One turn per prompt", emoji: "\u{1F415}" });
478
965
  return achievements;
479
966
  }
480
967
 
@@ -517,11 +1004,13 @@ function getStats() {
517
1004
  const deaths = runs.filter((r) => r.status === "died");
518
1005
  const avgSpend = wins.length ? wins.reduce((sum, r) => sum + (r.spent || 0), 0) / wins.length : 0;
519
1006
  const bestRun = wins.length ? wins.reduce((best, r) => !best || r.spent < best.spent ? r : best, null) : null;
520
- const allAchievements = runs.flatMap((r) => (r.achievements || []).map((a) => ({
521
- ...a,
522
- quest: r.quest,
523
- earnedAt: r.endedAt
524
- })));
1007
+ const allAchievements = runs.flatMap(
1008
+ (r) => (r.achievements || []).map((a) => ({
1009
+ ...a,
1010
+ quest: r.quest,
1011
+ earnedAt: r.endedAt
1012
+ }))
1013
+ );
525
1014
  return {
526
1015
  total: runs.length,
527
1016
  wins: wins.length,
@@ -579,7 +1068,10 @@ function parseCostFromTranscript(transcriptPath) {
579
1068
  }
580
1069
  function findTranscriptsSince(projectDir, sinceMs) {
581
1070
  try {
582
- return fs3.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ p: path3.join(projectDir, f), mtime: fs3.statSync(path3.join(projectDir, f)).mtimeMs })).filter(({ mtime }) => mtime >= sinceMs).map(({ p }) => p);
1071
+ return fs3.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({
1072
+ p: path3.join(projectDir, f),
1073
+ mtime: fs3.statSync(path3.join(projectDir, f)).mtimeMs
1074
+ })).filter(({ mtime }) => mtime >= sinceMs).map(({ p }) => p);
583
1075
  } catch {
584
1076
  return [];
585
1077
  }
@@ -623,6 +1115,34 @@ function parseAllTranscripts(paths) {
623
1115
  }
624
1116
  return total > 0 ? { total, byModel } : null;
625
1117
  }
1118
+ function modelFamily(model) {
1119
+ const m = (model || "").toLowerCase();
1120
+ if (m.includes("haiku")) return "haiku";
1121
+ if (m.includes("sonnet")) return "sonnet";
1122
+ if (m.includes("opus")) return "opus";
1123
+ return "unknown";
1124
+ }
1125
+ function parseModelSwitches(transcriptPath) {
1126
+ try {
1127
+ const lines = fs3.readFileSync(transcriptPath, "utf8").trim().split("\n");
1128
+ let lastFamily = null;
1129
+ let switches = 0;
1130
+ for (const line of lines) {
1131
+ try {
1132
+ const entry = JSON.parse(line);
1133
+ if (entry.type === "assistant" && entry.message?.model) {
1134
+ const family = modelFamily(entry.message.model);
1135
+ if (lastFamily !== null && family !== lastFamily) switches++;
1136
+ lastFamily = family;
1137
+ }
1138
+ } catch {
1139
+ }
1140
+ }
1141
+ return { switches };
1142
+ } catch {
1143
+ return { switches: 0 };
1144
+ }
1145
+ }
626
1146
  function findTranscript(sessionId, projectDir) {
627
1147
  if (sessionId) {
628
1148
  try {
@@ -648,11 +1168,19 @@ function autoDetectCost(run) {
648
1168
  if (spent === null) return null;
649
1169
  const modelBreakdown = parsed?.byModel ?? run.modelBreakdown ?? null;
650
1170
  const thinking = parseThinkingFromTranscripts(paths);
1171
+ const primaryPath = findTranscript(run.sessionId, projectDir);
1172
+ const { switches: modelSwitches } = primaryPath ? parseModelSwitches(primaryPath) : { switches: 0 };
1173
+ const families = new Set(
1174
+ Object.keys(parsed?.byModel ?? {}).map(modelFamily).filter((f) => f !== "unknown")
1175
+ );
1176
+ const distinctModels = families.size;
651
1177
  return {
652
1178
  spent,
653
1179
  modelBreakdown,
654
1180
  thinkingInvocations: thinking?.thinkingInvocations ?? 0,
655
- thinkingTokens: thinking?.thinkingTokens ?? 0
1181
+ thinkingTokens: thinking?.thinkingTokens ?? 0,
1182
+ modelSwitches,
1183
+ distinctModels
656
1184
  };
657
1185
  }
658
1186
 
@@ -663,6 +1191,7 @@ import { TextInput, Select, ConfirmInput } from "@inkjs/ui";
663
1191
  var MODEL_OPTIONS = [
664
1192
  { label: "\u2694\uFE0F Sonnet \u2014 Balanced. The default run. [Normal]", value: "claude-sonnet-4-6" },
665
1193
  { label: "\u{1F3F9} Haiku \u2014 Glass cannon. Hard mode. [Hard]", value: "claude-haiku-4-5-20251001" },
1194
+ { label: "\u269C\uFE0F Paladin \u2014 Opus plans, Sonnet executes. [Calculated]", value: "opusplan" },
666
1195
  { label: "\u{1F9D9} Opus \u2014 Powerful but expensive. [Easy]", value: "claude-opus-4-6" }
667
1196
  ];
668
1197
  var EFFORT_OPTIONS_BASE = [
@@ -674,11 +1203,14 @@ var EFFORT_OPTIONS_OPUS = [
674
1203
  ...EFFORT_OPTIONS_BASE,
675
1204
  { label: "\u{1F4A5} Max \u2014 Absolute max, no token constraints (Opus only)", value: "max" }
676
1205
  ];
677
- var getEffortOptions = (model) => model.toLowerCase().includes("opus") ? EFFORT_OPTIONS_OPUS : EFFORT_OPTIONS_BASE;
1206
+ var getEffortOptions = (model) => model.toLowerCase().includes("opus") && !model.toLowerCase().includes("opusplan") ? EFFORT_OPTIONS_OPUS : EFFORT_OPTIONS_BASE;
678
1207
  function getBudgetOptions(model) {
679
1208
  const b = getModelBudgets(model);
680
1209
  return [
681
- { label: `\u{1F48E} Diamond \u2014 $${b.diamond.toFixed(2)} surgical micro-task`, value: String(b.diamond) },
1210
+ {
1211
+ label: `\u{1F48E} Diamond \u2014 $${b.diamond.toFixed(2)} surgical micro-task`,
1212
+ value: String(b.diamond)
1213
+ },
682
1214
  { label: `\u{1F947} Gold \u2014 $${b.gold.toFixed(2)} focused small task`, value: String(b.gold) },
683
1215
  { label: `\u{1F948} Silver \u2014 $${b.silver.toFixed(2)} medium task`, value: String(b.silver) },
684
1216
  { label: `\u{1F949} Bronze \u2014 $${b.bronze.toFixed(2)} heavy / complex`, value: String(b.bronze) },
@@ -694,56 +1226,125 @@ function StartRun() {
694
1226
  const [budgetVal, setBudgetVal] = useState("");
695
1227
  const budget = parseFloat(budgetVal) || 0;
696
1228
  const mc = model ? getModelClass(model) : null;
697
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1, paddingX: 1, paddingY: 1 }, /* @__PURE__ */ React.createElement(Box, { gap: 2 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "yellow" }, "\u26F3 TokenGolf"), /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "New Run")), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, paddingY: 1 }, /* @__PURE__ */ React.createElement(Box, { gap: 2, alignItems: "flex-start" }, /* @__PURE__ */ React.createElement(Text, { color: step === "quest" ? "cyan" : "gray" }, "\u{1F4CB} Quest "), step === "quest" ? /* @__PURE__ */ React.createElement(TextInput, { placeholder: "What are you shipping?", onSubmit: (v) => {
698
- if (v.trim()) {
699
- setQuest(v.trim());
700
- setStep("model");
701
- }
702
- } }) : /* @__PURE__ */ React.createElement(Text, { color: "white" }, quest)), step !== "quest" && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React.createElement(Box, { gap: 2 }, /* @__PURE__ */ React.createElement(Text, { color: step === "model" ? "cyan" : "gray" }, "\u{1F3AE} Class "), step !== "model" && /* @__PURE__ */ React.createElement(Text, { color: "white" }, mc?.emoji, " ", mc?.name, " [", mc?.difficulty, "]")), step === "model" && /* @__PURE__ */ React.createElement(Select, { options: MODEL_OPTIONS, onChange: (v) => {
703
- setModel(v);
704
- if (v.toLowerCase().includes("haiku")) {
705
- setEffort(null);
706
- setStep("budget");
707
- } else setStep("effort");
708
- } })), (step === "effort" || step === "budget" || step === "custom" || step === "confirm") && effort !== null && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React.createElement(Box, { gap: 2 }, /* @__PURE__ */ React.createElement(Text, { color: step === "effort" ? "cyan" : "gray" }, "\u26A1 Effort "), step !== "effort" && effort && /* @__PURE__ */ React.createElement(Text, { color: "white" }, getEffortLevel(effort)?.emoji, " ", getEffortLevel(effort)?.label)), step === "effort" && /* @__PURE__ */ React.createElement(Select, { options: getEffortOptions(model), onChange: (v) => {
709
- setEffort(v);
710
- setStep("budget");
711
- } })), (step === "budget" || step === "custom" || step === "confirm") && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React.createElement(Box, { gap: 2 }, /* @__PURE__ */ React.createElement(Text, { color: step === "budget" || step === "custom" ? "cyan" : "gray" }, "\u{1F4B0} Budget "), step === "confirm" && /* @__PURE__ */ React.createElement(Text, { color: "green" }, "$", budget.toFixed(2))), step === "budget" && /* @__PURE__ */ React.createElement(Select, { options: getBudgetOptions(model), onChange: (v) => {
712
- if (v === "custom") {
713
- setStep("custom");
714
- } else {
715
- setBudgetVal(v);
716
- setStep("confirm");
717
- }
718
- } }), step === "custom" && /* @__PURE__ */ React.createElement(TextInput, { placeholder: "Enter amount e.g. 0.50", onSubmit: (v) => {
719
- const n = parseFloat(v);
720
- if (!isNaN(n) && n > 0) {
721
- setBudgetVal(String(n));
722
- setStep("confirm");
723
- }
724
- } })), step === "confirm" && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 }, /* @__PURE__ */ React.createElement(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "yellow" }, "Ready?"), /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Quest ", /* @__PURE__ */ React.createElement(Text, { color: "white" }, quest)), /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Model ", /* @__PURE__ */ React.createElement(Text, { color: "white" }, mc?.emoji, " ", mc?.name, " [", mc?.difficulty, "]")), effort && /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Effort ", /* @__PURE__ */ React.createElement(Text, { color: "white" }, getEffortLevel(effort)?.emoji, " ", getEffortLevel(effort)?.label)), /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Budget ", /* @__PURE__ */ React.createElement(Text, { color: "green" }, "$", budget.toFixed(2)))), /* @__PURE__ */ React.createElement(Box, { gap: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Confirm? "), /* @__PURE__ */ React.createElement(
725
- ConfirmInput,
1229
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1, paddingX: 1, paddingY: 1 }, /* @__PURE__ */ React.createElement(Box, { gap: 2 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "yellow" }, "\u26F3 TokenGolf"), /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "New Run")), /* @__PURE__ */ React.createElement(
1230
+ Box,
726
1231
  {
727
- onConfirm: () => {
728
- setCurrentRun({
729
- quest,
730
- model,
731
- budget,
732
- effort,
733
- spent: 0,
734
- status: "active",
735
- floor: 1,
736
- totalFloors: FLOORS.length,
737
- promptCount: 0,
738
- totalToolCalls: 0,
739
- toolCalls: {},
740
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
741
- });
742
- exit();
1232
+ flexDirection: "column",
1233
+ gap: 1,
1234
+ borderStyle: "single",
1235
+ borderColor: "gray",
1236
+ paddingX: 1,
1237
+ paddingY: 1
1238
+ },
1239
+ /* @__PURE__ */ React.createElement(Box, { gap: 2, alignItems: "flex-start" }, /* @__PURE__ */ React.createElement(Text, { color: step === "quest" ? "cyan" : "gray" }, "\u{1F4CB} Quest "), step === "quest" ? /* @__PURE__ */ React.createElement(
1240
+ TextInput,
1241
+ {
1242
+ placeholder: "What are you shipping?",
1243
+ onSubmit: (v) => {
1244
+ if (v.trim()) {
1245
+ setQuest(v.trim());
1246
+ setStep("model");
1247
+ }
1248
+ }
1249
+ }
1250
+ ) : /* @__PURE__ */ React.createElement(Text, { color: "white" }, quest)),
1251
+ step !== "quest" && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React.createElement(Box, { gap: 2 }, /* @__PURE__ */ React.createElement(Text, { color: step === "model" ? "cyan" : "gray" }, "\u{1F3AE} Class "), step !== "model" && /* @__PURE__ */ React.createElement(Text, { color: "white" }, mc?.emoji, " ", mc?.name, " [", mc?.difficulty, "]")), step === "model" && /* @__PURE__ */ React.createElement(
1252
+ Select,
1253
+ {
1254
+ options: MODEL_OPTIONS,
1255
+ onChange: (v) => {
1256
+ setModel(v);
1257
+ if (v.toLowerCase().includes("haiku")) {
1258
+ setEffort(null);
1259
+ setStep("budget");
1260
+ } else setStep("effort");
1261
+ }
1262
+ }
1263
+ )),
1264
+ (step === "effort" || step === "budget" || step === "custom" || step === "confirm") && effort !== null && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React.createElement(Box, { gap: 2 }, /* @__PURE__ */ React.createElement(Text, { color: step === "effort" ? "cyan" : "gray" }, "\u26A1 Effort "), step !== "effort" && effort && /* @__PURE__ */ React.createElement(Text, { color: "white" }, getEffortLevel(effort)?.emoji, " ", getEffortLevel(effort)?.label)), step === "effort" && /* @__PURE__ */ React.createElement(
1265
+ Select,
1266
+ {
1267
+ options: getEffortOptions(model),
1268
+ onChange: (v) => {
1269
+ setEffort(v);
1270
+ setStep("budget");
1271
+ }
1272
+ }
1273
+ )),
1274
+ (step === "budget" || step === "custom" || step === "confirm") && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React.createElement(Box, { gap: 2 }, /* @__PURE__ */ React.createElement(Text, { color: step === "budget" || step === "custom" ? "cyan" : "gray" }, "\u{1F4B0} Budget", " "), step === "confirm" && /* @__PURE__ */ React.createElement(Text, { color: "green" }, "$", budget.toFixed(2))), step === "budget" && /* @__PURE__ */ React.createElement(
1275
+ Select,
1276
+ {
1277
+ options: getBudgetOptions(model),
1278
+ onChange: (v) => {
1279
+ if (v === "custom") {
1280
+ setStep("custom");
1281
+ } else {
1282
+ setBudgetVal(v);
1283
+ setStep("confirm");
1284
+ }
1285
+ }
1286
+ }
1287
+ ), step === "custom" && /* @__PURE__ */ React.createElement(
1288
+ TextInput,
1289
+ {
1290
+ placeholder: "Enter amount e.g. 0.50",
1291
+ onSubmit: (v) => {
1292
+ const n = parseFloat(v);
1293
+ if (!isNaN(n) && n > 0) {
1294
+ setBudgetVal(String(n));
1295
+ setStep("confirm");
1296
+ }
1297
+ }
1298
+ }
1299
+ )),
1300
+ step === "confirm" && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 }, /* @__PURE__ */ React.createElement(
1301
+ Box,
1302
+ {
1303
+ borderStyle: "round",
1304
+ borderColor: "yellow",
1305
+ paddingX: 1,
1306
+ paddingY: 1,
1307
+ flexDirection: "column"
743
1308
  },
744
- onCancel: () => setStep("quest")
745
- }
746
- )))), step !== "confirm" && /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Use \u2191\u2193 to navigate, Enter to select"), step === "confirm" && /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "After confirming, work normally in Claude Code. Run `tokengolf win` or `tokengolf bust` when done."));
1309
+ /* @__PURE__ */ React.createElement(Text, { bold: true, color: "yellow" }, "Ready?"),
1310
+ /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Quest ", /* @__PURE__ */ React.createElement(Text, { color: "white" }, quest)),
1311
+ /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Model", " ", /* @__PURE__ */ React.createElement(Text, { color: "white" }, mc?.emoji, " ", mc?.name, " [", mc?.difficulty, "]")),
1312
+ effort && /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Effort", " ", /* @__PURE__ */ React.createElement(Text, { color: "white" }, getEffortLevel(effort)?.emoji, " ", getEffortLevel(effort)?.label)),
1313
+ /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Budget ", /* @__PURE__ */ React.createElement(Text, { color: "green" }, "$", budget.toFixed(2)))
1314
+ ), /* @__PURE__ */ React.createElement(Box, { gap: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Confirm? "), /* @__PURE__ */ React.createElement(
1315
+ ConfirmInput,
1316
+ {
1317
+ onConfirm: () => {
1318
+ setCurrentRun({
1319
+ id: `run_${Date.now()}`,
1320
+ quest,
1321
+ model,
1322
+ budget,
1323
+ effort,
1324
+ spent: 0,
1325
+ status: "active",
1326
+ mode: "roguelike",
1327
+ floor: 1,
1328
+ totalFloors: FLOORS.length,
1329
+ promptCount: 0,
1330
+ totalToolCalls: 0,
1331
+ toolCalls: {},
1332
+ failedToolCalls: 0,
1333
+ subagentSpawns: 0,
1334
+ turnCount: 0,
1335
+ thinkingInvocations: 0,
1336
+ thinkingTokens: 0,
1337
+ fainted: false,
1338
+ sessionCount: 1,
1339
+ compactionEvents: [],
1340
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1341
+ });
1342
+ exit();
1343
+ },
1344
+ onCancel: () => setStep("quest")
1345
+ }
1346
+ )))
1347
+ ), step !== "confirm" && /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "Use \u2191\u2193 to navigate, Enter to select"), step === "confirm" && /* @__PURE__ */ React.createElement(Text, { color: "gray", dimColor: true }, "After confirming, work normally in Claude Code. Run `tokengolf win` or `tokengolf bust` when done."));
747
1348
  }
748
1349
 
749
1350
  // src/components/ActiveRun.js
@@ -765,16 +1366,43 @@ function ActiveRun({ run: initialRun }) {
765
1366
  useInput((input) => {
766
1367
  if (input === "q") exit();
767
1368
  });
1369
+ const flowMode = !run.budget;
768
1370
  const mc = getModelClass(run.model);
769
- const pct = getBudgetPct(run.spent, run.budget);
770
- const efficiency = getEfficiencyRating(run.spent, run.budget);
771
- const barColor = pct >= 80 ? "red" : pct >= 50 ? "yellow" : "green";
772
- return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1, paddingX: 1, paddingY: 1 }, /* @__PURE__ */ React2.createElement(Box2, { gap: 2 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u26F3 TokenGolf"), /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Active Run"), /* @__PURE__ */ React2.createElement(Text2, { color: "gray", dimColor: true }, formatElapsed(run.startedAt))), /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, flexDirection: "column", gap: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "white" }, run.quest), /* @__PURE__ */ React2.createElement(Box2, { gap: 3 }, /* @__PURE__ */ React2.createElement(Text2, null, mc.emoji, " ", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, mc.name)), /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Budget ", /* @__PURE__ */ React2.createElement(Text2, { color: "green" }, "$", run.budget.toFixed(2))), /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Spent ", /* @__PURE__ */ React2.createElement(Text2, { color: barColor }, formatCost(run.spent))), /* @__PURE__ */ React2.createElement(Text2, { color: efficiency.color }, efficiency.emoji, " ", efficiency.label)), /* @__PURE__ */ React2.createElement(Box2, { gap: 1, alignItems: "center" }, /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "\u{1F4B0} "), /* @__PURE__ */ React2.createElement(Box2, { width: 24 }, /* @__PURE__ */ React2.createElement(ProgressBar, { value: Math.min(pct, 100) })), /* @__PURE__ */ React2.createElement(Text2, { color: barColor }, " ", pct, "%")), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 0, marginTop: 1 }, FLOORS.map((floor, i) => {
773
- const n = i + 1;
774
- const done = n < run.floor;
775
- const active = n === run.floor;
776
- return /* @__PURE__ */ React2.createElement(Box2, { key: i, gap: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: done ? "green" : active ? "yellow" : "gray" }, done ? "\u2713" : active ? "\u25B6" : "\u25CB"), /* @__PURE__ */ React2.createElement(Text2, { color: done ? "green" : active ? "white" : "gray", dimColor: !done && !active }, "Floor ", n, ": ", floor));
777
- })), /* @__PURE__ */ React2.createElement(Box2, { gap: 3, marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Prompts ", /* @__PURE__ */ React2.createElement(Text2, { color: "white" }, run.promptCount || 0)), /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Tools ", /* @__PURE__ */ React2.createElement(Text2, { color: "white" }, run.totalToolCalls || 0))), pct >= 80 && pct < 100 && /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "red", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red", bold: true }, "\u26A0\uFE0F BUDGET WARNING \u2014 ", formatCost(run.budget - run.spent), " left"))), /* @__PURE__ */ React2.createElement(Text2, { color: "gray", dimColor: true }, "tokengolf win [--spent 0.18] \xB7 tokengolf bust \xB7 q to close"));
1371
+ const pct = flowMode ? null : getBudgetPct(run.spent, run.budget);
1372
+ const efficiency = flowMode ? null : getEfficiencyRating(run.spent, run.budget);
1373
+ const barColor = !pct ? "green" : pct >= 80 ? "red" : pct >= 50 ? "yellow" : "green";
1374
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1, paddingX: 1, paddingY: 1 }, /* @__PURE__ */ React2.createElement(Box2, { gap: 2 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u26F3 TokenGolf"), /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Active Run"), /* @__PURE__ */ React2.createElement(Text2, { color: "gray", dimColor: true }, formatElapsed(run.startedAt))), /* @__PURE__ */ React2.createElement(
1375
+ Box2,
1376
+ {
1377
+ borderStyle: "round",
1378
+ borderColor: "yellow",
1379
+ paddingX: 1,
1380
+ paddingY: 1,
1381
+ flexDirection: "column",
1382
+ gap: 1
1383
+ },
1384
+ /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "white" }, run.quest),
1385
+ /* @__PURE__ */ React2.createElement(Box2, { gap: 3 }, /* @__PURE__ */ React2.createElement(Text2, null, mc.emoji, " ", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, mc.name)), flowMode ? /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Flow Mode") : /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Budget ", /* @__PURE__ */ React2.createElement(Text2, { color: "green" }, "$", run.budget.toFixed(2))), /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Spent ", /* @__PURE__ */ React2.createElement(Text2, { color: barColor }, formatCost(run.spent))), !flowMode && /* @__PURE__ */ React2.createElement(Text2, { color: efficiency.color }, efficiency.emoji, " ", efficiency.label)),
1386
+ !flowMode && /* @__PURE__ */ React2.createElement(Box2, { gap: 1, alignItems: "center" }, /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "\u{1F4B0} "), /* @__PURE__ */ React2.createElement(Box2, { width: 24 }, /* @__PURE__ */ React2.createElement(ProgressBar, { value: Math.min(pct, 100) })), /* @__PURE__ */ React2.createElement(Text2, { color: barColor }, " ", pct, "%")),
1387
+ /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 0, marginTop: 1 }, FLOORS.map((floor, i) => {
1388
+ const n = i + 1;
1389
+ const done = n < run.floor;
1390
+ const active = n === run.floor;
1391
+ return /* @__PURE__ */ React2.createElement(Box2, { key: i, gap: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: done ? "green" : active ? "yellow" : "gray" }, done ? "\u2713" : active ? "\u25B6" : "\u25CB"), /* @__PURE__ */ React2.createElement(
1392
+ Text2,
1393
+ {
1394
+ color: done ? "green" : active ? "white" : "gray",
1395
+ dimColor: !done && !active
1396
+ },
1397
+ "Floor ",
1398
+ n,
1399
+ ": ",
1400
+ floor
1401
+ ));
1402
+ })),
1403
+ /* @__PURE__ */ React2.createElement(Box2, { gap: 3, marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Prompts ", /* @__PURE__ */ React2.createElement(Text2, { color: "white" }, run.promptCount || 0)), /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Tools ", /* @__PURE__ */ React2.createElement(Text2, { color: "white" }, run.totalToolCalls || 0))),
1404
+ pct >= 80 && pct < 100 && /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "red", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red", bold: true }, "\u26A0\uFE0F BUDGET WARNING \u2014 ", formatCost(run.budget - run.spent), " left"))
1405
+ ), /* @__PURE__ */ React2.createElement(Text2, { color: "gray", dimColor: true }, "tokengolf win [--spent 0.18] \xB7 tokengolf bust \xB7 q to close"));
778
1406
  }
779
1407
 
780
1408
  // src/components/ScoreCard.js
@@ -796,14 +1424,46 @@ function ScoreCard({ run }) {
796
1424
  const efficiency = flowMode ? null : getEfficiencyRating(run.spent, run.budget);
797
1425
  const pct = flowMode ? null : getBudgetPct(run.spent, run.budget);
798
1426
  const haikuPct = getHaikuPct(run.modelBreakdown, run.spent);
799
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", paddingX: 1, paddingY: 1, gap: 1 }, /* @__PURE__ */ React3.createElement(Box3, { borderStyle: "double", borderColor: won ? "yellow" : "red", paddingX: 2, paddingY: 1, flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: won ? "yellow" : "red" }, won ? "\u{1F3C6} SESSION COMPLETE" : "\u{1F480} BUDGET BUSTED"), /* @__PURE__ */ React3.createElement(Text3, { color: "white", bold: true }, run.quest ?? /* @__PURE__ */ React3.createElement(Text3, { color: "gray" }, "Flow Mode")), /* @__PURE__ */ React3.createElement(Box3, { gap: 4, flexWrap: "wrap", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "SPENT"), /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: won ? "green" : "red" }, formatCost(run.spent))), !flowMode && /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "BUDGET"), /* @__PURE__ */ React3.createElement(Text3, { color: "white" }, "$", run.budget.toFixed(2))), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "USED"), /* @__PURE__ */ React3.createElement(Text3, { color: pct > 100 ? "red" : pct > 80 ? "yellow" : "green" }, pct, "%"))), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "MODEL"), /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, mc.emoji, " ", mc.name, [
800
- run.effort && run.effort !== "medium" ? getEffortLevel(run.effort)?.label : null,
801
- run.fastMode ? "Fast" : null
802
- ].filter(Boolean).map((s) => `\xB7${s}`).join(""))), run.effort && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "EFFORT"), /* @__PURE__ */ React3.createElement(Text3, { color: getEffortLevel(run.effort)?.color }, getEffortLevel(run.effort)?.emoji, " ", getEffortLevel(run.effort)?.label)), run.fastMode && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "MODE"), /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "\u21AF Fast")), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "TIER"), /* @__PURE__ */ React3.createElement(Text3, { color: tier.color }, tier.emoji, " ", tier.label))), efficiency && /* @__PURE__ */ React3.createElement(Box3, { gap: 2 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: efficiency.color }, efficiency.emoji, " ", efficiency.label)), run.achievements?.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 0, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Achievements unlocked:"), run.achievements.map((a, i) => /* @__PURE__ */ React3.createElement(Text3, { key: i, color: "yellow" }, " ", a.emoji, " ", a.label))), run.thinkingInvocations > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 0, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, { gap: 3, alignItems: "center" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Extended thinking:"), /* @__PURE__ */ React3.createElement(Text3, { color: "magenta" }, "\u{1F52E} ", run.thinkingInvocations, "\xD7 invoked"))), run.modelBreakdown && Object.keys(run.modelBreakdown).length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 0, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, { gap: 2, alignItems: "center" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Model usage:"), haikuPct !== null && /* @__PURE__ */ React3.createElement(Text3, { color: haikuPct >= 75 ? "magenta" : haikuPct >= 50 ? "cyan" : "yellow" }, "\u{1F3F9} ", haikuPct, "% Haiku")), /* @__PURE__ */ React3.createElement(Box3, { gap: 3, flexWrap: "wrap" }, Object.entries(run.modelBreakdown).map(([model, cost]) => {
803
- const short = model.includes("haiku") ? "Haiku" : model.includes("sonnet") ? "Sonnet" : "Opus";
804
- const pctOfTotal = Math.round(cost / run.spent * 100);
805
- return /* @__PURE__ */ React3.createElement(Text3, { key: model, color: "gray" }, short, " ", /* @__PURE__ */ React3.createElement(Text3, { color: "white" }, pctOfTotal, "%"), " ", /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, formatCost(cost)));
806
- }))), run.toolCalls && Object.keys(run.toolCalls).length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 0, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Tool calls:"), /* @__PURE__ */ React3.createElement(Box3, { gap: 2, flexWrap: "wrap" }, Object.entries(run.toolCalls).map(([tool, count]) => /* @__PURE__ */ React3.createElement(Text3, { key: tool, color: "gray" }, /* @__PURE__ */ React3.createElement(Text3, { color: "white" }, tool), " \xD7", count)))), !won && run.budget && /* @__PURE__ */ React3.createElement(Box3, { borderStyle: "single", borderColor: "red", paddingX: 1, marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "Cause of death: Budget exceeded by ", formatCost(run.spent - run.budget)), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Tip: Use Read with line ranges instead of full file reads."))), /* @__PURE__ */ React3.createElement(Box3, { gap: 2 }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "tokengolf start \u2014 run again"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "\xB7"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "tokengolf stats \u2014 career stats"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "\xB7"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "q to exit")));
1427
+ const opusPct = getOpusPct(run.modelBreakdown, run.spent);
1428
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", paddingX: 1, paddingY: 1, gap: 1 }, /* @__PURE__ */ React3.createElement(
1429
+ Box3,
1430
+ {
1431
+ borderStyle: "double",
1432
+ borderColor: won ? "yellow" : "red",
1433
+ paddingX: 2,
1434
+ paddingY: 1,
1435
+ flexDirection: "column",
1436
+ gap: 1
1437
+ },
1438
+ /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: won ? "yellow" : "red" }, won ? "\u{1F3C6} SESSION COMPLETE" : "\u{1F480} BUDGET BUSTED"),
1439
+ /* @__PURE__ */ React3.createElement(Text3, { color: "white", bold: true }, run.quest ?? /* @__PURE__ */ React3.createElement(Text3, { color: "gray" }, "Flow Mode")),
1440
+ /* @__PURE__ */ React3.createElement(Box3, { gap: 4, flexWrap: "wrap", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "SPENT"), /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: won ? "green" : "red" }, formatCost(run.spent))), !flowMode && /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "BUDGET"), /* @__PURE__ */ React3.createElement(Text3, { color: "white" }, "$", run.budget.toFixed(2))), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "USED"), /* @__PURE__ */ React3.createElement(Text3, { color: pct > 100 ? "red" : pct > 80 ? "yellow" : "green" }, pct, "%"))), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "MODEL"), /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, mc.emoji, " ", mc.name, [
1441
+ run.effort && run.effort !== "medium" ? getEffortLevel(run.effort)?.label : null,
1442
+ run.fastMode ? "Fast" : null
1443
+ ].filter(Boolean).map((s) => `\xB7${s}`).join(""))), run.effort && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "EFFORT"), /* @__PURE__ */ React3.createElement(Text3, { color: getEffortLevel(run.effort)?.color }, getEffortLevel(run.effort)?.emoji, " ", getEffortLevel(run.effort)?.label)), run.fastMode && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "MODE"), /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "\u21AF Fast")), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "TIER"), /* @__PURE__ */ React3.createElement(Text3, { color: tier.color }, tier.emoji, " ", tier.label))),
1444
+ efficiency && /* @__PURE__ */ React3.createElement(Box3, { gap: 2 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: efficiency.color }, efficiency.emoji, " ", efficiency.label)),
1445
+ run.achievements?.length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 0, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Achievements unlocked:"), run.achievements.map((a, i) => /* @__PURE__ */ React3.createElement(Text3, { key: i, color: "yellow" }, " ", a.emoji, " ", a.label))),
1446
+ run.thinkingInvocations > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 0, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, { gap: 3, alignItems: "center" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Extended thinking:"), /* @__PURE__ */ React3.createElement(Text3, { color: "magenta" }, "\u{1F52E} ", run.thinkingInvocations, "\xD7 invoked"))),
1447
+ run.modelBreakdown && Object.keys(run.modelBreakdown).length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 0, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, { gap: 2, alignItems: "center" }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Model usage:"), haikuPct !== null && /* @__PURE__ */ React3.createElement(Text3, { color: haikuPct >= 75 ? "magenta" : haikuPct >= 50 ? "cyan" : "yellow" }, "\u{1F3F9} ", haikuPct, "% Haiku"), mc === MODEL_CLASSES.opusplan && opusPct !== null && /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "\u269C\uFE0F ", opusPct, "% Opus (planning)")), /* @__PURE__ */ React3.createElement(Box3, { gap: 3, flexWrap: "wrap" }, Object.entries(run.modelBreakdown).map(([model, cost]) => {
1448
+ const m = model.toLowerCase();
1449
+ const short = m.includes("haiku") ? "Haiku" : m.includes("sonnet") ? "Sonnet" : m.includes("opusplan") || m.includes("paladin") ? "Paladin" : "Opus";
1450
+ const pctOfTotal = Math.round(cost / run.spent * 100);
1451
+ return /* @__PURE__ */ React3.createElement(Text3, { key: model, color: "gray" }, short, " ", /* @__PURE__ */ React3.createElement(Text3, { color: "white" }, pctOfTotal, "%"), " ", /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, formatCost(cost)));
1452
+ }))),
1453
+ run.toolCalls && Object.keys(run.toolCalls).length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 0, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Tool calls:"), /* @__PURE__ */ React3.createElement(Box3, { gap: 2, flexWrap: "wrap" }, Object.entries(run.toolCalls).map(([tool, count]) => /* @__PURE__ */ React3.createElement(Text3, { key: tool, color: "gray" }, /* @__PURE__ */ React3.createElement(Text3, { color: "white" }, tool), " \xD7", count)))),
1454
+ !won && run.budget && /* @__PURE__ */ React3.createElement(
1455
+ Box3,
1456
+ {
1457
+ borderStyle: "single",
1458
+ borderColor: "red",
1459
+ paddingX: 1,
1460
+ marginTop: 1,
1461
+ flexDirection: "column"
1462
+ },
1463
+ /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "Cause of death: Budget exceeded by ", formatCost(run.spent - run.budget)),
1464
+ /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Tip: Use Read with line ranges instead of full file reads.")
1465
+ )
1466
+ ), /* @__PURE__ */ React3.createElement(Box3, { gap: 2 }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "tokengolf start \u2014 run again"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "\xB7"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "tokengolf stats \u2014 career stats"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "\xB7"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "q to exit")));
807
1467
  }
808
1468
 
809
1469
  // src/components/StatsView.js
@@ -820,13 +1480,24 @@ function StatsView({ stats }) {
820
1480
  return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", paddingX: 1, paddingY: 1, gap: 1 }, /* @__PURE__ */ React4.createElement(Box4, { gap: 2 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "yellow" }, "\u26F3 TokenGolf"), /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "Career Stats")), /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "single", borderColor: "gray", paddingX: 1, paddingY: 1, gap: 4 }, /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "RUNS"), /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "white" }, stats.total)), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "WINS"), /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "green" }, stats.wins)), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "DEATHS"), /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "red" }, stats.deaths)), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "WIN RATE"), /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: stats.winRate >= 70 ? "green" : stats.winRate >= 40 ? "yellow" : "red" }, stats.winRate, "%")), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "AVG SPEND"), /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "cyan" }, formatCost(stats.avgSpend)))), stats.bestRun && (() => {
821
1481
  const bestTier = getTier(stats.bestRun.spent);
822
1482
  const bestMc = getModelClass(stats.bestRun.model);
823
- return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "\u{1F3C6} Personal Best"), /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "white" }, stats.bestRun.quest), /* @__PURE__ */ React4.createElement(Box4, { gap: 3, marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "green" }, formatCost(stats.bestRun.spent)), /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "of $", stats.bestRun.budget?.toFixed(2)), /* @__PURE__ */ React4.createElement(Text4, null, bestMc.emoji), /* @__PURE__ */ React4.createElement(Text4, { color: bestTier.color }, bestTier.emoji, " ", bestTier.label))));
1483
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "\u{1F3C6} Personal Best"), /* @__PURE__ */ React4.createElement(
1484
+ Box4,
1485
+ {
1486
+ borderStyle: "round",
1487
+ borderColor: "yellow",
1488
+ paddingX: 1,
1489
+ paddingY: 1,
1490
+ flexDirection: "column"
1491
+ },
1492
+ /* @__PURE__ */ React4.createElement(Text4, { color: "white" }, stats.bestRun.quest),
1493
+ /* @__PURE__ */ React4.createElement(Box4, { gap: 3, marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "green" }, formatCost(stats.bestRun.spent)), stats.bestRun.budget ? /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "of $", stats.bestRun.budget.toFixed(2)) : null, /* @__PURE__ */ React4.createElement(Text4, null, bestMc.emoji), /* @__PURE__ */ React4.createElement(Text4, { color: bestTier.color }, bestTier.emoji, " ", bestTier.label))
1494
+ ));
824
1495
  })(), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "Recent runs:"), stats.recentRuns.slice(0, 8).map((run, i) => {
825
1496
  const won = run.status === "won";
826
1497
  const tier = getTier(run.spent);
827
1498
  const mc = getModelClass(run.model);
828
- const pct = getBudgetPct(run.spent, run.budget);
829
- return /* @__PURE__ */ React4.createElement(Box4, { key: i, gap: 2 }, /* @__PURE__ */ React4.createElement(Text4, { color: won ? "green" : "red" }, won ? "\u2713" : "\u2717"), /* @__PURE__ */ React4.createElement(Text4, { color: "white" }, (run.quest || "").slice(0, 34).padEnd(34)), /* @__PURE__ */ React4.createElement(Text4, { color: won ? "green" : "red" }, formatCost(run.spent)), /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "/", formatCost(run.budget)), /* @__PURE__ */ React4.createElement(Text4, null, mc.emoji), /* @__PURE__ */ React4.createElement(Text4, { color: tier.color }, tier.emoji), /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, pct, "%"));
1499
+ const pct = run.budget ? getBudgetPct(run.spent, run.budget) : null;
1500
+ return /* @__PURE__ */ React4.createElement(Box4, { key: i, gap: 2 }, /* @__PURE__ */ React4.createElement(Text4, { color: won ? "green" : "red" }, won ? "\u2713" : "\u2717"), /* @__PURE__ */ React4.createElement(Text4, { color: "white" }, (run.quest || "Flow").slice(0, 34).padEnd(34)), /* @__PURE__ */ React4.createElement(Text4, { color: won ? "green" : "red" }, formatCost(run.spent)), run.budget ? /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "/", formatCost(run.budget)) : null, /* @__PURE__ */ React4.createElement(Text4, null, mc.emoji), /* @__PURE__ */ React4.createElement(Text4, { color: tier.color }, tier.emoji), pct !== null && /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, pct, "%"));
830
1501
  })), stats.achievements.length > 0 && /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "Recent achievements:"), /* @__PURE__ */ React4.createElement(Box4, { flexWrap: "wrap", gap: 1 }, stats.achievements.slice(0, 12).map((a, i) => /* @__PURE__ */ React4.createElement(Box4, { key: i, borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, a.emoji, " ", /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, a.label)))))), /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "q to exit"));
831
1502
  }
832
1503
 
@@ -851,7 +1522,13 @@ program.command("win").description("Mark current run as complete (won)").option(
851
1522
  }
852
1523
  const detected = opts.spent ? null : autoDetectCost(run);
853
1524
  const spent = opts.spent ? parseFloat(opts.spent) : detected?.spent ?? run.spent;
854
- const completed = { ...run, spent, status: "won", modelBreakdown: detected?.modelBreakdown ?? run.modelBreakdown ?? null, endedAt: (/* @__PURE__ */ new Date()).toISOString() };
1525
+ const completed = {
1526
+ ...run,
1527
+ spent,
1528
+ status: "won",
1529
+ modelBreakdown: detected?.modelBreakdown ?? run.modelBreakdown ?? null,
1530
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
1531
+ };
855
1532
  const saved = saveRun(completed);
856
1533
  clearCurrentRun();
857
1534
  render(React5.createElement(ScoreCard, { run: saved }));
@@ -864,7 +1541,13 @@ program.command("bust").description("Mark current run as budget busted (died)").
864
1541
  }
865
1542
  const detected = opts.spent ? null : autoDetectCost(run);
866
1543
  const spent = opts.spent ? parseFloat(opts.spent) : detected?.spent ?? run.budget + 0.01;
867
- const died = { ...run, spent, status: "died", modelBreakdown: detected?.modelBreakdown ?? run.modelBreakdown ?? null, endedAt: (/* @__PURE__ */ new Date()).toISOString() };
1544
+ const died = {
1545
+ ...run,
1546
+ spent,
1547
+ status: "died",
1548
+ modelBreakdown: detected?.modelBreakdown ?? run.modelBreakdown ?? null,
1549
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
1550
+ };
868
1551
  const saved = saveRun(died);
869
1552
  clearCurrentRun();
870
1553
  render(React5.createElement(ScoreCard, { run: saved }));
@@ -890,6 +1573,10 @@ program.command("scorecard").description("Show the last run scorecard").action((
890
1573
  program.command("stats").description("Show career stats dashboard").action(() => {
891
1574
  render(React5.createElement(StatsView, { stats: getStats() }));
892
1575
  });
1576
+ program.command("demo").description("Show HUD examples for all game states (great for screenshots)").action(async () => {
1577
+ const { runDemo: runDemo2 } = await Promise.resolve().then(() => (init_demo(), demo_exports));
1578
+ runDemo2();
1579
+ });
893
1580
  program.command("install").description("Install Claude Code hooks into ~/.claude/settings.json").action(async () => {
894
1581
  const { installHooks: installHooks2 } = await Promise.resolve().then(() => (init_install(), install_exports));
895
1582
  installHooks2();