@mauricode/token-derby 2.12.3 → 2.13.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/README.md CHANGED
@@ -11,7 +11,7 @@ Token Derby runs on the **real** output tokens your Claude Code produces — tha
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- npm i -g @mauricode/token-derby
14
+ npm i -g @mauricode/token-derby@latest
15
15
  ```
16
16
 
17
17
  Requires Node 20+.
@@ -64,6 +64,16 @@ The CLI sums `message.usage.output_tokens` across every `*.jsonl` under `~/.clau
64
64
 
65
65
  Races can optionally also count *fresh input tokens* — i.e. `input_tokens + cache_creation_input_tokens` (your new context this turn) in addition to output. `cache_read_input_tokens` is never counted, since those reflect passive context size rather than work. The race creator opts in at `token-derby create` time; thresholds for Stampede!, Pulled Away!, and the heartbeat rate cap scale 10× in these races so the achievement cadence stays comparable.
66
66
 
67
+ ## Stamina
68
+
69
+ Some races turn on stamina. Push a horse far above a sustainable pace and it tires — its tokens still count, but at a fraction of face value until it recovers. A normal working pace never triggers this; stamina only bites once you're running well past what the race considers sustainable.
70
+
71
+ Staying connected and easing off is how you recover. Disconnecting does not — a long gap since your last heartbeat earns the same recovery as one ordinary tick, not a bonus for the time you were away, so crashing is not a rest strategy. That's on top of the crash penalty above: you lose the tokens from the gap *and* you don't recover any faster for it.
72
+
73
+ Nothing here asks you to hold work back. A flat-out day still beats a lazy one — it just doesn't beat it by as much as the raw token count would suggest once your horse tires.
74
+
75
+ Stamina is off by default; org owners turn it on and tune it from the Race Settings tab of `token-derby web`. When it's on, the live view shows your horse's stamina as a percentage and bar, plus a multiplier once you're actually losing score to fatigue.
76
+
67
77
  ## Other models (Codex, Gemini)
68
78
 
69
79
  At join you pick one **primary** model — Claude, Codex, or Gemini — counted 1:1.
package/dist/bin.js CHANGED
@@ -252,6 +252,39 @@ function toTag(c, y) {
252
252
  }
253
253
  }
254
254
 
255
+ // ../shared/dist/scoring.js
256
+ function scoredOf(horse) {
257
+ return horse.scored_tokens ?? horse.current_tokens;
258
+ }
259
+ var STAMINA = {
260
+ SUSTAINABLE_PACE: 4e3,
261
+ DRAIN_PER_MIN: 4,
262
+ MAX_DRAIN_PER_MIN: 6,
263
+ RECOVER_PER_MIN: 2,
264
+ RECOVER_TICK_CAP_MS: 9e4,
265
+ TAPER_FLOOR: 25,
266
+ TIRED_MULTIPLIER: 0.5
267
+ };
268
+ function resolveStaminaConfig(race) {
269
+ const c = race.stamina_config ?? {};
270
+ return {
271
+ sustainable_pace: c.sustainable_pace ?? STAMINA.SUSTAINABLE_PACE,
272
+ drain_per_min: c.drain_per_min ?? STAMINA.DRAIN_PER_MIN,
273
+ max_drain_per_min: c.max_drain_per_min ?? STAMINA.MAX_DRAIN_PER_MIN,
274
+ recover_per_min: c.recover_per_min ?? STAMINA.RECOVER_PER_MIN,
275
+ taper_floor: c.taper_floor ?? STAMINA.TAPER_FLOOR,
276
+ tired_multiplier: c.tired_multiplier ?? STAMINA.TIRED_MULTIPLIER
277
+ };
278
+ }
279
+ var STAMINA_PARAM_BOUNDS = {
280
+ sustainable_pace: { min: 1e3, max: 2e4, step: 250, default: STAMINA.SUSTAINABLE_PACE },
281
+ drain_per_min: { min: 1, max: 12, step: 1, default: STAMINA.DRAIN_PER_MIN },
282
+ max_drain_per_min: { min: 2, max: 20, step: 1, default: STAMINA.MAX_DRAIN_PER_MIN },
283
+ recover_per_min: { min: 1, max: 8, step: 1, default: STAMINA.RECOVER_PER_MIN },
284
+ taper_floor: { min: 10, max: 60, step: 5, default: STAMINA.TAPER_FLOOR },
285
+ tired_multiplier: { min: 0.2, max: 0.9, step: 0.05, default: STAMINA.TIRED_MULTIPLIER }
286
+ };
287
+
255
288
  // src/ui/HorseSprite.tsx
256
289
  import { Box, Text } from "ink";
257
290
 
@@ -788,8 +821,8 @@ var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
788
821
  // src/version.ts
789
822
  import { createRequire } from "module";
790
823
  function readVersion() {
791
- if ("2.12.3".length > 0) {
792
- return "2.12.3";
824
+ if ("2.13.0".length > 0) {
825
+ return "2.13.0";
793
826
  }
794
827
  try {
795
828
  const req = createRequire(import.meta.url);
@@ -1326,6 +1359,8 @@ async function createRaceCommand(organisationName) {
1326
1359
  const counts_input = countInputRaw === "y" || countInputRaw === "yes";
1327
1360
  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
1361
  const primary_top5 = top5Raw === "y" || top5Raw === "yes";
1362
+ const staminaRaw = (await rl.question("Stamina \u2014 horses that run flat out tire and score less until they recover? [y/N]: ")).trim().toLowerCase();
1363
+ const stamina = staminaRaw === "y" || staminaRaw === "yes";
1329
1364
  const resp = await createRace({
1330
1365
  name,
1331
1366
  start_time: start,
@@ -1334,7 +1369,8 @@ async function createRaceCommand(organisationName) {
1334
1369
  ...max !== void 0 ? { max_participants: max } : {},
1335
1370
  ...org ? { organisation_name: org } : {},
1336
1371
  ...counts_input ? { counts_input: true } : {},
1337
- ...primary_top5 ? { primary_top5: true } : {}
1372
+ ...primary_top5 ? { primary_top5: true } : {},
1373
+ ...stamina ? { stamina: true } : {}
1338
1374
  });
1339
1375
  console.log("");
1340
1376
  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");
@@ -1353,6 +1389,9 @@ async function createRaceCommand(organisationName) {
1353
1389
  if (primary_top5) {
1354
1390
  console.log(" Primary score counts only each racer's top 5 conversations per beat.");
1355
1391
  }
1392
+ if (stamina) {
1393
+ console.log(" Stamina on \u2014 horses running above a sustainable pace will tire.");
1394
+ }
1356
1395
  console.log(` Share with participants: token-derby join ${resp.join_code}`);
1357
1396
  return 0;
1358
1397
  } catch (e) {
@@ -1415,7 +1454,7 @@ import { Box as Box8, Text as Text8, useApp } from "ink";
1415
1454
 
1416
1455
  // src/ui/StatusScreen.tsx
1417
1456
  import { Box as Box7, Text as Text7 } from "ink";
1418
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1457
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1419
1458
  var MODEL_LABELS = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
1420
1459
  function ModelList(props) {
1421
1460
  const { primaryModel } = props;
@@ -1439,6 +1478,32 @@ function StatusScreen(props) {
1439
1478
  const elapsedPct = elapsed(race);
1440
1479
  const timeLeft = formatDuration(race.time_left_seconds);
1441
1480
  const lvl = levelInfo((own?.xp ?? 0) + (own?.live_xp ?? 0));
1481
+ const divisionField = own?.division === void 0 ? [] : race.horses.filter((h) => h.division === own.division);
1482
+ const divisionRank = own ? divisionField.indexOf(own) + 1 : 0;
1483
+ const showDivision = (race.league_division_names?.length ?? 0) > 0 && divisionRank > 0;
1484
+ const staminaRow = race.stamina === true ? staminaLine(own, race) : null;
1485
+ const rows = [
1486
+ { label: "Tokens (race):", value: String(own?.final_scored_tokens ?? (own ? scoredOf(own) : 0)) },
1487
+ ...staminaRow ? [staminaRow] : [],
1488
+ { label: "Position:", value: `${own?.rank ?? "\u2014"} of ${race.horses.length}` },
1489
+ ...showDivision ? [{ label: "Position (Division):", value: `${divisionRank} of ${divisionField.length}` }] : [],
1490
+ { label: "Leader:", value: leaderText(leader) },
1491
+ ...showDivision ? [{ label: "Leader (Division):", value: leaderText(divisionField[0]) }] : [],
1492
+ { label: "Race elapsed:", value: `${(elapsedPct * 100).toFixed(0)}% ${bar(elapsedPct, 20)}` },
1493
+ { label: "Time left:", value: timeLeft },
1494
+ {
1495
+ label: "XP:",
1496
+ value: 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)}`
1497
+ },
1498
+ {
1499
+ label: "Last heartbeat:",
1500
+ value: /* @__PURE__ */ jsxs5(Fragment2, { children: [
1501
+ lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
1502
+ " ",
1503
+ /* @__PURE__ */ jsx7(Text7, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
1504
+ ] })
1505
+ }
1506
+ ];
1442
1507
  return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
1443
1508
  /* @__PURE__ */ jsxs5(Text7, { children: [
1444
1509
  "\u{1F3C7} TOKEN DERBY \u2500\u2500\u2500 ",
@@ -1470,40 +1535,7 @@ function StatusScreen(props) {
1470
1535
  ] })
1471
1536
  ] }),
1472
1537
  /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
1473
- /* @__PURE__ */ jsxs5(Text7, { children: [
1474
- "Tokens (race): ",
1475
- own?.current_tokens ?? 0
1476
- ] }),
1477
- /* @__PURE__ */ jsxs5(Text7, { children: [
1478
- "Position: ",
1479
- own?.rank ?? "\u2014",
1480
- " of ",
1481
- race.horses.length
1482
- ] }),
1483
- /* @__PURE__ */ jsxs5(Text7, { children: [
1484
- "Leader: ",
1485
- leader ? `${leader.name}${leader.user_name ? ` (${leader.user_name})` : ""} \u2014 ${leader.current_tokens}` : "\u2014"
1486
- ] }),
1487
- /* @__PURE__ */ jsxs5(Text7, { children: [
1488
- "Race elapsed: ",
1489
- (elapsedPct * 100).toFixed(0),
1490
- "% ",
1491
- bar(elapsedPct, 20)
1492
- ] }),
1493
- /* @__PURE__ */ jsxs5(Text7, { children: [
1494
- "Time left: ",
1495
- timeLeft
1496
- ] }),
1497
- /* @__PURE__ */ jsxs5(Text7, { children: [
1498
- "XP: ",
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)}`
1500
- ] }),
1501
- /* @__PURE__ */ jsxs5(Text7, { children: [
1502
- "Last heartbeat: ",
1503
- lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
1504
- " ",
1505
- /* @__PURE__ */ jsx7(Text7, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
1506
- ] }),
1538
+ /* @__PURE__ */ jsx7(StatLines, { rows }),
1507
1539
  stalled && /* @__PURE__ */ jsxs5(Text7, { color: "yellow", children: [
1508
1540
  "\u26A0 ",
1509
1541
  stallReason ?? "Can't read token usage",
@@ -1514,6 +1546,17 @@ function StatusScreen(props) {
1514
1546
  /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
1515
1547
  ] });
1516
1548
  }
1549
+ function StatLines(props) {
1550
+ const width = Math.max(...props.rows.map((r) => r.label.length)) + 1;
1551
+ return /* @__PURE__ */ jsx7(Fragment2, { children: props.rows.map((r) => /* @__PURE__ */ jsxs5(Text7, { children: [
1552
+ r.label.padEnd(width),
1553
+ r.value
1554
+ ] }, r.label)) });
1555
+ }
1556
+ function leaderText(h) {
1557
+ if (!h) return "\u2014";
1558
+ return `${h.name}${h.user_name ? ` (${h.user_name})` : ""} \u2014 ${h.final_scored_tokens ?? scoredOf(h)}`;
1559
+ }
1517
1560
  function elapsed(race) {
1518
1561
  const start = new Date(race.start_time).getTime();
1519
1562
  const end = new Date(race.end_time).getTime();
@@ -1526,6 +1569,22 @@ function bar(pct, width) {
1526
1569
  const filled = Math.round(pct * width);
1527
1570
  return "\u2593".repeat(filled) + "\u2591".repeat(width - filled);
1528
1571
  }
1572
+ function staminaLine(own, race) {
1573
+ const stamina = own?.stamina ?? 100;
1574
+ const cfg = resolveStaminaConfig(race);
1575
+ const floor = cfg.taper_floor;
1576
+ const band = stamina > 50 ? "green" : stamina >= floor ? "amber" : "red";
1577
+ const color = band === "green" ? "green" : band === "amber" ? "yellow" : "red";
1578
+ const pct = Math.max(0, Math.min(1, stamina / 100));
1579
+ const multiplier = band === "red" ? cfg.tired_multiplier + (1 - cfg.tired_multiplier) * (stamina / floor) : null;
1580
+ return {
1581
+ label: "Stamina:",
1582
+ value: /* @__PURE__ */ jsxs5(Text7, { color, children: [
1583
+ `${Math.round(stamina)}% ${bar(pct, 20)}`,
1584
+ multiplier !== null ? ` \xD7${multiplier.toFixed(2)}` : ""
1585
+ ] })
1586
+ };
1587
+ }
1529
1588
  function statusColor(status) {
1530
1589
  if (status === "live") return "green";
1531
1590
  if (status === "pending") return "yellow";