tokengolf 1.0.5 → 1.1.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.
@@ -11,7 +11,7 @@
11
11
  "name": "tokengolf",
12
12
  "source": "./plugin",
13
13
  "description": "Gamify your Claude Code sessions — track token efficiency, earn achievements, level up your prompting.",
14
- "version": "1.0.5",
14
+ "version": "1.1.0",
15
15
  "homepage": "https://josheche.github.io/tokengolf/",
16
16
  "license": "MIT"
17
17
  }
package/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ TokenGolf patch notes — what changed, what it measures, and why the mechanic e
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ---
10
+
11
+ ## [1.1.0] — 2026-03-18
12
+
13
+ ### Added
14
+ - **3-line statusline HUD** — New top line shows efficiency rating + project name + git branch/dirty status (`📂 myapp ⎇ main ✓`). Rating moved from line 1 to its own line for cleaner layout.
15
+ - **Per-project state isolation** — Run state is now keyed by working directory (`current-run-{cwd}.json`). Concurrent Claude Code sessions in different repos no longer collide. Migration from global `current-run.json` is automatic.
16
+ - **Stable statusline path** — `statusline.sh` is copied to `~/.tokengolf/statusline.sh` on every session start. Survives plugin uninstall and npm removal (exits cleanly with no active run). Wrapper script also writes to stable path.
17
+ - **python3 availability guard** — `statusline.sh` checks for python3 before invoking it. Graceful silent exit on systems without python3.
18
+ - **Plugin build in CI pipeline** — `npm run prepare` now includes `build:plugin` so the plugin bundle can't ship stale.
19
+
20
+ ### Fixed
21
+ - **npm-only auto-sync guard** — Early bail via `fs.existsSync(pkgPath)` so plugin users skip 50 lines of dead auto-sync code on every session start.
22
+ - **install.js error handling** — `copyFileSync` and version stamp wrapped in try-catch with user-facing warnings instead of raw stack traces.
23
+
9
24
  ### Changed
10
25
  - **Sublinear par scaling (sqrt)** — Par formula changed from `prompts × rate` to `rate × sqrt(prompts)`. Early prompts have headroom for exploration; pressure builds as the session goes on. Long wasteful sessions bust. Rates recalibrated: Haiku $0.55, Sonnet $7.00, Paladin $22.00, Opus $45.00. Floors unchanged. All models bust around 20 prompts at typical per-prompt spend.
11
26
  - **Design D HUD** — StatusLine HUD redesigned with `██` accent bar, inline `▓░` progress bars for budget and context, no separator lines. 1 line when context <50%, 2 lines when context visible. Accent bar turns red when budget >75%. Matches Design D across all UI surfaces.
package/README.md CHANGED
@@ -289,17 +289,21 @@ Thresholds scale per model — Haiku Diamond is $0.15, Opus Diamond is $2.50. Sa
289
289
 
290
290
  ## Live HUD
291
291
 
292
- After install, a status line appears in every Claude Code session showing cost, efficiency, context load, model class, and emotion.
292
+ After install, a 3-line status bar appears in every Claude Code session showing efficiency rating, project info, cost, emotion, model, and context load.
293
293
 
294
294
  ```
295
- ██ 😎 VIBING 💎 $0.42/9.90 ▓░░░░░░░░░░ 4% 🌟 LEGENDARY
295
+ ██ 🌟 LEGENDARY 📂 myapp ⎇ main
296
+ ██ 😎 VIBING 💎 $0.42/9.90 ▓░░░░░░░░░░ 4%
296
297
  ██ ⚔️ Sonnet 🪶 ▓░░░░░░░░░ 8%
297
298
 
298
- ██ 😤 GRINDING 🥉 $6.80/19.80 ▓▓▓▓░░░░░░░ 34% 💪 PRO
299
+ ██ 💪 PRO 📂 api-server ⎇ feat/auth
300
+ ██ 😤 GRINDING 🥉 $6.80/19.80 ▓▓▓▓░░░░░░░ 34%
299
301
  ██ ⚔️ Sonnet 📚 ▓▓▓░░░░░░░ 34%
300
302
  ```
301
303
 
302
- Accent `██` color matches your efficiency tier yellow for LEGENDARY, magenta for EPIC, cyan for PRO, green for SOLID, white for CLOSE CALL, red for BUST. Line 2 shows model class, context weight (**🪶** · **📚** · **🎒** · **🧱** · **🪨** · **🗿** — feather to stone), and prompt count. **💤** replaces the emotion icon when fainted.
304
+ **Line 1**: Efficiency rating + project name + git branch (✓ clean, dirty). **Line 2**: Emotion + cost vs par + progress bar. **Line 3**: Model class + context weight (**🪶** · **📚** · **🎒** · **🧱** · **🪨** · **🗿** — feather to stone). Accent `██` color matches your efficiency tier. **💤** replaces the emotion icon when fainted.
305
+
306
+ Emotions are a composite signal: par %, context %, failed tools, prompt count. From 😎 VIBING through 😤 GRINDING to 🧟 ZOMBIE (past bust). Three display modes: `tokengolf config emotions emoji` (default), `ascii` (kaomoji + label on a 4th line), or `off`.
303
307
 
304
308
  ---
305
309
 
@@ -407,8 +411,11 @@ Installed automatically via plugin, or manually via `tokengolf install` (npm):
407
411
 
408
412
  All data lives in `~/.tokengolf/`:
409
413
 
410
- - `current-run.json` — active run
411
- - `runs.json` — completed run history
414
+ - `current-run-{cwd}.json` — active run (per-project, so concurrent sessions in different repos don't collide)
415
+ - `session-cost-{cwd}` — cost sidecar from statusline (per-project)
416
+ - `runs.json` — completed run history (shared)
417
+ - `config.json` — user preferences (emotion mode, custom par rates)
418
+ - `statusline.sh` — stable copy of the HUD script (survives plugin/npm uninstall)
412
419
 
413
420
  No database, no native deps, no compilation.
414
421
 
package/dist/cli.js CHANGED
@@ -144,11 +144,12 @@ var init_score = __esm({
144
144
  // src/lib/state.js
145
145
  import path from "path";
146
146
  import os from "os";
147
- var STATE_DIR, STATE_FILE;
147
+ var STATE_DIR, cwdKey, STATE_FILE;
148
148
  var init_state = __esm({
149
149
  "src/lib/state.js"() {
150
150
  STATE_DIR = path.join(os.homedir(), ".tokengolf");
151
- STATE_FILE = path.join(STATE_DIR, "current-run.json");
151
+ cwdKey = (process.env.PWD || process.cwd()).replace(/\//g, "-");
152
+ STATE_FILE = path.join(STATE_DIR, `current-run${cwdKey}.json`);
152
153
  }
153
154
  });
154
155
 
@@ -367,7 +368,18 @@ function getPar(modelName, promptCount) {
367
368
  PAR_FLOORS[modelName] || 3
368
369
  );
369
370
  }
370
- function hudLine({ model, cost, prompts, ctxPct, effort, fainted, emotionKey }) {
371
+ function hudLine({
372
+ model,
373
+ cost,
374
+ prompts,
375
+ ctxPct,
376
+ effort,
377
+ fainted,
378
+ emotionKey,
379
+ project,
380
+ gitBranch,
381
+ gitDirty
382
+ }) {
371
383
  const m = (model || "").toLowerCase();
372
384
  let modelName, modelEmoji;
373
385
  if (m.includes("haiku")) {
@@ -432,7 +444,6 @@ function hudLine({ model, cost, prompts, ctxPct, effort, fainted, emotionKey })
432
444
  const barEmpty = barW - barFilled;
433
445
  const bar = `${accent}${"\u2593".repeat(barFilled)}${"\u2591".repeat(barEmpty)}${RESET}`;
434
446
  const costStr = `${tierEmoji} ${DIM}$${RESET}${cost.toFixed(2)}${DIM}/${par.toFixed(2)}${RESET} ${bar} ${pct.toFixed(0)}%`;
435
- const ratingStr = ` ${effEmoji} ${rc}${rating}${RESET}`;
436
447
  const ctxPctVal = ctxPct != null ? ctxPct : 0;
437
448
  const ctxW = 10;
438
449
  const ctxFilled = Math.min(ctxW, Math.round(ctxPctVal / 100 * ctxW));
@@ -465,10 +476,14 @@ function hudLine({ model, cost, prompts, ctxPct, effort, fainted, emotionKey })
465
476
  } else {
466
477
  emotionDisplay = `${G}VIBING${RESET}`;
467
478
  }
468
- const line1 = ` ${accent}\u2588\u2588${RESET} ${icon} ${emotionDisplay} ${costStr}${ratingStr}`;
469
- const promptStr = (prompts || 0) > 0 ? ` \u{1F4AC} ${prompts}p` : "";
470
- const line2 = ` ${accent}\u2588\u2588${RESET} ${modelLabel} ${ctxIcon} ${ctxBar} ${ctxPctVal}%${promptStr}`;
471
- return `${line1}
479
+ const line1 = ` ${accent}\u2588\u2588${RESET} ${icon} ${emotionDisplay} ${costStr}`;
480
+ const line2 = ` ${accent}\u2588\u2588${RESET} ${modelLabel} ${ctxIcon} ${ctxBar} ${ctxPctVal}%`;
481
+ const projName = project || "myproject";
482
+ const branch = gitBranch || "main";
483
+ const dirtyIcon = gitDirty ? `${Y}\u25CF${RESET}` : `${G}\u2713${RESET}`;
484
+ const line3 = ` ${accent}\u2588\u2588${RESET} ${effEmoji} ${rc}${rating}${RESET} ${DIM}\u{1F4C2}${RESET} ${projName} ${DIM}\u2387${RESET} ${branch} ${dirtyIcon}`;
485
+ return `${line3}
486
+ ${line1}
472
487
  ${line2}`;
473
488
  }
474
489
  function runDemo() {
@@ -517,79 +532,117 @@ var init_demo = __esm({
517
532
  };
518
533
  SCENARIOS = [
519
534
  {
520
- title: "Fresh session \xB7 Sonnet \xB7 2 prompts \xB7 VIBING",
521
- hud: { model: "claude-sonnet-4-6", cost: 0.42, prompts: 2, ctxPct: 8, emotionKey: "VIBING" }
535
+ title: "Fresh session \xB7 Sonnet \xB7 2 prompts \xB7 LEGENDARY",
536
+ // par = 1.5*sqrt(2) = $2.12, cost $0.18 8% = LEGENDARY
537
+ hud: {
538
+ model: "claude-sonnet-4-6",
539
+ cost: 0.18,
540
+ prompts: 2,
541
+ ctxPct: 8,
542
+ emotionKey: "VIBING",
543
+ project: "myapp",
544
+ gitBranch: "main",
545
+ gitDirty: false
546
+ }
522
547
  },
523
548
  {
524
- title: "Sonnet \xB7 8 prompts \xB7 efficient \xB7 GRINDING",
549
+ title: "Sonnet \xB7 8 prompts \xB7 efficient \xB7 PRO",
550
+ // par = 1.5*sqrt(8) = $4.24, cost $1.50 → 35% = PRO
525
551
  hud: {
526
552
  model: "claude-sonnet-4-6",
527
- cost: 6.8,
553
+ cost: 1.5,
528
554
  prompts: 8,
529
555
  ctxPct: 34,
530
- emotionKey: "GRINDING"
556
+ emotionKey: "GRINDING",
557
+ project: "api-server",
558
+ gitBranch: "feat/auth",
559
+ gitDirty: true
531
560
  }
532
561
  },
533
562
  {
534
563
  title: "Sonnet\xB7High \xB7 5 prompts \xB7 LEGENDARY",
564
+ // par = 1.5*sqrt(5) = $3.35, cost $0.41 → 12% = LEGENDARY
535
565
  hud: {
536
566
  model: "claude-sonnet-4-6",
537
567
  cost: 0.41,
538
568
  prompts: 5,
539
569
  ctxPct: 29,
540
570
  effort: "high",
541
- emotionKey: "VIBING"
571
+ emotionKey: "VIBING",
572
+ project: "tokengolf",
573
+ gitBranch: "main",
574
+ gitDirty: false
542
575
  }
543
576
  },
544
577
  {
545
578
  title: "Opus \xB7 4 prompts \xB7 EPIC",
579
+ // par = 8.0*sqrt(4) = $16.00, cost $3.80 → 24% = EPIC
546
580
  hud: {
547
581
  model: "claude-opus-4-6",
548
- cost: 18,
582
+ cost: 3.8,
549
583
  prompts: 4,
550
584
  ctxPct: 52,
551
- emotionKey: "CRUISING"
585
+ emotionKey: "CRUISING",
586
+ project: "ml-pipeline",
587
+ gitBranch: "refactor/v2",
588
+ gitDirty: false
552
589
  }
553
590
  },
554
591
  {
555
- title: "Haiku \xB7 20 prompts \xB7 CLOSE CALL",
592
+ title: "Haiku \xB7 12 prompts \xB7 CLOSE CALL",
593
+ // par = 0.15*sqrt(12) = $0.52, cost $0.45 → 87% = CLOSE CALL
556
594
  hud: {
557
595
  model: "claude-haiku-4-5-20251001",
558
- cost: 2.1,
559
- prompts: 20,
596
+ cost: 0.45,
597
+ prompts: 12,
560
598
  ctxPct: 78,
561
- emotionKey: "SWEATING"
599
+ emotionKey: "SWEATING",
600
+ project: "docs-site",
601
+ gitBranch: "fix/typos",
602
+ gitDirty: true
562
603
  }
563
604
  },
564
605
  {
565
- title: "Sonnet \xB7 10 prompts \xB7 BUSTED",
606
+ title: "Sonnet \xB7 10 prompts \xB7 BUST",
607
+ // par = 1.5*sqrt(10) = $4.74, cost $6.20 → 131% = BUST
566
608
  hud: {
567
609
  model: "claude-sonnet-4-6",
568
- cost: 34.24,
610
+ cost: 6.2,
569
611
  prompts: 10,
570
612
  ctxPct: 45,
571
- emotionKey: "ZOMBIE"
613
+ emotionKey: "ZOMBIE",
614
+ project: "monorepo",
615
+ gitBranch: "main",
616
+ gitDirty: true
572
617
  }
573
618
  },
574
619
  {
575
620
  title: "Opus \xB7 3 prompts \xB7 overencumbered context",
621
+ // par = 8.0*sqrt(3) = $13.86, cost $5.50 → 40% = PRO
576
622
  hud: {
577
623
  model: "claude-opus-4-6",
578
- cost: 28,
624
+ cost: 5.5,
579
625
  prompts: 3,
580
626
  ctxPct: 91,
581
- emotionKey: "FOCUSED"
627
+ emotionKey: "FOCUSED",
628
+ project: "kernel",
629
+ gitBranch: "dev",
630
+ gitDirty: false
582
631
  }
583
632
  },
584
633
  {
585
634
  title: "Fainted \u{1F4A4} \u2014 usage limit hit, run resumes next session",
635
+ // par = 1.5*sqrt(6) = $3.67, cost $0.92 → 25% = EPIC
586
636
  hud: {
587
637
  model: "claude-sonnet-4-6",
588
- cost: 4.22,
638
+ cost: 0.92,
589
639
  prompts: 6,
590
640
  ctxPct: 67,
591
641
  fainted: true,
592
- emotionKey: "SLEEPING"
642
+ emotionKey: "SLEEPING",
643
+ project: "webapp",
644
+ gitBranch: "feat/deploy",
645
+ gitDirty: false
593
646
  }
594
647
  }
595
648
  ];
@@ -1293,13 +1346,17 @@ function installHooks() {
1293
1346
  }
1294
1347
  ]
1295
1348
  });
1349
+ if (!fs3.existsSync(TG_DIR)) fs3.mkdirSync(TG_DIR, { recursive: true });
1296
1350
  try {
1297
- fs3.chmodSync(STATUSLINE_PATH, 493);
1298
- } catch {
1351
+ fs3.copyFileSync(SRC_STATUSLINE_PATH, STABLE_STATUSLINE);
1352
+ fs3.chmodSync(STABLE_STATUSLINE, 493);
1353
+ } catch (err) {
1354
+ console.log(` \u26A0\uFE0F Could not copy statusline.sh: ${err.message}`);
1355
+ console.log(" The HUD may not work. Try reinstalling tokengolf.");
1299
1356
  }
1300
1357
  const existing = settings.statusLine;
1301
1358
  const existingCmd = typeof existing === "string" ? existing : existing?.command ?? null;
1302
- const isTgStatusline = (cmd) => cmd && (cmd.includes("tokengolf/hooks/statusline") || cmd.includes("tokengolf\\hooks\\statusline"));
1359
+ const isTgStatusline = (cmd) => cmd && (cmd.includes("tokengolf/hooks/statusline") || cmd.includes("tokengolf\\hooks\\statusline") || cmd.includes(".tokengolf/statusline"));
1303
1360
  const alreadyOurs = isTgStatusline(existingCmd);
1304
1361
  function extractUserStatusline(wrapperPath, visited = /* @__PURE__ */ new Set()) {
1305
1362
  if (!wrapperPath || visited.has(wrapperPath)) return null;
@@ -1325,64 +1382,63 @@ function installHooks() {
1325
1382
  const userStatusline = existingCmd.includes("statusline-wrapper") ? extractUserStatusline(existingCmd) : null;
1326
1383
  if (userStatusline) {
1327
1384
  fs3.writeFileSync(
1328
- WRAPPER_PATH,
1385
+ STABLE_WRAPPER,
1329
1386
  [
1330
1387
  "#!/usr/bin/env bash",
1331
1388
  "SESSION_JSON=$(cat)",
1332
1389
  `echo "$SESSION_JSON" | ${userStatusline} 2>/dev/null || true`,
1333
- `echo "$SESSION_JSON" | bash ${STATUSLINE_PATH}`
1390
+ `echo "$SESSION_JSON" | bash ${STABLE_STATUSLINE}`
1334
1391
  ].join("\n") + "\n"
1335
1392
  );
1336
- fs3.chmodSync(WRAPPER_PATH, 493);
1393
+ fs3.chmodSync(STABLE_WRAPPER, 493);
1337
1394
  settings.statusLine = {
1338
1395
  type: "command",
1339
- command: WRAPPER_PATH,
1396
+ command: STABLE_WRAPPER,
1340
1397
  padding: existing?.padding ?? 1
1341
1398
  };
1342
1399
  console.log(" \u2713 statusLine \u2192 updated paths (kept your existing statusline)");
1343
1400
  } else {
1344
1401
  settings.statusLine = {
1345
1402
  type: "command",
1346
- command: STATUSLINE_PATH,
1403
+ command: STABLE_STATUSLINE,
1347
1404
  padding: existing?.padding ?? 1
1348
1405
  };
1349
1406
  console.log(" \u2713 statusLine \u2192 updated to current install path");
1350
1407
  }
1351
1408
  } else if (existingCmd) {
1352
1409
  fs3.writeFileSync(
1353
- WRAPPER_PATH,
1410
+ STABLE_WRAPPER,
1354
1411
  [
1355
1412
  "#!/usr/bin/env bash",
1356
1413
  "SESSION_JSON=$(cat)",
1357
1414
  `echo "$SESSION_JSON" | ${existingCmd} 2>/dev/null || true`,
1358
- `echo "$SESSION_JSON" | bash ${STATUSLINE_PATH}`
1415
+ `echo "$SESSION_JSON" | bash ${STABLE_STATUSLINE}`
1359
1416
  ].join("\n") + "\n"
1360
1417
  );
1361
- fs3.chmodSync(WRAPPER_PATH, 493);
1418
+ fs3.chmodSync(STABLE_WRAPPER, 493);
1362
1419
  settings.statusLine = {
1363
1420
  type: "command",
1364
- command: WRAPPER_PATH,
1421
+ command: STABLE_WRAPPER,
1365
1422
  padding: 1
1366
1423
  };
1367
1424
  console.log(" \u2713 statusLine \u2192 wrapped your existing statusline + tokengolf HUD");
1368
1425
  } else {
1369
1426
  settings.statusLine = {
1370
1427
  type: "command",
1371
- command: STATUSLINE_PATH,
1428
+ command: STABLE_STATUSLINE,
1372
1429
  padding: 1
1373
1430
  };
1374
1431
  console.log(" \u2713 statusLine \u2192 live HUD in every Claude session");
1375
1432
  }
1376
1433
  fs3.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
1377
- const TG_DIR = path4.join(os2.homedir(), ".tokengolf");
1378
- if (!fs3.existsSync(TG_DIR)) fs3.mkdirSync(TG_DIR, { recursive: true });
1379
1434
  try {
1380
1435
  const pkgVersion = JSON.parse(
1381
1436
  fs3.readFileSync(path4.resolve(path4.dirname(realEntry), "../package.json"), "utf8")
1382
1437
  ).version;
1383
1438
  fs3.writeFileSync(path4.join(TG_DIR, "installed-version"), pkgVersion);
1384
1439
  console.log(` \u2713 installed-version \u2192 ${pkgVersion}`);
1385
- } catch {
1440
+ } catch (err) {
1441
+ console.log(` \u26A0\uFE0F Could not stamp installed version: ${err.message}`);
1386
1442
  }
1387
1443
  const CONFIG_FILE2 = path4.join(TG_DIR, "config.json");
1388
1444
  if (!fs3.existsSync(CONFIG_FILE2)) {
@@ -1400,13 +1456,15 @@ function installHooks() {
1400
1456
  console.log(" \u2713 Stop \u2192 tracks turn count");
1401
1457
  console.log("\n \u2705 Done! Start a run: tokengolf start\n");
1402
1458
  }
1403
- var realEntry, HOOKS_DIR, STATUSLINE_PATH, WRAPPER_PATH, CLAUDE_DIR, CLAUDE_SETTINGS;
1459
+ var realEntry, HOOKS_DIR, SRC_STATUSLINE_PATH, TG_DIR, STABLE_STATUSLINE, STABLE_WRAPPER, CLAUDE_DIR, CLAUDE_SETTINGS;
1404
1460
  var init_install = __esm({
1405
1461
  "src/lib/install.js"() {
1406
1462
  realEntry = fs3.realpathSync(process.argv[1]);
1407
1463
  HOOKS_DIR = path4.resolve(path4.dirname(realEntry), "../hooks");
1408
- STATUSLINE_PATH = path4.join(HOOKS_DIR, "statusline.sh");
1409
- WRAPPER_PATH = path4.join(HOOKS_DIR, "statusline-wrapper.sh");
1464
+ SRC_STATUSLINE_PATH = path4.join(HOOKS_DIR, "statusline.sh");
1465
+ TG_DIR = path4.join(os2.homedir(), ".tokengolf");
1466
+ STABLE_STATUSLINE = path4.join(TG_DIR, "statusline.sh");
1467
+ STABLE_WRAPPER = path4.join(TG_DIR, "statusline-wrapper.sh");
1410
1468
  CLAUDE_DIR = path4.join(os2.homedir(), ".claude");
1411
1469
  CLAUDE_SETTINGS = path4.join(CLAUDE_DIR, "settings.json");
1412
1470
  }
package/docs/index.html CHANGED
@@ -1282,7 +1282,7 @@
1282
1282
  <!-- ── HUD panel ── -->
1283
1283
  <div class="tab-panel active" id="panel-hud" role="tabpanel" aria-labelledby="tab-hud">
1284
1284
  <p class="demo-label" style="margin-bottom:1.5rem">
1285
- <strong>Live HUD</strong> appears in every Claude Code session. Shows emotion, cost vs par, efficiency rating, context load, model class, and prompt count. Accent color matches your efficiency tier.
1285
+ <strong>Live HUD</strong> appears in every Claude Code session. 3-line display: efficiency rating with project/git info, emotion with cost vs par, model class with context load. Accent color matches your efficiency tier.
1286
1286
  </p>
1287
1287
  <div class="demos-grid">
1288
1288
 
@@ -1290,7 +1290,8 @@
1290
1290
  <div class="term" data-glow="yellow">
1291
1291
  <div class="term-header">Legendary · early session</div>
1292
1292
  <pre>
1293
- <span class="border-yellow">██</span> 😎 <span class="t-green">VIBING</span> 💎 <span class="t-dim">$</span>0.42<span class="t-dim">/9.90</span> <span class="t-yellow">▓░░░░░░░░░░</span> 4% 🌟 <span class="t-yellow">LEGENDARY</span>
1293
+ <span class="border-yellow">██</span> 🌟 <span class="t-yellow">LEGENDARY</span> <span class="t-dim">📂</span> myapp <span class="t-dim">⎇</span> main <span class="t-green">✓</span>
1294
+ <span class="border-yellow">██</span> 😎 <span class="t-green">VIBING</span> 💎 <span class="t-dim">$</span>0.18<span class="t-dim">/2.12</span> <span class="t-yellow">▓░░░░░░░░░░</span> 8%
1294
1295
  <span class="border-yellow">██</span> ⚔️ Sonnet 🪶 <span class="t-green">▓░░░░░░░░░</span> 8%</pre>
1295
1296
  </div>
1296
1297
  </div>
@@ -1299,7 +1300,8 @@
1299
1300
  <div class="term" data-glow="cyan">
1300
1301
  <div class="term-header">Pro · mid session</div>
1301
1302
  <pre>
1302
- <span class="border-cyan">██</span> 😤 <span class="t-yellow">GRINDING</span> 🥉 <span class="t-dim">$</span>6.80<span class="t-dim">/19.80</span> <span class="t-cyan">▓▓▓▓░░░░░░░</span> 34% 💪 <span class="t-cyan">PRO</span>
1303
+ <span class="border-cyan">██</span> 💪 <span class="t-cyan">PRO</span> <span class="t-dim">📂</span> api-server <span class="t-dim">⎇</span> feat/auth <span class="t-yellow">●</span>
1304
+ <span class="border-cyan">██</span> 😤 <span class="t-yellow">GRINDING</span> 🥈 <span class="t-dim">$</span>1.50<span class="t-dim">/4.24</span> <span class="t-cyan">▓▓▓▓░░░░░░░</span> 35%
1303
1305
  <span class="border-cyan">██</span> ⚔️ Sonnet 📚 <span class="t-green">▓▓▓░░░░░░░</span> 34%</pre>
1304
1306
  </div>
1305
1307
  </div>
@@ -1308,16 +1310,18 @@
1308
1310
  <div class="term" data-glow="yellow">
1309
1311
  <div class="term-header">Legendary · high effort</div>
1310
1312
  <pre>
1311
- <span class="border-yellow">██</span> 😎 <span class="t-green">VIBING</span> 💎 <span class="t-dim">$</span>0.41<span class="t-dim">/15.65</span> <span class="t-yellow">░░░░░░░░░░░</span> 3% 🌟 <span class="t-yellow">LEGENDARY</span>
1313
+ <span class="border-yellow">██</span> 🌟 <span class="t-yellow">LEGENDARY</span> <span class="t-dim">📂</span> tokengolf <span class="t-dim">⎇</span> main <span class="t-green">✓</span>
1314
+ <span class="border-yellow">██</span> 😎 <span class="t-green">VIBING</span> 💎 <span class="t-dim">$</span>0.41<span class="t-dim">/3.35</span> <span class="t-yellow">▓░░░░░░░░░░</span> 12%
1312
1315
  <span class="border-yellow">██</span> ⚔️ Sonnet·High 📚 <span class="t-green">▓▓▓░░░░░░░</span> 29%</pre>
1313
1316
  </div>
1314
1317
  </div>
1315
1318
 
1316
1319
  <div class="demo-item">
1317
- <div class="term" data-glow="cyan">
1320
+ <div class="term" data-glow="magenta">
1318
1321
  <div class="term-header">Epic · Opus run</div>
1319
1322
  <pre>
1320
- <span class="border-magenta">██</span> 🛹 <span class="t-green">CRUISING</span> 🥈 <span class="t-dim">$</span>18.00<span class="t-dim">/90.00</span> <span class="t-magenta">▓▓░░░░░░░░░</span> 20% 🔥 <span class="t-magenta">EPIC</span>
1323
+ <span class="border-magenta">██</span> 🔥 <span class="t-magenta">EPIC</span> <span class="t-dim">📂</span> ml-pipeline <span class="t-dim">⎇</span> refactor/v2 <span class="t-green">✓</span>
1324
+ <span class="border-magenta">██</span> 🛹 <span class="t-green">CRUISING</span> 💎 <span class="t-dim">$</span>3.80<span class="t-dim">/16.00</span> <span class="t-magenta">▓▓▓░░░░░░░░</span> 24%
1321
1325
  <span class="border-magenta">██</span> 🧙 Opus 🎒 <span class="t-cyan">▓▓▓▓▓░░░░░</span> 52%</pre>
1322
1326
  </div>
1323
1327
  </div>
@@ -1326,7 +1330,8 @@
1326
1330
  <div class="term">
1327
1331
  <div class="term-header">Close Call · Haiku near bust</div>
1328
1332
  <pre>
1329
- <span class="border-white">██</span> 😰 <span class="t-yellow">SWEATING</span> 🥉 <span class="t-dim">$</span>2.10<span class="t-dim">/2.46</span> <span class="t-white">▓▓▓▓▓▓▓▓▓░░</span> 85% ⚠️ <span class="t-white">CLOSE CALL</span>
1333
+ <span class="border-white">██</span> ⚠️ <span class="t-white">CLOSE CALL</span> <span class="t-dim">📂</span> docs-site <span class="t-dim">⎇</span> fix/typos <span class="t-yellow">●</span>
1334
+ <span class="border-white">██</span> 😰 <span class="t-yellow">SWEATING</span> 🥇 <span class="t-dim">$</span>0.45<span class="t-dim">/0.52</span> <span class="t-white">▓▓▓▓▓▓▓▓▓░░</span> 87%
1330
1335
  <span class="border-white">██</span> 🏹 Haiku 🪨 <span class="t-yellow">▓▓▓▓▓▓▓▓░░</span> 78%</pre>
1331
1336
  </div>
1332
1337
  </div>
@@ -1335,7 +1340,8 @@
1335
1340
  <div class="term" data-glow="red">
1336
1341
  <div class="term-header">Bust · past par, dead</div>
1337
1342
  <pre>
1338
- <span class="border-red">██</span> 🧟 <span class="t-red">ZOMBIE</span> 💸 <span class="t-dim">$</span>34.24<span class="t-dim">/22.14</span> <span class="t-red">▓▓▓▓▓▓▓▓▓▓▓</span> 155% 💥 <span class="t-red">BUST</span>
1343
+ <span class="border-red">██</span> 💥 <span class="t-red">BUST</span> <span class="t-dim">📂</span> monorepo <span class="t-dim">⎇</span> main <span class="t-yellow">●</span>
1344
+ <span class="border-red">██</span> 🧟 <span class="t-red">ZOMBIE</span> 🥈 <span class="t-dim">$</span>6.20<span class="t-dim">/4.74</span> <span class="t-red">▓▓▓▓▓▓▓▓▓▓▓</span> 131%
1339
1345
  <span class="border-red">██</span> ⚔️ Sonnet 🎒 <span class="t-cyan">▓▓▓▓▓░░░░░</span> 45%</pre>
1340
1346
  </div>
1341
1347
  </div>
@@ -1344,16 +1350,18 @@
1344
1350
  <div class="term" data-glow="cyan">
1345
1351
  <div class="term-header">Overwhelmed · high context</div>
1346
1352
  <pre>
1347
- <span class="border-cyan">██</span> 🤯 <span class="t-red">OVERWHELMED</span> 🥉 <span class="t-dim">$</span>28.00<span class="t-dim">/77.94</span> <span class="t-cyan">▓▓▓▓░░░░░░░</span> 36% 💪 <span class="t-cyan">PRO</span>
1353
+ <span class="border-cyan">██</span> 💪 <span class="t-cyan">PRO</span> <span class="t-dim">📂</span> kernel <span class="t-dim">⎇</span> dev <span class="t-green">✓</span>
1354
+ <span class="border-cyan">██</span> 🤯 <span class="t-red">OVERWHELMED</span> 💎 <span class="t-dim">$</span>5.50<span class="t-dim">/13.86</span> <span class="t-cyan">▓▓▓▓░░░░░░░</span> 40%
1348
1355
  <span class="border-cyan">██</span> 🧙 Opus 🗿 <span class="t-red">▓▓▓▓▓▓▓▓▓░</span> 91%</pre>
1349
1356
  </div>
1350
1357
  </div>
1351
1358
 
1352
1359
  <div class="demo-item">
1353
- <div class="term" data-glow="cyan">
1354
- <div class="term-header">Sleeping · idle session</div>
1360
+ <div class="term" data-glow="magenta">
1361
+ <div class="term-header">Sleeping · fainted session</div>
1355
1362
  <pre>
1356
- <span class="border-magenta">██</span> 💤 <span class="t-dim">SLEEPING</span> 🥉 <span class="t-dim">$</span>4.22<span class="t-dim">/17.15</span> <span class="t-magenta">▓▓▓░░░░░░░░</span> 25% 🔥 <span class="t-magenta">EPIC</span>
1363
+ <span class="border-magenta">██</span> 🔥 <span class="t-magenta">EPIC</span> <span class="t-dim">📂</span> webapp <span class="t-dim">⎇</span> feat/deploy <span class="t-green">✓</span>
1364
+ <span class="border-magenta">██</span> 💤 <span class="t-dim">SLEEPING</span> 💎 <span class="t-dim">$</span>0.92<span class="t-dim">/3.67</span> <span class="t-magenta">▓▓▓░░░░░░░░</span> 25%
1357
1365
  <span class="border-magenta">██</span> ⚔️ Sonnet 🧱 <span class="t-yellow">▓▓▓▓▓▓▓░░░</span> 67%</pre>
1358
1366
  </div>
1359
1367
  </div>
@@ -3,7 +3,8 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
 
6
- const STATE_FILE = path.join(os.homedir(), '.tokengolf', 'current-run.json');
6
+ const cwdKey = (process.env.PWD || process.cwd()).replace(/\//g, '-');
7
+ const STATE_FILE = path.join(os.homedir(), '.tokengolf', `current-run${cwdKey}.json`);
7
8
 
8
9
  let input = '';
9
10
  process.stdin.setEncoding('utf8');
@@ -3,7 +3,8 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
 
6
- const STATE_FILE = path.join(os.homedir(), '.tokengolf', 'current-run.json');
6
+ const cwdKey = (process.env.PWD || process.cwd()).replace(/\//g, '-');
7
+ const STATE_FILE = path.join(os.homedir(), '.tokengolf', `current-run${cwdKey}.json`);
7
8
 
8
9
  let input = '';
9
10
  process.stdin.setEncoding('utf8');
@@ -3,7 +3,8 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
 
6
- const STATE_FILE = path.join(os.homedir(), '.tokengolf', 'current-run.json');
6
+ const cwdKey = (process.env.PWD || process.cwd()).replace(/\//g, '-');
7
+ const STATE_FILE = path.join(os.homedir(), '.tokengolf', `current-run${cwdKey}.json`);
7
8
 
8
9
  try {
9
10
  let stdin = '';
@@ -32,11 +32,11 @@ try {
32
32
  const reason = event.reason || 'other';
33
33
 
34
34
  // Read authoritative cost from StatusLine sidecar (same source as the HUD)
35
+ const cwdKey = (process.env.PWD || process.cwd()).replace(/\//g, '-');
36
+ const costFile = path.join(os.homedir(), '.tokengolf', `session-cost${cwdKey}`);
35
37
  let liveCost = null;
36
38
  try {
37
- const raw = fs
38
- .readFileSync(path.join(os.homedir(), '.tokengolf', 'session-cost'), 'utf8')
39
- .trim();
39
+ const raw = fs.readFileSync(costFile, 'utf8').trim();
40
40
  const parsed = parseFloat(raw);
41
41
  if (!isNaN(parsed) && parsed > 0) liveCost = parsed;
42
42
  } catch {}
@@ -122,7 +122,7 @@ try {
122
122
  clearCurrentRun();
123
123
  // Clean up sidecar cost file
124
124
  try {
125
- fs.unlinkSync(path.join(os.homedir(), '.tokengolf', 'session-cost'));
125
+ fs.unlinkSync(costFile);
126
126
  } catch {}
127
127
 
128
128
  writeTTY('\n' + renderScorecard(saved) + '\n\n');
@@ -3,12 +3,14 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
 
6
- const STATE_FILE = path.join(os.homedir(), '.tokengolf', 'current-run.json');
7
6
  const STATE_DIR = path.join(os.homedir(), '.tokengolf');
7
+ const cwdKey = (process.env.PWD || process.cwd()).replace(/\//g, '-');
8
+ const STATE_FILE = path.join(STATE_DIR, `current-run${cwdKey}.json`);
8
9
 
9
- // Auto-sync: if npm package version changed since last install, update hook paths
10
+ // Auto-sync: if npm package version changed since last install, update hook paths (npm-only)
10
11
  try {
11
12
  const pkgPath = path.resolve(path.dirname(process.argv[1]), '../package.json');
13
+ if (!fs.existsSync(pkgPath)) throw new Error('not npm install');
12
14
  const currentVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
13
15
  const stampFile = path.join(STATE_DIR, 'installed-version');
14
16
  let installedVersion = null;
@@ -35,17 +37,7 @@ try {
35
37
  }
36
38
  }
37
39
  }
38
- // Update statusLine paths
39
- const statuslinePath = path.join(hooksDir, 'statusline.sh');
40
- const wrapperPath = path.join(hooksDir, 'statusline-wrapper.sh');
41
- if (settings.statusLine?.command) {
42
- const cmd = settings.statusLine.command;
43
- if (cmd.includes('statusline-wrapper')) {
44
- settings.statusLine.command = wrapperPath;
45
- } else if (cmd.includes('statusline.sh')) {
46
- settings.statusLine.command = statuslinePath;
47
- }
48
- }
40
+ // statusLine paths now use stable ~/.tokengolf/statusline.sh — auto-install block handles updates
49
41
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
50
42
  } catch {}
51
43
  fs.writeFileSync(stampFile, currentVersion);
@@ -56,18 +48,23 @@ try {
56
48
  }
57
49
  } catch {}
58
50
 
59
- // Auto-install statusLine if missing or stale
51
+ // Auto-install statusLine: copy to stable ~/.tokengolf/ path (survives plugin uninstall)
60
52
  try {
61
53
  const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
62
54
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
63
55
  const scriptDir = path.dirname(fs.realpathSync(process.argv[1]));
64
- const statuslinePath = path.join(scriptDir, 'statusline.sh');
65
- if (fs.existsSync(statuslinePath)) {
56
+ const srcStatusline = path.join(scriptDir, 'statusline.sh');
57
+ const stablePath = path.join(STATE_DIR, 'statusline.sh');
58
+ if (fs.existsSync(srcStatusline)) {
59
+ // Always copy latest version to stable location
60
+ if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
61
+ fs.copyFileSync(srcStatusline, stablePath);
62
+ fs.chmodSync(stablePath, 0o755);
66
63
  const current = settings.statusLine?.command || '';
67
64
  const needsInstall = !current || current.includes('tokengolf');
68
- const needsUpdate = needsInstall && current !== statuslinePath;
65
+ const needsUpdate = needsInstall && current !== stablePath;
69
66
  if (needsUpdate) {
70
- settings.statusLine = { type: 'command', command: statuslinePath, padding: 1 };
67
+ settings.statusLine = { type: 'command', command: stablePath, padding: 1 };
71
68
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
72
69
  }
73
70
  }
@@ -99,6 +96,17 @@ function detectFastMode() {
99
96
  }
100
97
  }
101
98
 
99
+ // Migrate from global current-run.json if this project's run doesn't exist yet
100
+ try {
101
+ const globalFile = path.join(STATE_DIR, 'current-run.json');
102
+ if (!fs.existsSync(STATE_FILE) && fs.existsSync(globalFile)) {
103
+ const globalRun = JSON.parse(fs.readFileSync(globalFile, 'utf8'));
104
+ if (globalRun.cwd === (process.env.PWD || process.cwd())) {
105
+ fs.renameSync(globalFile, STATE_FILE);
106
+ }
107
+ }
108
+ } catch {}
109
+
102
110
  try {
103
111
  const cwd = process.env.PWD || process.cwd();
104
112