tokengolf 0.3.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 ADDED
@@ -0,0 +1,897 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/lib/install.js
13
+ var install_exports = {};
14
+ __export(install_exports, {
15
+ installHooks: () => installHooks
16
+ });
17
+ import fs4 from "fs";
18
+ import path4 from "path";
19
+ import os3 from "os";
20
+ function installHooks() {
21
+ console.log("\n\u26F3 TokenGolf \u2014 Installing Claude Code hooks\n");
22
+ let settings = {};
23
+ if (fs4.existsSync(CLAUDE_SETTINGS)) {
24
+ try {
25
+ settings = JSON.parse(fs4.readFileSync(CLAUDE_SETTINGS, "utf8"));
26
+ console.log(" \u2713 Found ~/.claude/settings.json");
27
+ } catch {
28
+ console.log(" \u26A0\uFE0F Could not parse settings.json \u2014 starting fresh");
29
+ }
30
+ } else {
31
+ fs4.mkdirSync(CLAUDE_DIR, { recursive: true });
32
+ console.log(" \u2139\uFE0F Creating ~/.claude/settings.json");
33
+ }
34
+ if (!settings.hooks) settings.hooks = {};
35
+ function upsertHook(event, entry) {
36
+ const existing2 = settings.hooks[event] || [];
37
+ const filtered = existing2.filter(
38
+ (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")
40
+ )
41
+ );
42
+ settings.hooks[event] = [...filtered, { _tg: true, ...entry }];
43
+ }
44
+ if (settings.hooks.Stop) {
45
+ settings.hooks.Stop = (settings.hooks.Stop || []).filter(
46
+ (h) => !h._tg && !h.hooks?.some((e) => e.command?.includes("session-stop.js"))
47
+ );
48
+ if (settings.hooks.Stop.length === 0) delete settings.hooks.Stop;
49
+ }
50
+ upsertHook("SessionStart", {
51
+ hooks: [
52
+ {
53
+ type: "command",
54
+ command: `node ${path4.join(HOOKS_DIR, "session-start.js")}`,
55
+ timeout: 5
56
+ }
57
+ ]
58
+ });
59
+ upsertHook("PostToolUse", {
60
+ matcher: "",
61
+ hooks: [
62
+ {
63
+ type: "command",
64
+ command: `node ${path4.join(HOOKS_DIR, "post-tool-use.js")}`,
65
+ timeout: 5
66
+ }
67
+ ]
68
+ });
69
+ upsertHook("UserPromptSubmit", {
70
+ hooks: [
71
+ {
72
+ type: "command",
73
+ command: `node ${path4.join(HOOKS_DIR, "user-prompt-submit.js")}`,
74
+ timeout: 5
75
+ }
76
+ ]
77
+ });
78
+ upsertHook("SessionEnd", {
79
+ hooks: [
80
+ {
81
+ type: "command",
82
+ command: `node ${path4.join(HOOKS_DIR, "session-end.js")}`,
83
+ timeout: 30
84
+ }
85
+ ]
86
+ });
87
+ upsertHook("PreCompact", {
88
+ hooks: [
89
+ {
90
+ type: "command",
91
+ command: `node ${path4.join(HOOKS_DIR, "pre-compact.js")}`,
92
+ timeout: 5
93
+ }
94
+ ]
95
+ });
96
+ try {
97
+ fs4.chmodSync(STATUSLINE_PATH, 493);
98
+ } catch {
99
+ }
100
+ const existing = settings.statusLine;
101
+ const existingCmd = typeof existing === "string" ? existing : existing?.command ?? null;
102
+ const alreadyOurs = existingCmd === STATUSLINE_PATH || existingCmd === WRAPPER_PATH;
103
+ if (!alreadyOurs && existingCmd) {
104
+ fs4.writeFileSync(
105
+ WRAPPER_PATH,
106
+ [
107
+ "#!/usr/bin/env bash",
108
+ "SESSION_JSON=$(cat)",
109
+ `echo "$SESSION_JSON" | ${existingCmd} 2>/dev/null || true`,
110
+ `echo "$SESSION_JSON" | bash ${STATUSLINE_PATH}`
111
+ ].join("\n") + "\n"
112
+ );
113
+ fs4.chmodSync(WRAPPER_PATH, 493);
114
+ settings.statusLine = {
115
+ type: "command",
116
+ command: WRAPPER_PATH,
117
+ padding: 1
118
+ };
119
+ console.log(
120
+ " \u2713 statusLine \u2192 wrapped your existing statusline + tokengolf HUD"
121
+ );
122
+ } else if (!alreadyOurs) {
123
+ settings.statusLine = {
124
+ type: "command",
125
+ command: STATUSLINE_PATH,
126
+ padding: 1
127
+ };
128
+ console.log(" \u2713 statusLine \u2192 live HUD in every Claude session");
129
+ } else {
130
+ console.log(" \u2713 statusLine \u2192 already installed");
131
+ }
132
+ fs4.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
133
+ console.log(" \u2713 SessionStart \u2192 injects run context into Claude");
134
+ console.log(" \u2713 PostToolUse \u2192 tracks tool calls + 80% budget warning");
135
+ console.log(" \u2713 UserPromptSubmit \u2192 counts prompts + 50% nudge");
136
+ console.log(" \u2713 SessionEnd \u2192 auto-displays scorecard on /exit");
137
+ console.log(
138
+ " \u2713 PreCompact \u2192 tracks compaction events for gear achievements"
139
+ );
140
+ console.log("\n \u2705 Done! Start a run: tokengolf start\n");
141
+ }
142
+ var realEntry, HOOKS_DIR, STATUSLINE_PATH, WRAPPER_PATH, CLAUDE_DIR, CLAUDE_SETTINGS;
143
+ var init_install = __esm({
144
+ "src/lib/install.js"() {
145
+ realEntry = fs4.realpathSync(process.argv[1]);
146
+ HOOKS_DIR = path4.resolve(path4.dirname(realEntry), "../hooks");
147
+ STATUSLINE_PATH = path4.join(HOOKS_DIR, "statusline.sh");
148
+ WRAPPER_PATH = path4.join(HOOKS_DIR, "statusline-wrapper.sh");
149
+ CLAUDE_DIR = path4.join(os3.homedir(), ".claude");
150
+ CLAUDE_SETTINGS = path4.join(CLAUDE_DIR, "settings.json");
151
+ }
152
+ });
153
+
154
+ // src/cli.js
155
+ import { program } from "commander";
156
+ import { render } from "ink";
157
+ import React5 from "react";
158
+
159
+ // src/lib/state.js
160
+ import fs from "fs";
161
+ import path from "path";
162
+ import os from "os";
163
+ var STATE_DIR = path.join(os.homedir(), ".tokengolf");
164
+ var STATE_FILE = path.join(STATE_DIR, "current-run.json");
165
+ function ensureDir() {
166
+ if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
167
+ }
168
+ function getCurrentRun() {
169
+ try {
170
+ return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+ function setCurrentRun(run) {
176
+ ensureDir();
177
+ fs.writeFileSync(STATE_FILE, JSON.stringify(run, null, 2));
178
+ }
179
+ function updateCurrentRun(updates) {
180
+ const run = getCurrentRun();
181
+ if (!run) return null;
182
+ const updated = { ...run, ...updates };
183
+ setCurrentRun(updated);
184
+ return updated;
185
+ }
186
+ function clearCurrentRun() {
187
+ if (fs.existsSync(STATE_FILE)) fs.unlinkSync(STATE_FILE);
188
+ }
189
+
190
+ // src/lib/store.js
191
+ import fs2 from "fs";
192
+ import path2 from "path";
193
+
194
+ // src/lib/score.js
195
+ var BUDGET_TIERS = [
196
+ { label: "Diamond", emoji: "\u{1F48E}", max: 0.1, color: "cyan" },
197
+ { label: "Gold", emoji: "\u{1F947}", max: 0.3, color: "yellow" },
198
+ { label: "Silver", emoji: "\u{1F948}", max: 1, color: "white" },
199
+ { label: "Bronze", emoji: "\u{1F949}", max: 3, color: "yellow" },
200
+ { label: "Reckless", emoji: "\u{1F4B8}", max: Infinity, color: "red" }
201
+ ];
202
+ var EFFORT_LEVELS = {
203
+ low: { label: "Low", emoji: "\u{1FAB6}", color: "green" },
204
+ medium: { label: "Medium", emoji: "\u2696\uFE0F", color: "white" },
205
+ high: { label: "High", emoji: "\u{1F525}", color: "yellow" },
206
+ max: { label: "Max", emoji: "\u{1F4A5}", color: "magenta", opusOnly: true }
207
+ };
208
+ function getEffortLevel(effort) {
209
+ return EFFORT_LEVELS[effort] || null;
210
+ }
211
+ var MODEL_BUDGET_TIERS = {
212
+ haiku: { diamond: 0.15, gold: 0.4, silver: 1, bronze: 2.5 },
213
+ sonnet: { diamond: 0.5, gold: 1.5, silver: 4, bronze: 10 },
214
+ opus: { diamond: 2.5, gold: 7.5, silver: 20, bronze: 50 }
215
+ };
216
+ function getModelBudgets(model) {
217
+ const m = (model || "").toLowerCase();
218
+ if (m.includes("haiku")) return MODEL_BUDGET_TIERS.haiku;
219
+ if (m.includes("opus")) return MODEL_BUDGET_TIERS.opus;
220
+ return MODEL_BUDGET_TIERS.sonnet;
221
+ }
222
+ var MODEL_CLASSES = {
223
+ haiku: {
224
+ name: "Haiku",
225
+ label: "Rogue",
226
+ emoji: "\u{1F3F9}",
227
+ difficulty: "Hard",
228
+ color: "red"
229
+ },
230
+ sonnet: {
231
+ name: "Sonnet",
232
+ label: "Fighter",
233
+ emoji: "\u2694\uFE0F",
234
+ difficulty: "Normal",
235
+ color: "cyan"
236
+ },
237
+ opus: {
238
+ name: "Opus",
239
+ label: "Warlock",
240
+ emoji: "\u{1F9D9}",
241
+ difficulty: "Easy",
242
+ color: "magenta"
243
+ }
244
+ };
245
+ var FLOORS = [
246
+ "Write the code",
247
+ "Write the tests",
248
+ "Fix failing tests",
249
+ "Code review pass",
250
+ "PR merged \u2014 BOSS \u{1F3C6}"
251
+ ];
252
+ function getTier(spent) {
253
+ return BUDGET_TIERS.find((t) => spent <= t.max) || BUDGET_TIERS[BUDGET_TIERS.length - 1];
254
+ }
255
+ function getModelClass(model = "") {
256
+ const key = Object.keys(MODEL_CLASSES).find(
257
+ (k) => model.toLowerCase().includes(k)
258
+ );
259
+ return MODEL_CLASSES[key] || MODEL_CLASSES.sonnet;
260
+ }
261
+ function getEfficiencyRating(spent, budget) {
262
+ const pct = spent / budget;
263
+ if (pct <= 0.25) return { label: "LEGENDARY", emoji: "\u{1F31F}", color: "magenta" };
264
+ if (pct <= 0.5) return { label: "EFFICIENT", emoji: "\u26A1", color: "cyan" };
265
+ if (pct <= 0.75) return { label: "SOLID", emoji: "\u2713", color: "green" };
266
+ if (pct <= 1) return { label: "CLOSE CALL", emoji: "\u{1F605}", color: "yellow" };
267
+ return { label: "BUSTED", emoji: "\u{1F480}", color: "red" };
268
+ }
269
+ function getBudgetPct(spent, budget) {
270
+ return Math.min(Math.round(spent / budget * 100), 999);
271
+ }
272
+ function formatCost(amount = 0) {
273
+ if (amount === 0) return "$0.00";
274
+ if (amount < 0.01) return `$${(amount * 100).toFixed(3)}\xA2`;
275
+ return `$${amount.toFixed(4)}`;
276
+ }
277
+ function formatElapsed(startedAt) {
278
+ if (!startedAt) return "\u2014";
279
+ const ms = Date.now() - new Date(startedAt).getTime();
280
+ const s = Math.floor(ms / 1e3);
281
+ const m = Math.floor(s / 60);
282
+ const h = Math.floor(m / 60);
283
+ if (h > 0) return `${h}h ${m % 60}m`;
284
+ if (m > 0) return `${m}m ${s % 60}s`;
285
+ return `${s}s`;
286
+ }
287
+ function getHaikuPct(modelBreakdown, totalSpent) {
288
+ if (!modelBreakdown || !totalSpent) return null;
289
+ const haikuCost = Object.entries(modelBreakdown).filter(([m]) => m.toLowerCase().includes("haiku")).reduce((sum, [, c]) => sum + c, 0);
290
+ if (haikuCost === 0) return null;
291
+ return Math.round(haikuCost / totalSpent * 100);
292
+ }
293
+ function calculateAchievements(run) {
294
+ const achievements = [];
295
+ const won = run.status === "won";
296
+ const pct = run.budget ? run.spent / run.budget : null;
297
+ const mc = getModelClass(run.model);
298
+ if (run.thinkingInvocations > 0 && run.status === "died")
299
+ achievements.push({
300
+ key: "hubris",
301
+ label: "Hubris \u2014 Used ultrathink, busted anyway",
302
+ emoji: "\u{1F926}"
303
+ });
304
+ if (!won) return achievements;
305
+ if (mc === MODEL_CLASSES.haiku) {
306
+ achievements.push({
307
+ key: "gold_haiku",
308
+ label: "Gold \u2014 Completed with Haiku",
309
+ emoji: "\u{1F947}"
310
+ });
311
+ if (run.spent < 0.1)
312
+ achievements.push({
313
+ key: "diamond",
314
+ label: "Diamond \u2014 Haiku under $0.10",
315
+ emoji: "\u{1F48E}"
316
+ });
317
+ } else if (mc === MODEL_CLASSES.sonnet) {
318
+ achievements.push({
319
+ key: "silver_sonnet",
320
+ label: "Silver \u2014 Completed with Sonnet",
321
+ emoji: "\u{1F948}"
322
+ });
323
+ } else if (mc === MODEL_CLASSES.opus) {
324
+ achievements.push({
325
+ key: "bronze_opus",
326
+ label: "Bronze \u2014 Completed with Opus",
327
+ emoji: "\u{1F949}"
328
+ });
329
+ }
330
+ if (pct !== null) {
331
+ if (pct <= 0.25)
332
+ achievements.push({
333
+ key: "sniper",
334
+ label: "Sniper \u2014 Under 25% of budget",
335
+ emoji: "\u{1F3AF}"
336
+ });
337
+ if (pct <= 0.5)
338
+ achievements.push({
339
+ key: "efficient",
340
+ label: "Efficient \u2014 Under 50% of budget",
341
+ emoji: "\u26A1"
342
+ });
343
+ }
344
+ if (run.spent < 0.1)
345
+ achievements.push({
346
+ key: "penny",
347
+ label: "Penny Pincher \u2014 Under $0.10",
348
+ emoji: "\u{1FA99}"
349
+ });
350
+ if (run.effort) {
351
+ if (run.effort === "low" && pct !== null && pct < 1)
352
+ achievements.push({
353
+ key: "speedrunner",
354
+ label: "Speedrunner \u2014 Low effort, completed under budget",
355
+ emoji: "\u{1F3AF}"
356
+ });
357
+ if ((run.effort === "high" || run.effort === "max") && pct !== null && pct <= 0.25)
358
+ achievements.push({
359
+ key: "tryhard",
360
+ label: "Tryhard \u2014 High effort, LEGENDARY efficiency",
361
+ emoji: "\u{1F4AA}"
362
+ });
363
+ if (run.effort === "max" && mc === MODEL_CLASSES.opus)
364
+ achievements.push({
365
+ key: "archmagus",
366
+ label: "Archmagus \u2014 Opus at max effort, completed",
367
+ emoji: "\u{1F451}"
368
+ });
369
+ }
370
+ if (run.fastMode && mc === MODEL_CLASSES.opus) {
371
+ if (pct !== null && pct < 1)
372
+ achievements.push({
373
+ key: "lightning",
374
+ label: "Lightning Run \u2014 Opus fast mode, completed under budget",
375
+ emoji: "\u26A1"
376
+ });
377
+ if (pct !== null && pct <= 0.25)
378
+ achievements.push({
379
+ key: "daredevil",
380
+ label: "Daredevil \u2014 Opus fast mode, LEGENDARY efficiency",
381
+ emoji: "\u{1F3B0}"
382
+ });
383
+ }
384
+ const sessions = run.sessionCount || 1;
385
+ if (sessions >= 2)
386
+ achievements.push({
387
+ key: "made_camp",
388
+ label: `Made Camp \u2014 Completed across ${sessions} sessions`,
389
+ emoji: "\u{1F3D5}\uFE0F"
390
+ });
391
+ if (sessions === 1)
392
+ achievements.push({
393
+ key: "no_rest",
394
+ label: "No Rest for the Wicked \u2014 Completed in one session",
395
+ emoji: "\u26A1"
396
+ });
397
+ if (run.fainted)
398
+ achievements.push({
399
+ key: "came_back",
400
+ label: "Came Back \u2014 Fainted and finished anyway",
401
+ emoji: "\u{1F4AA}"
402
+ });
403
+ const compactionEvents = run.compactionEvents || [];
404
+ const manualCompactions = compactionEvents.filter(
405
+ (e) => e.trigger === "manual"
406
+ );
407
+ const autoCompactions = compactionEvents.filter((e) => e.trigger === "auto");
408
+ if (autoCompactions.length > 0)
409
+ achievements.push({
410
+ key: "overencumbered",
411
+ label: "Overencumbered \u2014 Context auto-compacted during run",
412
+ emoji: "\u{1F4E6}"
413
+ });
414
+ if (manualCompactions.length > 0) {
415
+ const minPct = Math.min(
416
+ ...manualCompactions.map((e) => e.contextPct ?? 100)
417
+ );
418
+ if (minPct <= 30)
419
+ achievements.push({
420
+ key: "ghost_run",
421
+ label: `Ghost Run \u2014 Manual compact at ${minPct}% context`,
422
+ emoji: "\u{1F977}"
423
+ });
424
+ else if (minPct <= 40)
425
+ achievements.push({
426
+ key: "ultralight",
427
+ label: `Ultralight \u2014 Manual compact at ${minPct}% context`,
428
+ emoji: "\u{1FAB6}"
429
+ });
430
+ else if (minPct <= 50)
431
+ achievements.push({
432
+ key: "traveling_light",
433
+ label: `Traveling Light \u2014 Manual compact at ${minPct}% context`,
434
+ emoji: "\u{1F392}"
435
+ });
436
+ }
437
+ const ti = run.thinkingInvocations;
438
+ if (ti > 0) {
439
+ achievements.push({
440
+ key: "spell_cast",
441
+ label: `Spell Cast \u2014 Used extended thinking (${ti}\xD7)`,
442
+ emoji: "\u{1F52E}"
443
+ });
444
+ if (pct !== null && pct <= 0.25)
445
+ achievements.push({
446
+ key: "calculated_risk",
447
+ label: "Calculated Risk \u2014 Ultrathink + LEGENDARY efficiency",
448
+ emoji: "\u{1F9E0}"
449
+ });
450
+ if (ti >= 3)
451
+ achievements.push({
452
+ key: "deep_thinker",
453
+ label: `Deep Thinker \u2014 ${ti} ultrathink invocations, completed`,
454
+ emoji: "\u{1F300}"
455
+ });
456
+ }
457
+ if (run.thinkingInvocations === 0 && pct !== null && pct <= 0.75)
458
+ achievements.push({
459
+ key: "silent_run",
460
+ label: "Silent Run \u2014 No extended thinking, completed under budget",
461
+ emoji: "\u{1F92B}"
462
+ });
463
+ const haikuPct = getHaikuPct(run.modelBreakdown, run.spent);
464
+ if (haikuPct !== null) {
465
+ if (haikuPct >= 50)
466
+ achievements.push({
467
+ key: "frugal",
468
+ label: `Frugal \u2014 Haiku handled ${haikuPct}% of session cost`,
469
+ emoji: "\u{1F3F9}"
470
+ });
471
+ if (haikuPct >= 75)
472
+ achievements.push({
473
+ key: "rogue_run",
474
+ label: `Rogue Run \u2014 Haiku handled ${haikuPct}% of session cost`,
475
+ emoji: "\u{1F3B2}"
476
+ });
477
+ }
478
+ return achievements;
479
+ }
480
+
481
+ // src/lib/store.js
482
+ var RUNS_FILE = path2.join(STATE_DIR, "runs.json");
483
+ function ensureDir2() {
484
+ if (!fs2.existsSync(STATE_DIR)) fs2.mkdirSync(STATE_DIR, { recursive: true });
485
+ }
486
+ function readRuns() {
487
+ try {
488
+ return JSON.parse(fs2.readFileSync(RUNS_FILE, "utf8"));
489
+ } catch {
490
+ return [];
491
+ }
492
+ }
493
+ function writeRuns(runs) {
494
+ ensureDir2();
495
+ fs2.writeFileSync(RUNS_FILE, JSON.stringify(runs, null, 2));
496
+ }
497
+ function saveRun(run) {
498
+ const runs = readRuns();
499
+ const achievements = calculateAchievements(run);
500
+ const record = {
501
+ id: `run_${Date.now()}`,
502
+ ...run,
503
+ achievements,
504
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
505
+ };
506
+ runs.push(record);
507
+ writeRuns(runs);
508
+ return record;
509
+ }
510
+ function getLastRun() {
511
+ const runs = readRuns();
512
+ return runs.length ? runs[runs.length - 1] : null;
513
+ }
514
+ function getStats() {
515
+ const runs = readRuns().filter((r) => r.status !== "active");
516
+ const wins = runs.filter((r) => r.status === "won");
517
+ const deaths = runs.filter((r) => r.status === "died");
518
+ const avgSpend = wins.length ? wins.reduce((sum, r) => sum + (r.spent || 0), 0) / wins.length : 0;
519
+ 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
+ })));
525
+ return {
526
+ total: runs.length,
527
+ wins: wins.length,
528
+ deaths: deaths.length,
529
+ avgSpend,
530
+ bestRun,
531
+ recentRuns: runs.slice(-10).reverse(),
532
+ achievements: allAchievements.slice(-20).reverse(),
533
+ winRate: runs.length > 0 ? Math.round(wins.length / runs.length * 100) : 0
534
+ };
535
+ }
536
+
537
+ // src/lib/cost.js
538
+ import fs3 from "fs";
539
+ import path3 from "path";
540
+ import os2 from "os";
541
+ var PRICING = {
542
+ "claude-opus-4": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
543
+ "claude-sonnet-4": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
544
+ "claude-haiku-4": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 }
545
+ };
546
+ function getPrice(model) {
547
+ const lower = (model || "").toLowerCase();
548
+ for (const [key, price] of Object.entries(PRICING)) {
549
+ if (lower.includes(key)) return price;
550
+ }
551
+ return PRICING["claude-sonnet-4"];
552
+ }
553
+ function getProjectDir(cwd) {
554
+ return path3.join(os2.homedir(), ".claude", "projects", cwd.replace(/\//g, "-"));
555
+ }
556
+ function parseCostFromTranscript(transcriptPath) {
557
+ try {
558
+ const lines = fs3.readFileSync(transcriptPath, "utf8").trim().split("\n");
559
+ let total = 0;
560
+ const byModel = {};
561
+ for (const line of lines) {
562
+ try {
563
+ const entry = JSON.parse(line);
564
+ if (entry.type === "assistant" && entry.message?.usage && entry.message?.model) {
565
+ const model = entry.message.model;
566
+ const p = getPrice(model);
567
+ const u = entry.message.usage;
568
+ const cost = (u.input_tokens || 0) / 1e6 * p.input + (u.output_tokens || 0) / 1e6 * p.output + (u.cache_creation_input_tokens || 0) / 1e6 * p.cacheWrite + (u.cache_read_input_tokens || 0) / 1e6 * p.cacheRead;
569
+ total += cost;
570
+ byModel[model] = (byModel[model] || 0) + cost;
571
+ }
572
+ } catch {
573
+ }
574
+ }
575
+ return total > 0 ? { total, byModel } : null;
576
+ } catch {
577
+ return null;
578
+ }
579
+ }
580
+ function findTranscriptsSince(projectDir, sinceMs) {
581
+ 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);
583
+ } catch {
584
+ return [];
585
+ }
586
+ }
587
+ function parseThinkingFromTranscripts(paths) {
588
+ let invocations = 0;
589
+ let tokens = 0;
590
+ for (const p of paths) {
591
+ try {
592
+ const lines = fs3.readFileSync(p, "utf8").trim().split("\n");
593
+ for (const line of lines) {
594
+ try {
595
+ const entry = JSON.parse(line);
596
+ if (entry.type === "assistant" && Array.isArray(entry.message?.content)) {
597
+ const thinkBlocks = entry.message.content.filter((b) => b.type === "thinking");
598
+ if (thinkBlocks.length > 0) {
599
+ invocations++;
600
+ for (const block of thinkBlocks) {
601
+ tokens += Math.round((block.thinking?.length || 0) / 4);
602
+ }
603
+ }
604
+ }
605
+ } catch {
606
+ }
607
+ }
608
+ } catch {
609
+ }
610
+ }
611
+ return invocations > 0 ? { thinkingInvocations: invocations, thinkingTokens: tokens } : null;
612
+ }
613
+ function parseAllTranscripts(paths) {
614
+ let total = 0;
615
+ const byModel = {};
616
+ for (const p of paths) {
617
+ const result = parseCostFromTranscript(p);
618
+ if (!result) continue;
619
+ total += result.total;
620
+ for (const [model, cost] of Object.entries(result.byModel)) {
621
+ byModel[model] = (byModel[model] || 0) + cost;
622
+ }
623
+ }
624
+ return total > 0 ? { total, byModel } : null;
625
+ }
626
+ function findTranscript(sessionId, projectDir) {
627
+ if (sessionId) {
628
+ try {
629
+ const p = path3.join(projectDir, `${sessionId}.jsonl`);
630
+ fs3.accessSync(p);
631
+ return p;
632
+ } catch {
633
+ }
634
+ }
635
+ try {
636
+ const files = fs3.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: fs3.statSync(path3.join(projectDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
637
+ return files.length ? path3.join(projectDir, files[0].f) : null;
638
+ } catch {
639
+ return null;
640
+ }
641
+ }
642
+ function autoDetectCost(run) {
643
+ const projectDir = getProjectDir(process.cwd());
644
+ const sinceMs = run.startedAt ? new Date(run.startedAt).getTime() : 0;
645
+ const paths = sinceMs > 0 ? findTranscriptsSince(projectDir, sinceMs) : [findTranscript(run.sessionId, projectDir)].filter(Boolean);
646
+ const parsed = paths.length > 0 ? parseAllTranscripts(paths) : null;
647
+ const spent = parsed?.total ?? (run.spent > 0 ? run.spent : null);
648
+ if (spent === null) return null;
649
+ const modelBreakdown = parsed?.byModel ?? run.modelBreakdown ?? null;
650
+ const thinking = parseThinkingFromTranscripts(paths);
651
+ return {
652
+ spent,
653
+ modelBreakdown,
654
+ thinkingInvocations: thinking?.thinkingInvocations ?? 0,
655
+ thinkingTokens: thinking?.thinkingTokens ?? 0
656
+ };
657
+ }
658
+
659
+ // src/components/StartRun.js
660
+ import React, { useState } from "react";
661
+ import { Box, Text, useApp } from "ink";
662
+ import { TextInput, Select, ConfirmInput } from "@inkjs/ui";
663
+ var MODEL_OPTIONS = [
664
+ { label: "\u2694\uFE0F Sonnet \u2014 Balanced. The default run. [Normal]", value: "claude-sonnet-4-6" },
665
+ { label: "\u{1F3F9} Haiku \u2014 Glass cannon. Hard mode. [Hard]", value: "claude-haiku-4-5-20251001" },
666
+ { label: "\u{1F9D9} Opus \u2014 Powerful but expensive. [Easy]", value: "claude-opus-4-6" }
667
+ ];
668
+ var EFFORT_OPTIONS_BASE = [
669
+ { label: "\u2696\uFE0F Medium \u2014 Balanced (Anthropic recommended for Sonnet)", value: "medium" },
670
+ { label: "\u{1FAB6} Low \u2014 Fewest tokens, fastest, cheapest", value: "low" },
671
+ { label: "\u{1F525} High \u2014 Most thorough, costs more", value: "high" }
672
+ ];
673
+ var EFFORT_OPTIONS_OPUS = [
674
+ ...EFFORT_OPTIONS_BASE,
675
+ { label: "\u{1F4A5} Max \u2014 Absolute max, no token constraints (Opus only)", value: "max" }
676
+ ];
677
+ var getEffortOptions = (model) => model.toLowerCase().includes("opus") ? EFFORT_OPTIONS_OPUS : EFFORT_OPTIONS_BASE;
678
+ function getBudgetOptions(model) {
679
+ const b = getModelBudgets(model);
680
+ return [
681
+ { label: `\u{1F48E} Diamond \u2014 $${b.diamond.toFixed(2)} surgical micro-task`, value: String(b.diamond) },
682
+ { label: `\u{1F947} Gold \u2014 $${b.gold.toFixed(2)} focused small task`, value: String(b.gold) },
683
+ { label: `\u{1F948} Silver \u2014 $${b.silver.toFixed(2)} medium task`, value: String(b.silver) },
684
+ { label: `\u{1F949} Bronze \u2014 $${b.bronze.toFixed(2)} heavy / complex`, value: String(b.bronze) },
685
+ { label: `\u270F\uFE0F Custom \u2014 set your own`, value: "custom" }
686
+ ];
687
+ }
688
+ function StartRun() {
689
+ const { exit } = useApp();
690
+ const [step, setStep] = useState("quest");
691
+ const [quest, setQuest] = useState("");
692
+ const [model, setModel] = useState("");
693
+ const [effort, setEffort] = useState("medium");
694
+ const [budgetVal, setBudgetVal] = useState("");
695
+ const budget = parseFloat(budgetVal) || 0;
696
+ 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,
726
+ {
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();
743
+ },
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."));
747
+ }
748
+
749
+ // src/components/ActiveRun.js
750
+ import React2, { useState as useState2, useEffect } from "react";
751
+ import { Box as Box2, Text as Text2, useApp as useApp2, useInput } from "ink";
752
+ import { ProgressBar } from "@inkjs/ui";
753
+ function ActiveRun({ run: initialRun }) {
754
+ const { exit } = useApp2();
755
+ const [run, setRun] = useState2(initialRun);
756
+ const [tick, setTick] = useState2(0);
757
+ useEffect(() => {
758
+ const interval = setInterval(() => {
759
+ const latest = getCurrentRun();
760
+ if (latest) setRun(latest);
761
+ setTick((t) => t + 1);
762
+ }, 2e3);
763
+ return () => clearInterval(interval);
764
+ }, []);
765
+ useInput((input) => {
766
+ if (input === "q") exit();
767
+ });
768
+ 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"));
778
+ }
779
+
780
+ // src/components/ScoreCard.js
781
+ import React3, { useEffect as useEffect2 } from "react";
782
+ import { Box as Box3, Text as Text3, useApp as useApp3, useInput as useInput2 } from "ink";
783
+ function ScoreCard({ run }) {
784
+ const { exit } = useApp3();
785
+ const won = run.status === "won";
786
+ useInput2((input) => {
787
+ if (input === "q") exit();
788
+ });
789
+ useEffect2(() => {
790
+ const t = setTimeout(() => exit(), 6e4);
791
+ return () => clearTimeout(t);
792
+ }, [exit]);
793
+ const tier = getTier(run.spent);
794
+ const mc = getModelClass(run.model);
795
+ const flowMode = !run.budget;
796
+ const efficiency = flowMode ? null : getEfficiencyRating(run.spent, run.budget);
797
+ const pct = flowMode ? null : getBudgetPct(run.spent, run.budget);
798
+ 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")));
807
+ }
808
+
809
+ // src/components/StatsView.js
810
+ import React4 from "react";
811
+ import { Box as Box4, Text as Text4, useApp as useApp4, useInput as useInput3 } from "ink";
812
+ function StatsView({ stats }) {
813
+ const { exit } = useApp4();
814
+ useInput3((input) => {
815
+ if (input === "q") exit();
816
+ });
817
+ if (stats.total === 0) {
818
+ return /* @__PURE__ */ React4.createElement(Box4, { paddingX: 1, paddingY: 1, flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "yellow" }, "\u26F3 TokenGolf Stats"), /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "No completed runs yet."), /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "Start one: ", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, "tokengolf start")));
819
+ }
820
+ 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
+ const bestTier = getTier(stats.bestRun.spent);
822
+ 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))));
824
+ })(), /* @__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
+ const won = run.status === "won";
826
+ const tier = getTier(run.spent);
827
+ 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, "%"));
830
+ })), 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
+ }
832
+
833
+ // src/cli.js
834
+ program.name("tokengolf").description("\u26F3 Gamify your Claude Code sessions").version("0.1.0");
835
+ program.command("start").description("Declare a quest and start a new run").action(() => {
836
+ render(React5.createElement(StartRun));
837
+ });
838
+ program.command("status").description("Show current run status").action(() => {
839
+ const run = getCurrentRun();
840
+ if (!run) {
841
+ console.log("No active run. Start one with: tokengolf start");
842
+ process.exit(0);
843
+ }
844
+ render(React5.createElement(ActiveRun, { run }));
845
+ });
846
+ program.command("win").description("Mark current run as complete (won)").option("--spent <amount>", "How much did you spend? (e.g. 0.18)").action((opts) => {
847
+ const run = getCurrentRun();
848
+ if (!run) {
849
+ console.log("No active run.");
850
+ process.exit(1);
851
+ }
852
+ const detected = opts.spent ? null : autoDetectCost(run);
853
+ 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() };
855
+ const saved = saveRun(completed);
856
+ clearCurrentRun();
857
+ render(React5.createElement(ScoreCard, { run: saved }));
858
+ });
859
+ program.command("bust").description("Mark current run as budget busted (died)").option("--spent <amount>", "How much did you spend? (e.g. 0.45)").action((opts) => {
860
+ const run = getCurrentRun();
861
+ if (!run) {
862
+ console.log("No active run.");
863
+ process.exit(1);
864
+ }
865
+ const detected = opts.spent ? null : autoDetectCost(run);
866
+ 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() };
868
+ const saved = saveRun(died);
869
+ clearCurrentRun();
870
+ render(React5.createElement(ScoreCard, { run: saved }));
871
+ });
872
+ program.command("floor").description("Advance to the next floor").action(() => {
873
+ const run = getCurrentRun();
874
+ if (!run) {
875
+ console.log("No active run.");
876
+ process.exit(1);
877
+ }
878
+ const nextFloor = Math.min((run.floor || 1) + 1, run.totalFloors || 5);
879
+ updateCurrentRun({ floor: nextFloor });
880
+ console.log(`Floor ${nextFloor} / ${run.totalFloors}`);
881
+ });
882
+ program.command("scorecard").description("Show the last run scorecard").action(() => {
883
+ const run = getLastRun();
884
+ if (!run) {
885
+ console.log("No runs yet. Start one with: tokengolf start");
886
+ process.exit(0);
887
+ }
888
+ render(React5.createElement(ScoreCard, { run }));
889
+ });
890
+ program.command("stats").description("Show career stats dashboard").action(() => {
891
+ render(React5.createElement(StatsView, { stats: getStats() }));
892
+ });
893
+ program.command("install").description("Install Claude Code hooks into ~/.claude/settings.json").action(async () => {
894
+ const { installHooks: installHooks2 } = await Promise.resolve().then(() => (init_install(), install_exports));
895
+ installHooks2();
896
+ });
897
+ program.parse();