@mauricode/token-derby 2.9.0 → 2.10.1

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/bin.js CHANGED
@@ -24,6 +24,22 @@ import { useState as useState2 } from "react";
24
24
  import { Box as Box3, Text as Text3, useInput } from "ink";
25
25
  import TextInput from "ink-text-input";
26
26
 
27
+ // ../shared/dist/models.js
28
+ var MODEL_KEYS = ["claude", "codex", "gemini"];
29
+ var SECONDARY_WEIGHT = 0.1;
30
+ function isModelKey(v) {
31
+ return typeof v === "string" && MODEL_KEYS.includes(v);
32
+ }
33
+ function weightFor(primary, key) {
34
+ return key === primary ? 1 : SECONDARY_WEIGHT;
35
+ }
36
+ function weightedTotal(primary, perSource) {
37
+ let total = 0;
38
+ for (const key of MODEL_KEYS)
39
+ total += perSource[key] * weightFor(primary, key);
40
+ return total;
41
+ }
42
+
27
43
  // ../shared/dist/constants.js
28
44
  var CLI_VERSION_HEADER = "x-cli-version";
29
45
  var USER_ID_HEADER = "x-user-id";
@@ -724,8 +740,8 @@ var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
724
740
  // src/version.ts
725
741
  import { createRequire } from "module";
726
742
  function readVersion() {
727
- if ("2.9.0".length > 0) {
728
- return "2.9.0";
743
+ if ("2.10.1".length > 0) {
744
+ return "2.10.1";
729
745
  }
730
746
  try {
731
747
  const req = createRequire(import.meta.url);
@@ -759,6 +775,12 @@ function activeRacesDir() {
759
775
  function claudeProjectsDir() {
760
776
  return process.env.TOKEN_DERBY_CLAUDE_DIR ?? path.join(os.homedir(), ".claude", "projects");
761
777
  }
778
+ function codexSessionsDir() {
779
+ return process.env.TOKEN_DERBY_CODEX_DIR ?? path.join(os.homedir(), ".codex");
780
+ }
781
+ function geminiTmpDir() {
782
+ return process.env.TOKEN_DERBY_GEMINI_DIR ?? path.join(os.homedir(), ".gemini", "tmp");
783
+ }
762
784
 
763
785
  // src/identity/identity.ts
764
786
  async function loadIdentity() {
@@ -813,8 +835,8 @@ function getIdentity() {
813
835
  function _resetIdentityCacheForTests() {
814
836
  identityCache = null;
815
837
  }
816
- async function request(method, path5, body, horseAuthToken, fetchImpl = fetch) {
817
- const url = path5.startsWith("http") ? path5 : `${apiBase()}${path5}`;
838
+ async function request(method, path7, body, horseAuthToken, fetchImpl = fetch) {
839
+ const url = path7.startsWith("http") ? path7 : `${apiBase()}${path7}`;
818
840
  const headers = {};
819
841
  headers[CLI_VERSION_HEADER] = CLI_VERSION;
820
842
  headers["user-agent"] = `token-derby/${CLI_VERSION}`;
@@ -1302,6 +1324,8 @@ async function createRaceCommand(organisationName) {
1302
1324
  }
1303
1325
  const countInputRaw = (await rl.question("Count input tokens (fresh input + cache creation) toward race totals? [y/N]: ")).trim().toLowerCase();
1304
1326
  const counts_input = countInputRaw === "y" || countInputRaw === "yes";
1327
+ const top5Raw = (await rl.question("Count only each racer's 5 most-active conversations toward their primary model's score? [y/N]: ")).trim().toLowerCase();
1328
+ const primary_top5 = top5Raw === "y" || top5Raw === "yes";
1305
1329
  const resp = await createRace({
1306
1330
  name,
1307
1331
  start_time: start,
@@ -1309,7 +1333,8 @@ async function createRaceCommand(organisationName) {
1309
1333
  tz,
1310
1334
  ...max !== void 0 ? { max_participants: max } : {},
1311
1335
  ...org ? { organisation_name: org } : {},
1312
- ...counts_input ? { counts_input: true } : {}
1336
+ ...counts_input ? { counts_input: true } : {},
1337
+ ...primary_top5 ? { primary_top5: true } : {}
1313
1338
  });
1314
1339
  console.log("");
1315
1340
  console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
@@ -1325,6 +1350,9 @@ async function createRaceCommand(organisationName) {
1325
1350
  if (counts_input) {
1326
1351
  console.log(" Counting input + output tokens (excluding cache reads).");
1327
1352
  }
1353
+ if (primary_top5) {
1354
+ console.log(" Primary score counts only each racer's top 5 conversations per beat.");
1355
+ }
1328
1356
  console.log(` Share with participants: token-derby join ${resp.join_code}`);
1329
1357
  return 0;
1330
1358
  } catch (e) {
@@ -1344,9 +1372,31 @@ function isIso(s) {
1344
1372
  }
1345
1373
 
1346
1374
  // src/commands/join.ts
1347
- import React8 from "react";
1375
+ import React9 from "react";
1348
1376
  import { render as render4 } from "ink";
1349
1377
 
1378
+ // src/ui/PrimaryPicker.tsx
1379
+ import { useState as useState4 } from "react";
1380
+ import { Box as Box6, Text as Text6, useInput as useInput3 } from "ink";
1381
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1382
+ var LABELS = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
1383
+ function PrimaryPicker({ onPick }) {
1384
+ const [i, setI] = useState4(0);
1385
+ useInput3((_input, key) => {
1386
+ if (key.upArrow) setI((p) => (p + MODEL_KEYS.length - 1) % MODEL_KEYS.length);
1387
+ else if (key.downArrow) setI((p) => (p + 1) % MODEL_KEYS.length);
1388
+ else if (key.return) onPick(MODEL_KEYS[i]);
1389
+ });
1390
+ return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
1391
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Pick your primary model for this race (counts 1:1; the others count at 10%)." }),
1392
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "This is locked for the whole race \u2014 you can't change it, even by rejoining." }),
1393
+ MODEL_KEYS.map((m, idx) => /* @__PURE__ */ jsxs4(Text6, { color: idx === i ? "cyan" : void 0, children: [
1394
+ idx === i ? "\u276F " : " ",
1395
+ LABELS[m]
1396
+ ] }, m))
1397
+ ] });
1398
+ }
1399
+
1350
1400
  // src/stable/active-race.ts
1351
1401
  import * as fs2 from "fs/promises";
1352
1402
  import * as path3 from "path";
@@ -1360,45 +1410,58 @@ async function saveActiveRace(active) {
1360
1410
  }
1361
1411
 
1362
1412
  // src/runtime/run-race.tsx
1363
- import { useEffect as useEffect2, useRef, useState as useState4 } from "react";
1364
- import { Box as Box7, Text as Text7, useApp } from "ink";
1413
+ import { useEffect as useEffect2, useRef, useState as useState5 } from "react";
1414
+ import { Box as Box8, Text as Text8, useApp } from "ink";
1365
1415
 
1366
1416
  // src/ui/StatusScreen.tsx
1367
- import { Box as Box6, Text as Text6 } from "ink";
1368
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1417
+ import { Box as Box7, Text as Text7 } from "ink";
1418
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1419
+ var MODEL_LABELS = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
1420
+ function TokenBreakdown(props) {
1421
+ const { primaryModel, perSource, raceScore, primaryCapped } = props;
1422
+ const primaryTag = primaryCapped ? "(primary \xB7 top 5/beat)" : "(primary)";
1423
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
1424
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Tokens by model (since join)" }),
1425
+ MODEL_KEYS.map((m) => /* @__PURE__ */ jsxs5(Text7, { children: [
1426
+ ` ${MODEL_LABELS[m].padEnd(10)} ${perSource[m].toLocaleString().padStart(12)} `,
1427
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: m === primaryModel ? primaryTag : "(10%)" })
1428
+ ] }, m)),
1429
+ /* @__PURE__ */ jsx7(Text7, { children: ` ${"Race score".padEnd(10)} ${Math.round(raceScore).toLocaleString().padStart(12)}` })
1430
+ ] });
1431
+ }
1369
1432
  function StatusScreen(props) {
1370
- const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled } = props;
1433
+ const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, stallReason, primaryModel, perSource, primaryCapped } = props;
1371
1434
  if (!race) {
1372
- return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: /* @__PURE__ */ jsx6(Text6, { children: "Joining race\u2026" }) });
1435
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx7(Text7, { children: "Joining race\u2026" }) });
1373
1436
  }
1374
1437
  const own = race.horses.find((h) => h.horse_id === ownHorseId);
1375
1438
  const leader = race.horses[0];
1376
1439
  const elapsedPct = elapsed(race);
1377
1440
  const timeLeft = formatDuration(race.time_left_seconds);
1378
1441
  const lvl = levelInfo((own?.xp ?? 0) + (own?.live_xp ?? 0));
1379
- return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
1380
- /* @__PURE__ */ jsxs4(Text6, { children: [
1442
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
1443
+ /* @__PURE__ */ jsxs5(Text7, { children: [
1381
1444
  "\u{1F3C7} TOKEN DERBY \u2500\u2500\u2500 ",
1382
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: race.name }),
1445
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: race.name }),
1383
1446
  " \u2500\u2500\u2500 status: ",
1384
- /* @__PURE__ */ jsx6(Text6, { color: statusColor(race.status), children: race.status })
1447
+ /* @__PURE__ */ jsx7(Text7, { color: statusColor(race.status), children: race.status })
1385
1448
  ] }),
1386
- /* @__PURE__ */ jsxs4(Box6, { marginTop: 1, flexDirection: "row", children: [
1387
- /* @__PURE__ */ jsx6(HorseSprite, { sprite: MINI_SPRITE, colors: ownColors }),
1388
- /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
1389
- /* @__PURE__ */ jsxs4(Text6, { children: [
1449
+ /* @__PURE__ */ jsxs5(Box7, { marginTop: 1, flexDirection: "row", children: [
1450
+ /* @__PURE__ */ jsx7(HorseSprite, { sprite: MINI_SPRITE, colors: ownColors }),
1451
+ /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
1452
+ /* @__PURE__ */ jsxs5(Text7, { children: [
1390
1453
  " ",
1391
1454
  ownHorseName,
1392
1455
  " ",
1393
- /* @__PURE__ */ jsxs4(Text6, { color: "cyan", children: [
1456
+ /* @__PURE__ */ jsxs5(Text7, { color: "cyan", children: [
1394
1457
  "[Lvl. ",
1395
1458
  lvl.level,
1396
1459
  "]"
1397
1460
  ] })
1398
1461
  ] }),
1399
- /* @__PURE__ */ jsxs4(Text6, { children: [
1462
+ /* @__PURE__ */ jsxs5(Text7, { children: [
1400
1463
  " ",
1401
- /* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
1464
+ /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
1402
1465
  "(",
1403
1466
  ownUserName,
1404
1467
  ")"
@@ -1406,44 +1469,57 @@ function StatusScreen(props) {
1406
1469
  ] })
1407
1470
  ] })
1408
1471
  ] }),
1409
- /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", marginTop: 1, children: [
1410
- /* @__PURE__ */ jsxs4(Text6, { children: [
1472
+ /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
1473
+ /* @__PURE__ */ jsxs5(Text7, { children: [
1411
1474
  "Tokens (race): ",
1412
1475
  own?.current_tokens ?? 0
1413
1476
  ] }),
1414
- /* @__PURE__ */ jsxs4(Text6, { children: [
1477
+ /* @__PURE__ */ jsxs5(Text7, { children: [
1415
1478
  "Position: ",
1416
1479
  own?.rank ?? "\u2014",
1417
1480
  " of ",
1418
1481
  race.horses.length
1419
1482
  ] }),
1420
- /* @__PURE__ */ jsxs4(Text6, { children: [
1483
+ /* @__PURE__ */ jsxs5(Text7, { children: [
1421
1484
  "Leader: ",
1422
1485
  leader ? `${leader.name}${leader.user_name ? ` (${leader.user_name})` : ""} \u2014 ${leader.current_tokens}` : "\u2014"
1423
1486
  ] }),
1424
- /* @__PURE__ */ jsxs4(Text6, { children: [
1487
+ /* @__PURE__ */ jsxs5(Text7, { children: [
1425
1488
  "Race elapsed: ",
1426
1489
  (elapsedPct * 100).toFixed(0),
1427
1490
  "% ",
1428
1491
  bar(elapsedPct, 20)
1429
1492
  ] }),
1430
- /* @__PURE__ */ jsxs4(Text6, { children: [
1493
+ /* @__PURE__ */ jsxs5(Text7, { children: [
1431
1494
  "Time left: ",
1432
1495
  timeLeft
1433
1496
  ] }),
1434
- /* @__PURE__ */ jsxs4(Text6, { children: [
1497
+ /* @__PURE__ */ jsxs5(Text7, { children: [
1435
1498
  "XP: ",
1436
1499
  lvl.next_level_xp === null ? `${lvl.xp} (max level) ${bar(1, 20)}` : `${lvl.xp_into_level}/${lvl.xp_for_level} \u2192 Lvl. ${lvl.level + 1} ${bar(lvl.progress, 20)}`
1437
1500
  ] }),
1438
- /* @__PURE__ */ jsxs4(Text6, { children: [
1501
+ /* @__PURE__ */ jsxs5(Text7, { children: [
1439
1502
  "Last heartbeat: ",
1440
1503
  lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
1441
1504
  " ",
1442
- /* @__PURE__ */ jsx6(Text6, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
1505
+ /* @__PURE__ */ jsx7(Text7, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
1443
1506
  ] }),
1444
- stalled && /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u26A0 Can't read token usage \u2014 try restarting this terminal. Your race continues." })
1507
+ stalled && /* @__PURE__ */ jsxs5(Text7, { color: "yellow", children: [
1508
+ "\u26A0 ",
1509
+ stallReason ?? "Can't read token usage",
1510
+ ". Your race continues."
1511
+ ] })
1445
1512
  ] }),
1446
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
1513
+ primaryModel && perSource && /* @__PURE__ */ jsx7(
1514
+ TokenBreakdown,
1515
+ {
1516
+ primaryModel,
1517
+ perSource,
1518
+ raceScore: own?.current_tokens ?? weightedTotal(primaryModel, perSource),
1519
+ primaryCapped
1520
+ }
1521
+ ),
1522
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
1447
1523
  ] });
1448
1524
  }
1449
1525
  function elapsed(race) {
@@ -1515,30 +1591,45 @@ function runHeartbeatLoop(opts) {
1515
1591
  // src/tokens/transcripts.ts
1516
1592
  import * as fs3 from "fs/promises";
1517
1593
  import * as path4 from "path";
1518
- async function sumTokens() {
1594
+ var MAX_PROJECT_DEPTH = 8;
1595
+ function conversationId(file, root) {
1596
+ const rel = path4.relative(root, file);
1597
+ const [project, session] = rel.split(path4.sep);
1598
+ if (project === void 0 || session === void 0) return rel.replace(/\.jsonl$/, "");
1599
+ return `${project}/${session.replace(/\.jsonl$/, "")}`;
1600
+ }
1601
+ async function sumTokensByConversation() {
1519
1602
  const root = claudeProjectsDir();
1520
1603
  const files = await listJsonlFiles(root);
1521
- let input = 0;
1522
- let output = 0;
1604
+ const byConv = /* @__PURE__ */ new Map();
1523
1605
  for (const file of files) {
1524
1606
  const t = await sumFile(file);
1607
+ const id = conversationId(file, root);
1608
+ const acc = byConv.get(id) ?? { input: 0, output: 0 };
1609
+ acc.input += t.input;
1610
+ acc.output += t.output;
1611
+ byConv.set(id, acc);
1612
+ }
1613
+ return byConv;
1614
+ }
1615
+ async function sumTokens() {
1616
+ const byConv = await sumTokensByConversation();
1617
+ let input = 0;
1618
+ let output = 0;
1619
+ for (const t of byConv.values()) {
1525
1620
  input += t.input;
1526
1621
  output += t.output;
1527
1622
  }
1528
1623
  return { input, output };
1529
1624
  }
1530
- async function sumTokensForRace(race) {
1531
- const { input, output } = await sumTokens();
1532
- return race.counts_input ? input + output : output;
1533
- }
1534
1625
  async function listJsonlFiles(root) {
1535
1626
  const projects = await fs3.readdir(root);
1536
1627
  const out = [];
1537
1628
  for (const project of projects) {
1538
1629
  const projectDir = path4.join(root, project);
1539
- const stat2 = await fs3.stat(projectDir);
1540
- if (!stat2.isDirectory()) continue;
1541
- await collectJsonl(projectDir, 3, out);
1630
+ const stat3 = await fs3.stat(projectDir);
1631
+ if (!stat3.isDirectory()) continue;
1632
+ await collectJsonl(projectDir, MAX_PROJECT_DEPTH, out);
1542
1633
  }
1543
1634
  return out;
1544
1635
  }
@@ -1578,68 +1669,342 @@ async function sumFile(file) {
1578
1669
  return { input, output };
1579
1670
  }
1580
1671
 
1672
+ // src/tokens/codex.ts
1673
+ import * as fs4 from "fs/promises";
1674
+ import * as path5 from "path";
1675
+ function num(v) {
1676
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
1677
+ }
1678
+ async function sumCodexByConversation() {
1679
+ const root = codexSessionsDir();
1680
+ await fs4.stat(root);
1681
+ const files = [
1682
+ ...await collectRollouts(path5.join(root, "sessions")),
1683
+ ...await collectRollouts(path5.join(root, "archived_sessions"))
1684
+ ];
1685
+ const byConv = /* @__PURE__ */ new Map();
1686
+ for (const file of files) {
1687
+ byConv.set(file, await lastTokenCount(file));
1688
+ }
1689
+ return byConv;
1690
+ }
1691
+ async function sumCodexTokens() {
1692
+ const byConv = await sumCodexByConversation();
1693
+ let input = 0;
1694
+ let output = 0;
1695
+ for (const t of byConv.values()) {
1696
+ input += t.input;
1697
+ output += t.output;
1698
+ }
1699
+ return { input, output };
1700
+ }
1701
+ async function collectRollouts(dir) {
1702
+ let entries;
1703
+ try {
1704
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1705
+ } catch (e) {
1706
+ if (e?.code === "ENOENT") return [];
1707
+ throw e;
1708
+ }
1709
+ const out = [];
1710
+ for (const entry of entries) {
1711
+ const full = path5.join(dir, entry.name);
1712
+ if (entry.isDirectory()) out.push(...await collectRollouts(full));
1713
+ else if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) out.push(full);
1714
+ }
1715
+ return out;
1716
+ }
1717
+ async function lastTokenCount(file) {
1718
+ let raw;
1719
+ try {
1720
+ raw = await fs4.readFile(file, "utf8");
1721
+ } catch {
1722
+ return { input: 0, output: 0 };
1723
+ }
1724
+ let usage = null;
1725
+ for (const line of raw.split("\n")) {
1726
+ if (!line.trim()) continue;
1727
+ let parsed;
1728
+ try {
1729
+ parsed = JSON.parse(line);
1730
+ } catch {
1731
+ continue;
1732
+ }
1733
+ if (parsed?.payload?.type === "token_count" && parsed.payload.info?.total_token_usage) {
1734
+ usage = parsed.payload.info.total_token_usage;
1735
+ }
1736
+ }
1737
+ if (!usage) return { input: 0, output: 0 };
1738
+ return {
1739
+ input: Math.max(0, num(usage.input_tokens) - num(usage.cached_input_tokens)),
1740
+ output: num(usage.output_tokens)
1741
+ };
1742
+ }
1743
+
1744
+ // src/tokens/gemini.ts
1745
+ import * as fs5 from "fs/promises";
1746
+ import * as path6 from "path";
1747
+ function num2(v) {
1748
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
1749
+ }
1750
+ async function sumGeminiByConversation() {
1751
+ const files = await listChatFiles(geminiTmpDir());
1752
+ const byConv = /* @__PURE__ */ new Map();
1753
+ for (const file of files) {
1754
+ byConv.set(file, await sumGeminiFile(file));
1755
+ }
1756
+ return byConv;
1757
+ }
1758
+ async function sumGeminiTokens() {
1759
+ const byConv = await sumGeminiByConversation();
1760
+ let input = 0;
1761
+ let output = 0;
1762
+ for (const t of byConv.values()) {
1763
+ input += t.input;
1764
+ output += t.output;
1765
+ }
1766
+ return { input, output };
1767
+ }
1768
+ async function listChatFiles(root) {
1769
+ const entries = await fs5.readdir(root);
1770
+ const out = [];
1771
+ for (const entry of entries) {
1772
+ const chatsDir = path6.join(root, entry, "chats");
1773
+ let files;
1774
+ try {
1775
+ files = await fs5.readdir(chatsDir);
1776
+ } catch {
1777
+ continue;
1778
+ }
1779
+ for (const f of files) {
1780
+ if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(path6.join(chatsDir, f));
1781
+ }
1782
+ }
1783
+ return out;
1784
+ }
1785
+ async function sumGeminiFile(file) {
1786
+ let raw;
1787
+ try {
1788
+ raw = await fs5.readFile(file, "utf8");
1789
+ } catch {
1790
+ return { input: 0, output: 0 };
1791
+ }
1792
+ const messages = file.endsWith(".jsonl") ? parseJsonl(raw) : parseJson(raw);
1793
+ let input = 0;
1794
+ let output = 0;
1795
+ for (const m of messages) {
1796
+ const tk = m?.tokens;
1797
+ if (!tk || typeof tk !== "object") continue;
1798
+ input += Math.max(0, num2(tk.input) - num2(tk.cached));
1799
+ output += num2(tk.output);
1800
+ }
1801
+ return { input, output };
1802
+ }
1803
+ function parseJson(raw) {
1804
+ try {
1805
+ const data = JSON.parse(raw);
1806
+ return Array.isArray(data?.messages) ? data.messages : [];
1807
+ } catch {
1808
+ return [];
1809
+ }
1810
+ }
1811
+ function parseJsonl(raw) {
1812
+ const out = [];
1813
+ for (const line of raw.split("\n")) {
1814
+ if (!line.trim()) continue;
1815
+ try {
1816
+ out.push(JSON.parse(line));
1817
+ } catch {
1818
+ }
1819
+ }
1820
+ return out;
1821
+ }
1822
+
1823
+ // src/tokens/race-tokens.ts
1824
+ function isStall(r) {
1825
+ return "stall" in r;
1826
+ }
1827
+ var SCALAR_READERS = {
1828
+ claude: sumTokens,
1829
+ codex: sumCodexTokens,
1830
+ gemini: sumGeminiTokens
1831
+ };
1832
+ var BY_CONVERSATION_READERS = {
1833
+ claude: sumTokensByConversation,
1834
+ codex: sumCodexByConversation,
1835
+ gemini: sumGeminiByConversation
1836
+ };
1837
+ function scoreFor(race, t) {
1838
+ return race.counts_input ? t.input + t.output : t.output;
1839
+ }
1840
+ async function readAllSources(race, primary) {
1841
+ const primaryByConv = /* @__PURE__ */ new Map();
1842
+ try {
1843
+ const primaryMap = await BY_CONVERSATION_READERS[primary]();
1844
+ for (const [id, totals] of primaryMap) primaryByConv.set(id, scoreFor(race, totals));
1845
+ } catch (e) {
1846
+ if (e?.code !== "ENOENT") {
1847
+ return { stall: `Can't read ${primary} token usage: ${e?.message ?? String(e)}` };
1848
+ }
1849
+ }
1850
+ const secondary = { claude: 0, codex: 0, gemini: 0 };
1851
+ await Promise.all(
1852
+ MODEL_KEYS.filter((k) => k !== primary).map(async (k) => {
1853
+ secondary[k] = await SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch(() => 0);
1854
+ })
1855
+ );
1856
+ return { secondary, primaryByConv };
1857
+ }
1858
+
1859
+ // src/tokens/primary-cap.ts
1860
+ var PRIMARY_TOP_CONVERSATIONS = 5;
1861
+ function primaryConversationCap(enabled) {
1862
+ return enabled ? PRIMARY_TOP_CONVERSATIONS : Infinity;
1863
+ }
1864
+
1581
1865
  // src/tokens/race-score.ts
1582
1866
  var STALL_THRESHOLD = 5;
1867
+ function zero() {
1868
+ return { claude: 0, codex: 0, gemini: 0 };
1869
+ }
1583
1870
  var RaceScoreTracker = class {
1584
1871
  acked;
1585
1872
  lastGood;
1873
+ primaryConvAcked;
1874
+ primaryConvLast;
1875
+ counted;
1586
1876
  seq;
1587
1877
  stalls = 0;
1588
- constructor(init) {
1589
- this.acked = init.ackedReading;
1590
- this.lastGood = init.lastGoodReading;
1878
+ lastStall = null;
1879
+ primary;
1880
+ primaryTop5;
1881
+ constructor(init, primary, primaryTop5) {
1882
+ this.acked = { ...init.acked };
1883
+ this.lastGood = { ...init.lastGood };
1884
+ this.primaryConvAcked = { ...init.primaryConvAcked };
1885
+ this.primaryConvLast = { ...init.primaryConvAcked };
1886
+ this.counted = init.primaryCounted;
1591
1887
  this.seq = init.seq;
1888
+ this.primary = primary;
1889
+ this.primaryTop5 = primaryTop5;
1592
1890
  }
1593
1891
  /**
1594
1892
  * Record a scan result.
1595
- * - `null` scan failed/timed-out/missing dir: a stall (counts toward the warning), anchors untouched.
1596
- * - `0` readable but empty: the read mechanism works (resets the stall counter), but we never anchor to 0.
1597
- * - `> 0` real reading; follows up or down.
1893
+ * - `null` or a `{ stall }` reading → stall (warning), anchors untouched. A
1894
+ * stall reading also captures its cause for the UI.
1895
+ * - otherwisesecondaries advance scalar lastGood (never down to 0); the
1896
+ * primary's per-conversation latest readings are updated (monotonic).
1598
1897
  */
1599
1898
  recordReading(reading) {
1600
- if (reading === null) {
1899
+ if (reading === null || isStall(reading)) {
1601
1900
  this.stalls += 1;
1901
+ this.lastStall = reading?.stall ?? null;
1602
1902
  return;
1603
1903
  }
1604
1904
  this.stalls = 0;
1605
- if (reading > 0) this.lastGood = reading;
1905
+ this.lastStall = null;
1906
+ for (const key of MODEL_KEYS) {
1907
+ if (key === this.primary) continue;
1908
+ const v = reading.secondary[key];
1909
+ if (v > 0) this.lastGood[key] = v;
1910
+ }
1911
+ for (const [id, v] of reading.primaryByConv) {
1912
+ const prev = this.primaryConvLast[id] ?? 0;
1913
+ if (v > prev) this.primaryConvLast[id] = v;
1914
+ }
1606
1915
  }
1607
1916
  /** Frozen payload for the next heartbeat. Pure — call repeatedly for retries. */
1608
1917
  nextBeat() {
1609
- return { seq: this.seq + 1, delta: Math.max(0, this.lastGood - this.acked), reading: this.lastGood };
1918
+ const components = zero();
1919
+ for (const key of MODEL_KEYS) {
1920
+ if (key === this.primary) continue;
1921
+ components[key] = Math.max(0, this.lastGood[key] - this.acked[key]);
1922
+ }
1923
+ const pending = [];
1924
+ for (const [id, last] of Object.entries(this.primaryConvLast)) {
1925
+ const d = Math.max(0, last - (this.primaryConvAcked[id] ?? 0));
1926
+ if (d > 0) pending.push(d);
1927
+ }
1928
+ pending.sort((a, b) => b - a);
1929
+ const cap = primaryConversationCap(this.primaryTop5);
1930
+ const take = cap === Infinity ? pending.length : Math.min(cap, pending.length);
1931
+ let primarySum = 0;
1932
+ for (const d of pending.slice(0, take)) primarySum += d;
1933
+ components[this.primary] = primarySum;
1934
+ return {
1935
+ seq: this.seq + 1,
1936
+ components,
1937
+ readings: { ...this.lastGood },
1938
+ primaryConvReadings: { ...this.primaryConvLast }
1939
+ };
1610
1940
  }
1611
1941
  /** Commit a heartbeat the server accepted. `serverLastSeq` self-heals drift. */
1612
1942
  ack(snapshot, serverLastSeq) {
1613
- this.acked = snapshot.reading;
1943
+ for (const key of MODEL_KEYS) {
1944
+ if (key === this.primary) continue;
1945
+ this.acked[key] = snapshot.readings[key];
1946
+ }
1947
+ this.primaryConvAcked = { ...snapshot.primaryConvReadings };
1948
+ this.counted += snapshot.components[this.primary];
1614
1949
  this.seq = Math.max(snapshot.seq, serverLastSeq);
1615
1950
  }
1616
- /** Pin the anchor to the latest reading so the next delta is 0 (used while a race is pending). */
1951
+ /** Pin anchors to the latest readings so the next deltas are 0 (pending race). */
1617
1952
  reprime() {
1618
- this.acked = this.lastGood;
1953
+ for (const key of MODEL_KEYS) {
1954
+ if (key === this.primary) continue;
1955
+ this.acked[key] = this.lastGood[key];
1956
+ }
1957
+ this.primaryConvAcked = { ...this.primaryConvLast };
1619
1958
  }
1620
1959
  get stalled() {
1621
1960
  return this.stalls >= STALL_THRESHOLD;
1622
1961
  }
1962
+ /** Human-readable cause of the most recent stall (null once a good read recovers). */
1963
+ get stallReason() {
1964
+ return this.lastStall;
1965
+ }
1966
+ /** Cumulative primary tokens credited so far (for the UI's primary "since join" row). */
1967
+ primaryCounted() {
1968
+ return this.counted;
1969
+ }
1970
+ /** Secondary "since join" totals = lastGood − baseline (for the UI). Primary key is 0 here. */
1971
+ secondarySinceJoin(baseline) {
1972
+ const out = zero();
1973
+ for (const key of MODEL_KEYS) {
1974
+ if (key === this.primary) continue;
1975
+ out[key] = Math.max(0, this.lastGood[key] - baseline[key]);
1976
+ }
1977
+ return out;
1978
+ }
1623
1979
  toState() {
1624
- return { ackedReading: this.acked, lastGoodReading: this.lastGood, seq: this.seq };
1980
+ return {
1981
+ acked: { ...this.acked },
1982
+ lastGood: { ...this.lastGood },
1983
+ primaryConvAcked: { ...this.primaryConvAcked },
1984
+ primaryCounted: this.counted,
1985
+ seq: this.seq
1986
+ };
1625
1987
  }
1626
1988
  };
1627
1989
 
1628
1990
  // src/runtime/run-race.tsx
1629
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1991
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1630
1992
  function RunRace({ active, initialState, pendingMode, ownUserName }) {
1631
1993
  const { exit } = useApp();
1632
- const [race, setRace] = useState4(null);
1633
- const [lastHbAt, setLastHbAt] = useState4(null);
1634
- const [lastHbOk, setLastHbOk] = useState4(true);
1635
- const [tickNow, setTickNow] = useState4(/* @__PURE__ */ new Date());
1636
- const [fatalError, setFatalError] = useState4(null);
1637
- const [achievements, setAchievements] = useState4([]);
1994
+ const [race, setRace] = useState5(null);
1995
+ const [lastHbAt, setLastHbAt] = useState5(null);
1996
+ const [lastHbOk, setLastHbOk] = useState5(true);
1997
+ const [tickNow, setTickNow] = useState5(/* @__PURE__ */ new Date());
1998
+ const [fatalError, setFatalError] = useState5(null);
1999
+ const [achievements, setAchievements] = useState5([]);
1638
2000
  const shownAchievementAtRef = useRef(0);
1639
- const trackerRef = useRef(new RaceScoreTracker(initialState));
2001
+ const trackerRef = useRef(new RaceScoreTracker(initialState, active.primary_model, active.primary_top5 ?? false));
1640
2002
  const pendingRef = useRef(pendingMode);
1641
2003
  const ctrl = useRef(new AbortController());
1642
- const [stalled, setStalled] = useState4(false);
2004
+ const [stalled, setStalled] = useState5(false);
2005
+ const [stallReason, setStallReason] = useState5(null);
2006
+ const baselineRef = useRef(initialState.acked);
2007
+ const [perSource, setPerSource] = useState5({ claude: 0, codex: 0, gemini: 0 });
1643
2008
  useEffect2(() => {
1644
2009
  const t = setInterval(() => setTickNow(/* @__PURE__ */ new Date()), 1e3);
1645
2010
  return () => clearInterval(t);
@@ -1655,32 +2020,39 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
1655
2020
  const scanWithTimeout = async () => {
1656
2021
  try {
1657
2022
  return await Promise.race([
1658
- sumTokensForRace(active),
2023
+ readAllSources(active, active.primary_model),
1659
2024
  new Promise((_, reject) => setTimeout(() => reject(new Error("scan timeout")), 1e4))
1660
2025
  ]);
1661
2026
  } catch {
1662
- return null;
2027
+ return { stall: "Token scan timed out" };
1663
2028
  }
1664
2029
  };
1665
2030
  runHeartbeatLoop({
1666
2031
  prepareBeat: async () => {
1667
2032
  const reading = await scanWithTimeout();
1668
2033
  tracker.recordReading(reading);
1669
- if (pendingRef.current) tracker.reprime();
2034
+ if (pendingRef.current && !isStall(reading)) tracker.reprime();
1670
2035
  setStalled(tracker.stalled);
2036
+ setStallReason(tracker.stalled ? tracker.stallReason : null);
2037
+ const since = tracker.secondarySinceJoin(baselineRef.current);
2038
+ const ps = { claude: 0, codex: 0, gemini: 0 };
2039
+ for (const k of MODEL_KEYS) {
2040
+ ps[k] = k === active.primary_model ? tracker.primaryCounted() : since[k];
2041
+ }
2042
+ setPerSource(ps);
1671
2043
  return tracker.nextBeat();
1672
2044
  },
1673
2045
  sendBeat: async (snapshot) => {
1674
2046
  return heartbeat(active.join_code, active.horse_id, active.heartbeat_token, {
1675
2047
  seq: snapshot.seq,
1676
- delta: snapshot.delta
2048
+ components: snapshot.components
1677
2049
  });
1678
2050
  },
1679
2051
  onSuccess: (resp, snapshot) => {
1680
2052
  tracker.ack(snapshot, resp.last_seq);
1681
2053
  const updated = {
1682
2054
  ...active,
1683
- ...tracker.toState(),
2055
+ score: tracker.toState(),
1684
2056
  last_heartbeat_at: (/* @__PURE__ */ new Date()).toISOString()
1685
2057
  };
1686
2058
  void saveActiveRace(updated);
@@ -1717,13 +2089,13 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
1717
2089
  }, []);
1718
2090
  const lastHeartbeatAgoSec = lastHbAt ? Math.max(0, Math.floor((tickNow.getTime() - lastHbAt.getTime()) / 1e3)) : null;
1719
2091
  if (fatalError) {
1720
- return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", padding: 1, children: [
1721
- /* @__PURE__ */ jsx7(Text7, { color: "red", bold: true, children: "CLI version mismatch \u2014 disconnected" }),
1722
- /* @__PURE__ */ jsx7(Text7, { children: fatalError })
2092
+ return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", padding: 1, children: [
2093
+ /* @__PURE__ */ jsx8(Text8, { color: "red", bold: true, children: "CLI version mismatch \u2014 disconnected" }),
2094
+ /* @__PURE__ */ jsx8(Text8, { children: fatalError })
1723
2095
  ] });
1724
2096
  }
1725
- return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
1726
- /* @__PURE__ */ jsx7(
2097
+ return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
2098
+ /* @__PURE__ */ jsx8(
1727
2099
  StatusScreen,
1728
2100
  {
1729
2101
  race,
@@ -1733,26 +2105,30 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
1733
2105
  ownUserName,
1734
2106
  lastHeartbeatAgoSec,
1735
2107
  lastHeartbeatOk: lastHbOk,
1736
- stalled
2108
+ stalled,
2109
+ stallReason,
2110
+ primaryModel: active.primary_model,
2111
+ perSource,
2112
+ primaryCapped: primaryConversationCap(active.primary_top5 ?? false) !== Infinity
1737
2113
  }
1738
2114
  ),
1739
- achievements.length > 0 && /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
1740
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Achievements" }),
2115
+ achievements.length > 0 && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", marginTop: 1, children: [
2116
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Achievements" }),
1741
2117
  achievements.map(({ key, event }) => {
1742
2118
  const description = describeAchievement(event, active);
1743
- return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "row", children: [
1744
- /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
2119
+ return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "row", children: [
2120
+ /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
1745
2121
  " ",
1746
2122
  formatClockTime(event.at),
1747
2123
  " "
1748
2124
  ] }),
1749
- /* @__PURE__ */ jsxs5(Text7, { color: "yellow", bold: true, children: [
2125
+ /* @__PURE__ */ jsxs6(Text8, { color: "yellow", bold: true, children: [
1750
2126
  "+",
1751
2127
  event.xp,
1752
2128
  " XP "
1753
2129
  ] }),
1754
- /* @__PURE__ */ jsx7(Text7, { children: event.name }),
1755
- /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
2130
+ /* @__PURE__ */ jsx8(Text8, { children: event.name }),
2131
+ /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
1756
2132
  " \u2014 ",
1757
2133
  description
1758
2134
  ] })
@@ -1778,25 +2154,54 @@ function raceViewFrom(resp) {
1778
2154
  };
1779
2155
  }
1780
2156
  async function buildInitialState(args) {
1781
- let diskNow = 0;
2157
+ let secondary = { claude: 0, codex: 0, gemini: 0 };
2158
+ const primaryConvAcked = {};
1782
2159
  try {
1783
- diskNow = await sumTokensForRace(args.active);
2160
+ const now = await readAllSources(args.active, args.active.primary_model);
2161
+ if (!isStall(now)) {
2162
+ secondary = now.secondary;
2163
+ for (const [id, v] of now.primaryByConv) primaryConvAcked[id] = v;
2164
+ }
1784
2165
  } catch {
1785
- diskNow = 0;
1786
2166
  }
1787
2167
  return {
1788
- initialState: { ackedReading: diskNow, lastGoodReading: diskNow, seq: args.serverLastSeq },
2168
+ initialState: {
2169
+ acked: { ...secondary },
2170
+ lastGood: { ...secondary },
2171
+ primaryConvAcked,
2172
+ primaryCounted: 0,
2173
+ seq: args.serverLastSeq
2174
+ },
1789
2175
  pendingMode: args.raceStatus === "pending"
1790
2176
  };
1791
2177
  }
1792
2178
 
1793
2179
  // src/commands/join.ts
1794
- async function joinCommand(joinCode) {
2180
+ function parsePrimaryFlag(argv) {
2181
+ for (let i = 0; i < argv.length; i++) {
2182
+ const a = argv[i];
2183
+ let value;
2184
+ if (a === "--primary") value = argv[i + 1];
2185
+ else if (a.startsWith("--primary=")) value = a.slice("--primary=".length);
2186
+ else continue;
2187
+ if (!isModelKey(value)) throw new Error(`--primary must be one of claude, codex, gemini (got ${value ?? ""})`);
2188
+ return value;
2189
+ }
2190
+ return null;
2191
+ }
2192
+ async function joinCommand(joinCode, argv = []) {
1795
2193
  if (!joinCode) {
1796
2194
  console.error("Usage: token-derby join <join-code>");
1797
2195
  return 2;
1798
2196
  }
1799
2197
  const code = joinCode.toUpperCase();
2198
+ let primaryFlag;
2199
+ try {
2200
+ primaryFlag = parsePrimaryFlag(argv);
2201
+ } catch (e) {
2202
+ console.error(e.message);
2203
+ return 2;
2204
+ }
1800
2205
  const identity = await loadIdentity();
1801
2206
  if (!identity) {
1802
2207
  console.error("Run `token-derby init` to set up your identity.");
@@ -1860,9 +2265,14 @@ async function joinCommand(joinCode) {
1860
2265
  chosenName = picked.name;
1861
2266
  chosenColors = picked.colors;
1862
2267
  }
2268
+ let chosenPrimary = "claude";
2269
+ if (!ownHorse) {
2270
+ if (primaryFlag) chosenPrimary = primaryFlag;
2271
+ else if (process.stdout.isTTY) chosenPrimary = await pickPrimary();
2272
+ }
1863
2273
  let joinResp;
1864
2274
  try {
1865
- joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId });
2275
+ joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId, primary_model: chosenPrimary });
1866
2276
  } catch (e) {
1867
2277
  if (e instanceof ApiError) {
1868
2278
  if (e.code === "RACE_FULL") console.error("This race is full.");
@@ -1887,22 +2297,28 @@ async function joinCommand(joinCode) {
1887
2297
  horse_name: chosenName,
1888
2298
  horse_colors: chosenColors,
1889
2299
  joined_at: ownHorse?.joined_at ?? (/* @__PURE__ */ new Date()).toISOString(),
1890
- ackedReading: 0,
1891
- lastGoodReading: 0,
1892
- seq: ownHorse?.last_seq ?? 0,
1893
2300
  last_heartbeat_at: (/* @__PURE__ */ new Date(0)).toISOString(),
1894
- ...race.counts_input ? { counts_input: true } : {}
2301
+ primary_model: joinResp.primary_model,
2302
+ score: {
2303
+ acked: { claude: 0, codex: 0, gemini: 0 },
2304
+ lastGood: { claude: 0, codex: 0, gemini: 0 },
2305
+ primaryConvAcked: {},
2306
+ primaryCounted: 0,
2307
+ seq: ownHorse?.last_seq ?? 0
2308
+ },
2309
+ ...race.counts_input ? { counts_input: true } : {},
2310
+ ...race.primary_top5 ? { primary_top5: true } : {}
1895
2311
  };
1896
2312
  await saveActiveRace(active);
1897
2313
  const initial = await buildInitialState({ active, raceStatus: status, serverLastSeq: ownHorse?.last_seq ?? 0 });
1898
- const app = render4(React8.createElement(RunRace, { active, initialState: initial.initialState, pendingMode: initial.pendingMode, ownUserName: identity.display_name }));
2314
+ const app = render4(React9.createElement(RunRace, { active, initialState: initial.initialState, pendingMode: initial.pendingMode, ownUserName: identity.display_name }));
1899
2315
  await app.waitUntilExit();
1900
2316
  return 0;
1901
2317
  }
1902
2318
  async function pickHorse(horses) {
1903
2319
  return new Promise((resolve) => {
1904
2320
  const app = render4(
1905
- React8.createElement(HorsePicker, {
2321
+ React9.createElement(HorsePicker, {
1906
2322
  horses,
1907
2323
  onPick: (h) => {
1908
2324
  app.unmount();
@@ -1916,6 +2332,18 @@ async function pickHorse(horses) {
1916
2332
  );
1917
2333
  });
1918
2334
  }
2335
+ async function pickPrimary() {
2336
+ return new Promise((resolve) => {
2337
+ const app = render4(
2338
+ React9.createElement(PrimaryPicker, {
2339
+ onPick: (m) => {
2340
+ app.unmount();
2341
+ resolve(m);
2342
+ }
2343
+ })
2344
+ );
2345
+ });
2346
+ }
1919
2347
 
1920
2348
  // src/commands/end.ts
1921
2349
  import * as readline3 from "readline/promises";
@@ -2114,30 +2542,30 @@ function runNpmUpgrade(spawnImpl) {
2114
2542
  }
2115
2543
 
2116
2544
  // src/commands/roll.ts
2117
- import React12 from "react";
2545
+ import React13 from "react";
2118
2546
  import { render as render5 } from "ink";
2119
2547
 
2120
2548
  // src/ui/RollReveal.tsx
2121
- import { useState as useState6, useEffect as useEffect4, useMemo } from "react";
2122
- import { Box as Box9, Text as Text9 } from "ink";
2549
+ import { useState as useState7, useEffect as useEffect4, useMemo } from "react";
2550
+ import { Box as Box10, Text as Text10 } from "ink";
2123
2551
 
2124
2552
  // src/ui/HatSprite.tsx
2125
- import { useEffect as useEffect3, useState as useState5 } from "react";
2126
- import { Box as Box8, Text as Text8 } from "ink";
2127
- import { jsx as jsx8 } from "react/jsx-runtime";
2553
+ import { useEffect as useEffect3, useState as useState6 } from "react";
2554
+ import { Box as Box9, Text as Text9 } from "ink";
2555
+ import { jsx as jsx9 } from "react/jsx-runtime";
2128
2556
  function HatSprite({ hat, variant, centerIn }) {
2129
2557
  const colors = hatColorsFor2(hat, variant ?? 0);
2130
2558
  const grid = makeHatGrid(hat, colors, centerIn);
2131
2559
  const lines = hexGridToHalfBlocks(grid);
2132
- return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx8(Text8, { children: line }, i)) });
2560
+ return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx9(Text9, { children: line }, i)) });
2133
2561
  }
2134
2562
  function AnimatedHatSprite({ hat, variant, centerIn }) {
2135
2563
  if (hat.rarity !== "legendary") {
2136
- return /* @__PURE__ */ jsx8(HatSprite, { hat, variant, centerIn });
2564
+ return /* @__PURE__ */ jsx9(HatSprite, { hat, variant, centerIn });
2137
2565
  }
2138
2566
  const frames = hat.animation.frames;
2139
2567
  const fps = hat.animation.fps;
2140
- const [idx, setIdx] = useState5(0);
2568
+ const [idx, setIdx] = useState6(0);
2141
2569
  useEffect3(() => {
2142
2570
  if (frames.length <= 1) return;
2143
2571
  const interval = setInterval(
@@ -2147,7 +2575,7 @@ function AnimatedHatSprite({ hat, variant, centerIn }) {
2147
2575
  return () => clearInterval(interval);
2148
2576
  }, [frames.length, fps]);
2149
2577
  const framed = { ...hat, colors: { ...hat.colors, A: frames[idx] } };
2150
- return /* @__PURE__ */ jsx8(HatSprite, { hat: framed, variant, centerIn });
2578
+ return /* @__PURE__ */ jsx9(HatSprite, { hat: framed, variant, centerIn });
2151
2579
  }
2152
2580
  function hatColorsFor2(hat, variantIdx) {
2153
2581
  if (hat.rarity === "legendary") return hat.colors;
@@ -2174,7 +2602,7 @@ function makeHatGrid(hat, colors, centerIn) {
2174
2602
  }
2175
2603
 
2176
2604
  // src/ui/RollReveal.tsx
2177
- import { jsx as jsx9 } from "react/jsx-runtime";
2605
+ import { jsx as jsx10 } from "react/jsx-runtime";
2178
2606
  var RESET3 = "\x1B[0m";
2179
2607
  var BOX_COLOR = "#E5C76B";
2180
2608
  var TIER_PALETTE = {
@@ -2243,7 +2671,7 @@ var BOX_EMPTY = [
2243
2671
  ""
2244
2672
  ].map(pad);
2245
2673
  function GiftBox({ frame, color }) {
2246
- return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx9(Text9, { children: line ? ansiFg(color) + line + RESET3 : line }, i)) });
2674
+ return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx10(Text10, { children: line ? ansiFg(color) + line + RESET3 : line }, i)) });
2247
2675
  }
2248
2676
  function spawnParticles(tier, count, cx, cy) {
2249
2677
  const palette = TIER_PALETTE[tier];
@@ -2266,7 +2694,7 @@ function ConfettiBurst({ tier }) {
2266
2694
  const cx = Math.floor(SCENE_W / 2);
2267
2695
  const cy = Math.floor(SCENE_H / 2);
2268
2696
  const particles = useMemo(() => spawnParticles(tier, 36, cx, cy), [tier, cx, cy]);
2269
- const [tick, setTick] = useState6(0);
2697
+ const [tick, setTick] = useState7(0);
2270
2698
  useEffect4(() => {
2271
2699
  const i = setInterval(() => setTick((t) => t + 1), 70);
2272
2700
  return () => clearInterval(i);
@@ -2279,13 +2707,13 @@ function ConfettiBurst({ tier }) {
2279
2707
  grid[y][x] = ansiFg(p.color) + p.char + RESET3;
2280
2708
  }
2281
2709
  }
2282
- return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: grid.map((row, y) => /* @__PURE__ */ jsx9(Text9, { children: row.join("") }, y)) });
2710
+ return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: grid.map((row, y) => /* @__PURE__ */ jsx10(Text10, { children: row.join("") }, y)) });
2283
2711
  }
2284
2712
  var CLOSED_HOLD_MS = 3e3;
2285
2713
  function RollReveal({ outcome, onDone }) {
2286
2714
  const isNoHat = outcome.kind === "no_hat";
2287
2715
  const isLegendary = outcome.kind !== "no_hat" && outcome.hat.rarity === "legendary";
2288
- const [phase, setPhase] = useState6("closed");
2716
+ const [phase, setPhase] = useState7("closed");
2289
2717
  useEffect4(() => {
2290
2718
  const timers = [];
2291
2719
  timers.push(setTimeout(() => setPhase("open1"), CLOSED_HOLD_MS));
@@ -2300,24 +2728,24 @@ function RollReveal({ outcome, onDone }) {
2300
2728
  }
2301
2729
  return () => timers.forEach(clearTimeout);
2302
2730
  }, [isNoHat, isLegendary, onDone]);
2303
- if (phase === "closed") return /* @__PURE__ */ jsx9(GiftBox, { frame: BOX_CLOSED, color: BOX_COLOR });
2304
- if (phase === "open1") return /* @__PURE__ */ jsx9(GiftBox, { frame: BOX_OPENING_1, color: BOX_COLOR });
2305
- if (phase === "open2") return /* @__PURE__ */ jsx9(GiftBox, { frame: BOX_OPENING_2, color: BOX_COLOR });
2306
- if (phase === "empty") return /* @__PURE__ */ jsx9(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
2731
+ if (phase === "closed") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_CLOSED, color: BOX_COLOR });
2732
+ if (phase === "open1") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_1, color: BOX_COLOR });
2733
+ if (phase === "open2") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_2, color: BOX_COLOR });
2734
+ if (phase === "empty") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
2307
2735
  if (phase === "burst" && outcome.kind !== "no_hat") {
2308
- return /* @__PURE__ */ jsx9(ConfettiBurst, { tier: outcome.hat.rarity });
2736
+ return /* @__PURE__ */ jsx10(ConfettiBurst, { tier: outcome.hat.rarity });
2309
2737
  }
2310
- if (outcome.kind === "no_hat") return /* @__PURE__ */ jsx9(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
2311
- return outcome.hat.rarity === "legendary" ? /* @__PURE__ */ jsx9(AnimatedHatSprite, { hat: outcome.hat, centerIn: { w: SCENE_W, h: SCENE_H } }) : /* @__PURE__ */ jsx9(HatSprite, { hat: outcome.hat, variant: outcome.variant, centerIn: { w: SCENE_W, h: SCENE_H } });
2738
+ if (outcome.kind === "no_hat") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
2739
+ return outcome.hat.rarity === "legendary" ? /* @__PURE__ */ jsx10(AnimatedHatSprite, { hat: outcome.hat, centerIn: { w: SCENE_W, h: SCENE_H } }) : /* @__PURE__ */ jsx10(HatSprite, { hat: outcome.hat, variant: outcome.variant, centerIn: { w: SCENE_W, h: SCENE_H } });
2312
2740
  }
2313
2741
 
2314
2742
  // src/ui/RollHorsePicker.tsx
2315
- import { useState as useState7 } from "react";
2316
- import { Box as Box10, Text as Text10, useInput as useInput3 } from "ink";
2317
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2743
+ import { useState as useState8 } from "react";
2744
+ import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
2745
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2318
2746
  function RollHorsePicker({ horses, onPick, onCancel }) {
2319
- const [idx, setIdx] = useState7(0);
2320
- useInput3((input, key) => {
2747
+ const [idx, setIdx] = useState8(0);
2748
+ useInput4((input, key) => {
2321
2749
  if (key.escape) {
2322
2750
  onCancel();
2323
2751
  return;
@@ -2336,33 +2764,33 @@ function RollHorsePicker({ horses, onPick, onCancel }) {
2336
2764
  return;
2337
2765
  }
2338
2766
  });
2339
- return /* @__PURE__ */ jsxs6(Box10, { flexDirection: "column", children: [
2340
- /* @__PURE__ */ jsx10(Text10, { children: "Pick a horse to roll for:" }),
2341
- horses.map((h, i) => /* @__PURE__ */ jsxs6(Box10, { flexDirection: "column", children: [
2342
- /* @__PURE__ */ jsx10(Box10, { flexDirection: "row", children: /* @__PURE__ */ jsxs6(Text10, { children: [
2767
+ return /* @__PURE__ */ jsxs7(Box11, { flexDirection: "column", children: [
2768
+ /* @__PURE__ */ jsx11(Text11, { children: "Pick a horse to roll for:" }),
2769
+ horses.map((h, i) => /* @__PURE__ */ jsxs7(Box11, { flexDirection: "column", children: [
2770
+ /* @__PURE__ */ jsx11(Box11, { flexDirection: "row", children: /* @__PURE__ */ jsxs7(Text11, { children: [
2343
2771
  i === idx ? "\u25BA" : " ",
2344
2772
  " ",
2345
2773
  h.name,
2346
2774
  " ",
2347
- /* @__PURE__ */ jsxs6(Text10, { color: "cyan", children: [
2775
+ /* @__PURE__ */ jsxs7(Text11, { color: "cyan", children: [
2348
2776
  "[Lvl. ",
2349
2777
  levelFromXp(h.xp),
2350
2778
  "]"
2351
2779
  ] }),
2352
2780
  " ",
2353
- /* @__PURE__ */ jsxs6(Text10, { color: "yellow", children: [
2781
+ /* @__PURE__ */ jsxs7(Text11, { color: "yellow", children: [
2354
2782
  "\u2014 ",
2355
2783
  h.pending,
2356
2784
  " roll",
2357
2785
  h.pending === 1 ? "" : "s"
2358
2786
  ] })
2359
2787
  ] }) }),
2360
- /* @__PURE__ */ jsxs6(Box10, { flexDirection: "row", children: [
2361
- /* @__PURE__ */ jsx10(Text10, { children: " " }),
2362
- /* @__PURE__ */ jsx10(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
2788
+ /* @__PURE__ */ jsxs7(Box11, { flexDirection: "row", children: [
2789
+ /* @__PURE__ */ jsx11(Text11, { children: " " }),
2790
+ /* @__PURE__ */ jsx11(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
2363
2791
  ] })
2364
2792
  ] }, h.stable_horse_id)),
2365
- /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
2793
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
2366
2794
  ] });
2367
2795
  }
2368
2796
 
@@ -2391,7 +2819,7 @@ function resetStdinAfterInk() {
2391
2819
  }
2392
2820
  async function runReveal(outcome) {
2393
2821
  await new Promise((resolve) => {
2394
- const app = render5(React12.createElement(RollReveal, {
2822
+ const app = render5(React13.createElement(RollReveal, {
2395
2823
  outcome,
2396
2824
  onDone: () => {
2397
2825
  app.unmount();
@@ -2417,7 +2845,7 @@ async function rollCommand() {
2417
2845
  return 0;
2418
2846
  }
2419
2847
  const picked = await new Promise((resolve) => {
2420
- const app = render5(React12.createElement(RollHorsePicker, {
2848
+ const app = render5(React13.createElement(RollHorsePicker, {
2421
2849
  horses: eligible,
2422
2850
  onPick: (h) => {
2423
2851
  app.unmount();
@@ -2682,7 +3110,7 @@ function flag(args, name) {
2682
3110
  }
2683
3111
  return void 0;
2684
3112
  }
2685
- var USAGE = 'Usage: token-derby organisation schedule set <org-name> --days mon-fri --start 09:00 --end 17:30 --tz Europe/London [--name "Daily"] [--max 30] [--counts-input]';
3113
+ var USAGE = 'Usage: token-derby organisation schedule set <org-name> --days mon-fri --start 09:00 --end 17:30 --tz Europe/London [--name "Daily"] [--max 30] [--counts-input] [--primary-top5]';
2686
3114
  async function orgScheduleSetCommand(orgName, rest) {
2687
3115
  if (!orgName) {
2688
3116
  console.error(USAGE);
@@ -2695,6 +3123,7 @@ async function orgScheduleSetCommand(orgName, rest) {
2695
3123
  const name = flag(rest, "--name");
2696
3124
  const maxStr = flag(rest, "--max");
2697
3125
  const counts_input = rest.includes("--counts-input");
3126
+ const primary_top5 = rest.includes("--primary-top5");
2698
3127
  if (!daysSpec || !start || !end || !tz) {
2699
3128
  console.error("Required flags: --days, --start, --end, --tz");
2700
3129
  console.error(USAGE);
@@ -2721,10 +3150,12 @@ async function orgScheduleSetCommand(orgName, rest) {
2721
3150
  tz,
2722
3151
  ...name ? { race_name: name } : {},
2723
3152
  ...max_participants !== void 0 ? { max_participants } : {},
2724
- ...counts_input ? { counts_input: true } : {}
3153
+ ...counts_input ? { counts_input: true } : {},
3154
+ ...primary_top5 ? { primary_top5: true } : {}
2725
3155
  });
2726
3156
  const s = resp.schedule;
2727
3157
  console.log(`Schedule set for ${orgName}: days [${s.weekdays.join(",")}] ${s.start_local}\u2013${s.end_local} ${s.tz}`);
3158
+ if (s.primary_top5) console.log(" Primary score counts only each racer's top 5 conversations per beat.");
2728
3159
  return 0;
2729
3160
  } catch (e) {
2730
3161
  if (e instanceof ApiError) {
@@ -2746,6 +3177,7 @@ async function orgScheduleGetCommand(orgName) {
2746
3177
  if (resp.schedule) {
2747
3178
  const s = resp.schedule;
2748
3179
  console.log(`Schedule for ${orgName}: days [${s.weekdays.join(",")}] ${s.start_local}\u2013${s.end_local} ${s.tz}`);
3180
+ if (s.primary_top5) console.log(" Primary top-5 conversations cap: ON.");
2749
3181
  } else {
2750
3182
  console.log(`No schedule configured for ${orgName}.`);
2751
3183
  }
@@ -2896,7 +3328,7 @@ async function main() {
2896
3328
  const orgName = parseFlag(argv.slice(1), "--organisation");
2897
3329
  return createRaceCommand(orgName);
2898
3330
  }
2899
- if (cmd === "join") return joinCommand(argv[1]);
3331
+ if (cmd === "join") return joinCommand(argv[1], argv.slice(2));
2900
3332
  if (cmd === "end") return endCommand(argv[1]);
2901
3333
  if (cmd === "roll") return rollCommand();
2902
3334
  console.error(`Unknown command: ${cmd}`);