@mauricode/token-derby 2.9.0 → 2.10.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 +21 -1
- package/dist/bin.js +545 -135
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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.
|
|
728
|
-
return "2.
|
|
743
|
+
if ("2.10.0".length > 0) {
|
|
744
|
+
return "2.10.0";
|
|
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,
|
|
817
|
-
const url =
|
|
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
|
|
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
|
|
1364
|
-
import { Box as
|
|
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
|
|
1368
|
-
import { jsx as
|
|
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, primaryModel, perSource, primaryCapped } = props;
|
|
1371
1434
|
if (!race) {
|
|
1372
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
1380
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1445
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, children: race.name }),
|
|
1383
1446
|
" \u2500\u2500\u2500 status: ",
|
|
1384
|
-
/* @__PURE__ */
|
|
1447
|
+
/* @__PURE__ */ jsx7(Text7, { color: statusColor(race.status), children: race.status })
|
|
1385
1448
|
] }),
|
|
1386
|
-
/* @__PURE__ */
|
|
1387
|
-
/* @__PURE__ */
|
|
1388
|
-
/* @__PURE__ */
|
|
1389
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1456
|
+
/* @__PURE__ */ jsxs5(Text7, { color: "cyan", children: [
|
|
1394
1457
|
"[Lvl. ",
|
|
1395
1458
|
lvl.level,
|
|
1396
1459
|
"]"
|
|
1397
1460
|
] })
|
|
1398
1461
|
] }),
|
|
1399
|
-
/* @__PURE__ */
|
|
1462
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1400
1463
|
" ",
|
|
1401
|
-
/* @__PURE__ */
|
|
1464
|
+
/* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
|
|
1402
1465
|
"(",
|
|
1403
1466
|
ownUserName,
|
|
1404
1467
|
")"
|
|
@@ -1406,44 +1469,53 @@ function StatusScreen(props) {
|
|
|
1406
1469
|
] })
|
|
1407
1470
|
] })
|
|
1408
1471
|
] }),
|
|
1409
|
-
/* @__PURE__ */
|
|
1410
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
1493
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1431
1494
|
"Time left: ",
|
|
1432
1495
|
timeLeft
|
|
1433
1496
|
] }),
|
|
1434
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1501
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1439
1502
|
"Last heartbeat: ",
|
|
1440
1503
|
lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
|
|
1441
1504
|
" ",
|
|
1442
|
-
/* @__PURE__ */
|
|
1505
|
+
/* @__PURE__ */ jsx7(Text7, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
|
|
1443
1506
|
] }),
|
|
1444
|
-
stalled && /* @__PURE__ */
|
|
1507
|
+
stalled && /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u26A0 Can't read token usage \u2014 try restarting this terminal. Your race continues." })
|
|
1445
1508
|
] }),
|
|
1446
|
-
|
|
1509
|
+
primaryModel && perSource && /* @__PURE__ */ jsx7(
|
|
1510
|
+
TokenBreakdown,
|
|
1511
|
+
{
|
|
1512
|
+
primaryModel,
|
|
1513
|
+
perSource,
|
|
1514
|
+
raceScore: own?.current_tokens ?? weightedTotal(primaryModel, perSource),
|
|
1515
|
+
primaryCapped
|
|
1516
|
+
}
|
|
1517
|
+
),
|
|
1518
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
|
|
1447
1519
|
] });
|
|
1448
1520
|
}
|
|
1449
1521
|
function elapsed(race) {
|
|
@@ -1515,30 +1587,45 @@ function runHeartbeatLoop(opts) {
|
|
|
1515
1587
|
// src/tokens/transcripts.ts
|
|
1516
1588
|
import * as fs3 from "fs/promises";
|
|
1517
1589
|
import * as path4 from "path";
|
|
1518
|
-
|
|
1590
|
+
var MAX_PROJECT_DEPTH = 8;
|
|
1591
|
+
function conversationId(file, root) {
|
|
1592
|
+
const rel = path4.relative(root, file);
|
|
1593
|
+
const [project, session] = rel.split(path4.sep);
|
|
1594
|
+
if (project === void 0 || session === void 0) return rel.replace(/\.jsonl$/, "");
|
|
1595
|
+
return `${project}/${session.replace(/\.jsonl$/, "")}`;
|
|
1596
|
+
}
|
|
1597
|
+
async function sumTokensByConversation() {
|
|
1519
1598
|
const root = claudeProjectsDir();
|
|
1520
1599
|
const files = await listJsonlFiles(root);
|
|
1521
|
-
|
|
1522
|
-
let output = 0;
|
|
1600
|
+
const byConv = /* @__PURE__ */ new Map();
|
|
1523
1601
|
for (const file of files) {
|
|
1524
1602
|
const t = await sumFile(file);
|
|
1603
|
+
const id = conversationId(file, root);
|
|
1604
|
+
const acc = byConv.get(id) ?? { input: 0, output: 0 };
|
|
1605
|
+
acc.input += t.input;
|
|
1606
|
+
acc.output += t.output;
|
|
1607
|
+
byConv.set(id, acc);
|
|
1608
|
+
}
|
|
1609
|
+
return byConv;
|
|
1610
|
+
}
|
|
1611
|
+
async function sumTokens() {
|
|
1612
|
+
const byConv = await sumTokensByConversation();
|
|
1613
|
+
let input = 0;
|
|
1614
|
+
let output = 0;
|
|
1615
|
+
for (const t of byConv.values()) {
|
|
1525
1616
|
input += t.input;
|
|
1526
1617
|
output += t.output;
|
|
1527
1618
|
}
|
|
1528
1619
|
return { input, output };
|
|
1529
1620
|
}
|
|
1530
|
-
async function sumTokensForRace(race) {
|
|
1531
|
-
const { input, output } = await sumTokens();
|
|
1532
|
-
return race.counts_input ? input + output : output;
|
|
1533
|
-
}
|
|
1534
1621
|
async function listJsonlFiles(root) {
|
|
1535
1622
|
const projects = await fs3.readdir(root);
|
|
1536
1623
|
const out = [];
|
|
1537
1624
|
for (const project of projects) {
|
|
1538
1625
|
const projectDir = path4.join(root, project);
|
|
1539
|
-
const
|
|
1540
|
-
if (!
|
|
1541
|
-
await collectJsonl(projectDir,
|
|
1626
|
+
const stat3 = await fs3.stat(projectDir);
|
|
1627
|
+
if (!stat3.isDirectory()) continue;
|
|
1628
|
+
await collectJsonl(projectDir, MAX_PROJECT_DEPTH, out);
|
|
1542
1629
|
}
|
|
1543
1630
|
return out;
|
|
1544
1631
|
}
|
|
@@ -1578,23 +1665,224 @@ async function sumFile(file) {
|
|
|
1578
1665
|
return { input, output };
|
|
1579
1666
|
}
|
|
1580
1667
|
|
|
1668
|
+
// src/tokens/codex.ts
|
|
1669
|
+
import * as fs4 from "fs/promises";
|
|
1670
|
+
import * as path5 from "path";
|
|
1671
|
+
function num(v) {
|
|
1672
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1673
|
+
}
|
|
1674
|
+
async function sumCodexByConversation() {
|
|
1675
|
+
const root = codexSessionsDir();
|
|
1676
|
+
await fs4.stat(root);
|
|
1677
|
+
const files = [
|
|
1678
|
+
...await collectRollouts(path5.join(root, "sessions")),
|
|
1679
|
+
...await collectRollouts(path5.join(root, "archived_sessions"))
|
|
1680
|
+
];
|
|
1681
|
+
const byConv = /* @__PURE__ */ new Map();
|
|
1682
|
+
for (const file of files) {
|
|
1683
|
+
byConv.set(file, await lastTokenCount(file));
|
|
1684
|
+
}
|
|
1685
|
+
return byConv;
|
|
1686
|
+
}
|
|
1687
|
+
async function sumCodexTokens() {
|
|
1688
|
+
const byConv = await sumCodexByConversation();
|
|
1689
|
+
let input = 0;
|
|
1690
|
+
let output = 0;
|
|
1691
|
+
for (const t of byConv.values()) {
|
|
1692
|
+
input += t.input;
|
|
1693
|
+
output += t.output;
|
|
1694
|
+
}
|
|
1695
|
+
return { input, output };
|
|
1696
|
+
}
|
|
1697
|
+
async function collectRollouts(dir) {
|
|
1698
|
+
let entries;
|
|
1699
|
+
try {
|
|
1700
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1701
|
+
} catch (e) {
|
|
1702
|
+
if (e?.code === "ENOENT") return [];
|
|
1703
|
+
throw e;
|
|
1704
|
+
}
|
|
1705
|
+
const out = [];
|
|
1706
|
+
for (const entry of entries) {
|
|
1707
|
+
const full = path5.join(dir, entry.name);
|
|
1708
|
+
if (entry.isDirectory()) out.push(...await collectRollouts(full));
|
|
1709
|
+
else if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) out.push(full);
|
|
1710
|
+
}
|
|
1711
|
+
return out;
|
|
1712
|
+
}
|
|
1713
|
+
async function lastTokenCount(file) {
|
|
1714
|
+
let raw;
|
|
1715
|
+
try {
|
|
1716
|
+
raw = await fs4.readFile(file, "utf8");
|
|
1717
|
+
} catch {
|
|
1718
|
+
return { input: 0, output: 0 };
|
|
1719
|
+
}
|
|
1720
|
+
let usage = null;
|
|
1721
|
+
for (const line of raw.split("\n")) {
|
|
1722
|
+
if (!line.trim()) continue;
|
|
1723
|
+
let parsed;
|
|
1724
|
+
try {
|
|
1725
|
+
parsed = JSON.parse(line);
|
|
1726
|
+
} catch {
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
if (parsed?.payload?.type === "token_count" && parsed.payload.info?.total_token_usage) {
|
|
1730
|
+
usage = parsed.payload.info.total_token_usage;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
if (!usage) return { input: 0, output: 0 };
|
|
1734
|
+
return {
|
|
1735
|
+
input: Math.max(0, num(usage.input_tokens) - num(usage.cached_input_tokens)),
|
|
1736
|
+
output: num(usage.output_tokens)
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/tokens/gemini.ts
|
|
1741
|
+
import * as fs5 from "fs/promises";
|
|
1742
|
+
import * as path6 from "path";
|
|
1743
|
+
function num2(v) {
|
|
1744
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1745
|
+
}
|
|
1746
|
+
async function sumGeminiByConversation() {
|
|
1747
|
+
const files = await listChatFiles(geminiTmpDir());
|
|
1748
|
+
const byConv = /* @__PURE__ */ new Map();
|
|
1749
|
+
for (const file of files) {
|
|
1750
|
+
byConv.set(file, await sumGeminiFile(file));
|
|
1751
|
+
}
|
|
1752
|
+
return byConv;
|
|
1753
|
+
}
|
|
1754
|
+
async function sumGeminiTokens() {
|
|
1755
|
+
const byConv = await sumGeminiByConversation();
|
|
1756
|
+
let input = 0;
|
|
1757
|
+
let output = 0;
|
|
1758
|
+
for (const t of byConv.values()) {
|
|
1759
|
+
input += t.input;
|
|
1760
|
+
output += t.output;
|
|
1761
|
+
}
|
|
1762
|
+
return { input, output };
|
|
1763
|
+
}
|
|
1764
|
+
async function listChatFiles(root) {
|
|
1765
|
+
const entries = await fs5.readdir(root);
|
|
1766
|
+
const out = [];
|
|
1767
|
+
for (const entry of entries) {
|
|
1768
|
+
const chatsDir = path6.join(root, entry, "chats");
|
|
1769
|
+
let files;
|
|
1770
|
+
try {
|
|
1771
|
+
files = await fs5.readdir(chatsDir);
|
|
1772
|
+
} catch {
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
for (const f of files) {
|
|
1776
|
+
if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(path6.join(chatsDir, f));
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return out;
|
|
1780
|
+
}
|
|
1781
|
+
async function sumGeminiFile(file) {
|
|
1782
|
+
let raw;
|
|
1783
|
+
try {
|
|
1784
|
+
raw = await fs5.readFile(file, "utf8");
|
|
1785
|
+
} catch {
|
|
1786
|
+
return { input: 0, output: 0 };
|
|
1787
|
+
}
|
|
1788
|
+
const messages = file.endsWith(".jsonl") ? parseJsonl(raw) : parseJson(raw);
|
|
1789
|
+
let input = 0;
|
|
1790
|
+
let output = 0;
|
|
1791
|
+
for (const m of messages) {
|
|
1792
|
+
const tk = m?.tokens;
|
|
1793
|
+
if (!tk || typeof tk !== "object") continue;
|
|
1794
|
+
input += Math.max(0, num2(tk.input) - num2(tk.cached));
|
|
1795
|
+
output += num2(tk.output);
|
|
1796
|
+
}
|
|
1797
|
+
return { input, output };
|
|
1798
|
+
}
|
|
1799
|
+
function parseJson(raw) {
|
|
1800
|
+
try {
|
|
1801
|
+
const data = JSON.parse(raw);
|
|
1802
|
+
return Array.isArray(data?.messages) ? data.messages : [];
|
|
1803
|
+
} catch {
|
|
1804
|
+
return [];
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
function parseJsonl(raw) {
|
|
1808
|
+
const out = [];
|
|
1809
|
+
for (const line of raw.split("\n")) {
|
|
1810
|
+
if (!line.trim()) continue;
|
|
1811
|
+
try {
|
|
1812
|
+
out.push(JSON.parse(line));
|
|
1813
|
+
} catch {
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return out;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// src/tokens/race-tokens.ts
|
|
1820
|
+
var SCALAR_READERS = {
|
|
1821
|
+
claude: sumTokens,
|
|
1822
|
+
codex: sumCodexTokens,
|
|
1823
|
+
gemini: sumGeminiTokens
|
|
1824
|
+
};
|
|
1825
|
+
var BY_CONVERSATION_READERS = {
|
|
1826
|
+
claude: sumTokensByConversation,
|
|
1827
|
+
codex: sumCodexByConversation,
|
|
1828
|
+
gemini: sumGeminiByConversation
|
|
1829
|
+
};
|
|
1830
|
+
function scoreFor(race, t) {
|
|
1831
|
+
return race.counts_input ? t.input + t.output : t.output;
|
|
1832
|
+
}
|
|
1833
|
+
async function readAllSources(race, primary) {
|
|
1834
|
+
try {
|
|
1835
|
+
const primaryMap = await BY_CONVERSATION_READERS[primary]();
|
|
1836
|
+
const primaryByConv = /* @__PURE__ */ new Map();
|
|
1837
|
+
for (const [id, totals] of primaryMap) primaryByConv.set(id, scoreFor(race, totals));
|
|
1838
|
+
const secondary = { claude: 0, codex: 0, gemini: 0 };
|
|
1839
|
+
await Promise.all(
|
|
1840
|
+
MODEL_KEYS.filter((k) => k !== primary).map(async (k) => {
|
|
1841
|
+
secondary[k] = await SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch(() => 0);
|
|
1842
|
+
})
|
|
1843
|
+
);
|
|
1844
|
+
return { secondary, primaryByConv };
|
|
1845
|
+
} catch {
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// src/tokens/primary-cap.ts
|
|
1851
|
+
var PRIMARY_TOP_CONVERSATIONS = 5;
|
|
1852
|
+
function primaryConversationCap(enabled) {
|
|
1853
|
+
return enabled ? PRIMARY_TOP_CONVERSATIONS : Infinity;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1581
1856
|
// src/tokens/race-score.ts
|
|
1582
1857
|
var STALL_THRESHOLD = 5;
|
|
1858
|
+
function zero() {
|
|
1859
|
+
return { claude: 0, codex: 0, gemini: 0 };
|
|
1860
|
+
}
|
|
1583
1861
|
var RaceScoreTracker = class {
|
|
1584
1862
|
acked;
|
|
1585
1863
|
lastGood;
|
|
1864
|
+
primaryConvAcked;
|
|
1865
|
+
primaryConvLast;
|
|
1866
|
+
counted;
|
|
1586
1867
|
seq;
|
|
1587
1868
|
stalls = 0;
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1869
|
+
primary;
|
|
1870
|
+
primaryTop5;
|
|
1871
|
+
constructor(init, primary, primaryTop5) {
|
|
1872
|
+
this.acked = { ...init.acked };
|
|
1873
|
+
this.lastGood = { ...init.lastGood };
|
|
1874
|
+
this.primaryConvAcked = { ...init.primaryConvAcked };
|
|
1875
|
+
this.primaryConvLast = { ...init.primaryConvAcked };
|
|
1876
|
+
this.counted = init.primaryCounted;
|
|
1591
1877
|
this.seq = init.seq;
|
|
1878
|
+
this.primary = primary;
|
|
1879
|
+
this.primaryTop5 = primaryTop5;
|
|
1592
1880
|
}
|
|
1593
1881
|
/**
|
|
1594
1882
|
* Record a scan result.
|
|
1595
|
-
* - `null` →
|
|
1596
|
-
* -
|
|
1597
|
-
* -
|
|
1883
|
+
* - `null` → stall (warning), anchors untouched.
|
|
1884
|
+
* - otherwise → secondaries advance scalar lastGood (never down to 0); the
|
|
1885
|
+
* primary's per-conversation latest readings are updated (monotonic).
|
|
1598
1886
|
*/
|
|
1599
1887
|
recordReading(reading) {
|
|
1600
1888
|
if (reading === null) {
|
|
@@ -1602,44 +1890,103 @@ var RaceScoreTracker = class {
|
|
|
1602
1890
|
return;
|
|
1603
1891
|
}
|
|
1604
1892
|
this.stalls = 0;
|
|
1605
|
-
|
|
1893
|
+
for (const key of MODEL_KEYS) {
|
|
1894
|
+
if (key === this.primary) continue;
|
|
1895
|
+
const v = reading.secondary[key];
|
|
1896
|
+
if (v > 0) this.lastGood[key] = v;
|
|
1897
|
+
}
|
|
1898
|
+
for (const [id, v] of reading.primaryByConv) {
|
|
1899
|
+
const prev = this.primaryConvLast[id] ?? 0;
|
|
1900
|
+
if (v > prev) this.primaryConvLast[id] = v;
|
|
1901
|
+
}
|
|
1606
1902
|
}
|
|
1607
1903
|
/** Frozen payload for the next heartbeat. Pure — call repeatedly for retries. */
|
|
1608
1904
|
nextBeat() {
|
|
1609
|
-
|
|
1905
|
+
const components = zero();
|
|
1906
|
+
for (const key of MODEL_KEYS) {
|
|
1907
|
+
if (key === this.primary) continue;
|
|
1908
|
+
components[key] = Math.max(0, this.lastGood[key] - this.acked[key]);
|
|
1909
|
+
}
|
|
1910
|
+
const pending = [];
|
|
1911
|
+
for (const [id, last] of Object.entries(this.primaryConvLast)) {
|
|
1912
|
+
const d = Math.max(0, last - (this.primaryConvAcked[id] ?? 0));
|
|
1913
|
+
if (d > 0) pending.push(d);
|
|
1914
|
+
}
|
|
1915
|
+
pending.sort((a, b) => b - a);
|
|
1916
|
+
const cap = primaryConversationCap(this.primaryTop5);
|
|
1917
|
+
const take = cap === Infinity ? pending.length : Math.min(cap, pending.length);
|
|
1918
|
+
let primarySum = 0;
|
|
1919
|
+
for (const d of pending.slice(0, take)) primarySum += d;
|
|
1920
|
+
components[this.primary] = primarySum;
|
|
1921
|
+
return {
|
|
1922
|
+
seq: this.seq + 1,
|
|
1923
|
+
components,
|
|
1924
|
+
readings: { ...this.lastGood },
|
|
1925
|
+
primaryConvReadings: { ...this.primaryConvLast }
|
|
1926
|
+
};
|
|
1610
1927
|
}
|
|
1611
1928
|
/** Commit a heartbeat the server accepted. `serverLastSeq` self-heals drift. */
|
|
1612
1929
|
ack(snapshot, serverLastSeq) {
|
|
1613
|
-
|
|
1930
|
+
for (const key of MODEL_KEYS) {
|
|
1931
|
+
if (key === this.primary) continue;
|
|
1932
|
+
this.acked[key] = snapshot.readings[key];
|
|
1933
|
+
}
|
|
1934
|
+
this.primaryConvAcked = { ...snapshot.primaryConvReadings };
|
|
1935
|
+
this.counted += snapshot.components[this.primary];
|
|
1614
1936
|
this.seq = Math.max(snapshot.seq, serverLastSeq);
|
|
1615
1937
|
}
|
|
1616
|
-
/** Pin
|
|
1938
|
+
/** Pin anchors to the latest readings so the next deltas are 0 (pending race). */
|
|
1617
1939
|
reprime() {
|
|
1618
|
-
|
|
1940
|
+
for (const key of MODEL_KEYS) {
|
|
1941
|
+
if (key === this.primary) continue;
|
|
1942
|
+
this.acked[key] = this.lastGood[key];
|
|
1943
|
+
}
|
|
1944
|
+
this.primaryConvAcked = { ...this.primaryConvLast };
|
|
1619
1945
|
}
|
|
1620
1946
|
get stalled() {
|
|
1621
1947
|
return this.stalls >= STALL_THRESHOLD;
|
|
1622
1948
|
}
|
|
1949
|
+
/** Cumulative primary tokens credited so far (for the UI's primary "since join" row). */
|
|
1950
|
+
primaryCounted() {
|
|
1951
|
+
return this.counted;
|
|
1952
|
+
}
|
|
1953
|
+
/** Secondary "since join" totals = lastGood − baseline (for the UI). Primary key is 0 here. */
|
|
1954
|
+
secondarySinceJoin(baseline) {
|
|
1955
|
+
const out = zero();
|
|
1956
|
+
for (const key of MODEL_KEYS) {
|
|
1957
|
+
if (key === this.primary) continue;
|
|
1958
|
+
out[key] = Math.max(0, this.lastGood[key] - baseline[key]);
|
|
1959
|
+
}
|
|
1960
|
+
return out;
|
|
1961
|
+
}
|
|
1623
1962
|
toState() {
|
|
1624
|
-
return {
|
|
1963
|
+
return {
|
|
1964
|
+
acked: { ...this.acked },
|
|
1965
|
+
lastGood: { ...this.lastGood },
|
|
1966
|
+
primaryConvAcked: { ...this.primaryConvAcked },
|
|
1967
|
+
primaryCounted: this.counted,
|
|
1968
|
+
seq: this.seq
|
|
1969
|
+
};
|
|
1625
1970
|
}
|
|
1626
1971
|
};
|
|
1627
1972
|
|
|
1628
1973
|
// src/runtime/run-race.tsx
|
|
1629
|
-
import { jsx as
|
|
1974
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1630
1975
|
function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
1631
1976
|
const { exit } = useApp();
|
|
1632
|
-
const [race, setRace] =
|
|
1633
|
-
const [lastHbAt, setLastHbAt] =
|
|
1634
|
-
const [lastHbOk, setLastHbOk] =
|
|
1635
|
-
const [tickNow, setTickNow] =
|
|
1636
|
-
const [fatalError, setFatalError] =
|
|
1637
|
-
const [achievements, setAchievements] =
|
|
1977
|
+
const [race, setRace] = useState5(null);
|
|
1978
|
+
const [lastHbAt, setLastHbAt] = useState5(null);
|
|
1979
|
+
const [lastHbOk, setLastHbOk] = useState5(true);
|
|
1980
|
+
const [tickNow, setTickNow] = useState5(/* @__PURE__ */ new Date());
|
|
1981
|
+
const [fatalError, setFatalError] = useState5(null);
|
|
1982
|
+
const [achievements, setAchievements] = useState5([]);
|
|
1638
1983
|
const shownAchievementAtRef = useRef(0);
|
|
1639
|
-
const trackerRef = useRef(new RaceScoreTracker(initialState));
|
|
1984
|
+
const trackerRef = useRef(new RaceScoreTracker(initialState, active.primary_model, active.primary_top5 ?? false));
|
|
1640
1985
|
const pendingRef = useRef(pendingMode);
|
|
1641
1986
|
const ctrl = useRef(new AbortController());
|
|
1642
|
-
const [stalled, setStalled] =
|
|
1987
|
+
const [stalled, setStalled] = useState5(false);
|
|
1988
|
+
const baselineRef = useRef(initialState.acked);
|
|
1989
|
+
const [perSource, setPerSource] = useState5({ claude: 0, codex: 0, gemini: 0 });
|
|
1643
1990
|
useEffect2(() => {
|
|
1644
1991
|
const t = setInterval(() => setTickNow(/* @__PURE__ */ new Date()), 1e3);
|
|
1645
1992
|
return () => clearInterval(t);
|
|
@@ -1655,7 +2002,7 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
1655
2002
|
const scanWithTimeout = async () => {
|
|
1656
2003
|
try {
|
|
1657
2004
|
return await Promise.race([
|
|
1658
|
-
|
|
2005
|
+
readAllSources(active, active.primary_model),
|
|
1659
2006
|
new Promise((_, reject) => setTimeout(() => reject(new Error("scan timeout")), 1e4))
|
|
1660
2007
|
]);
|
|
1661
2008
|
} catch {
|
|
@@ -1668,19 +2015,25 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
1668
2015
|
tracker.recordReading(reading);
|
|
1669
2016
|
if (pendingRef.current) tracker.reprime();
|
|
1670
2017
|
setStalled(tracker.stalled);
|
|
2018
|
+
const since = tracker.secondarySinceJoin(baselineRef.current);
|
|
2019
|
+
const ps = { claude: 0, codex: 0, gemini: 0 };
|
|
2020
|
+
for (const k of MODEL_KEYS) {
|
|
2021
|
+
ps[k] = k === active.primary_model ? tracker.primaryCounted() : since[k];
|
|
2022
|
+
}
|
|
2023
|
+
setPerSource(ps);
|
|
1671
2024
|
return tracker.nextBeat();
|
|
1672
2025
|
},
|
|
1673
2026
|
sendBeat: async (snapshot) => {
|
|
1674
2027
|
return heartbeat(active.join_code, active.horse_id, active.heartbeat_token, {
|
|
1675
2028
|
seq: snapshot.seq,
|
|
1676
|
-
|
|
2029
|
+
components: snapshot.components
|
|
1677
2030
|
});
|
|
1678
2031
|
},
|
|
1679
2032
|
onSuccess: (resp, snapshot) => {
|
|
1680
2033
|
tracker.ack(snapshot, resp.last_seq);
|
|
1681
2034
|
const updated = {
|
|
1682
2035
|
...active,
|
|
1683
|
-
|
|
2036
|
+
score: tracker.toState(),
|
|
1684
2037
|
last_heartbeat_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1685
2038
|
};
|
|
1686
2039
|
void saveActiveRace(updated);
|
|
@@ -1717,13 +2070,13 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
1717
2070
|
}, []);
|
|
1718
2071
|
const lastHeartbeatAgoSec = lastHbAt ? Math.max(0, Math.floor((tickNow.getTime() - lastHbAt.getTime()) / 1e3)) : null;
|
|
1719
2072
|
if (fatalError) {
|
|
1720
|
-
return /* @__PURE__ */
|
|
1721
|
-
/* @__PURE__ */
|
|
1722
|
-
/* @__PURE__ */
|
|
2073
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", padding: 1, children: [
|
|
2074
|
+
/* @__PURE__ */ jsx8(Text8, { color: "red", bold: true, children: "CLI version mismatch \u2014 disconnected" }),
|
|
2075
|
+
/* @__PURE__ */ jsx8(Text8, { children: fatalError })
|
|
1723
2076
|
] });
|
|
1724
2077
|
}
|
|
1725
|
-
return /* @__PURE__ */
|
|
1726
|
-
/* @__PURE__ */
|
|
2078
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
|
|
2079
|
+
/* @__PURE__ */ jsx8(
|
|
1727
2080
|
StatusScreen,
|
|
1728
2081
|
{
|
|
1729
2082
|
race,
|
|
@@ -1733,26 +2086,29 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
1733
2086
|
ownUserName,
|
|
1734
2087
|
lastHeartbeatAgoSec,
|
|
1735
2088
|
lastHeartbeatOk: lastHbOk,
|
|
1736
|
-
stalled
|
|
2089
|
+
stalled,
|
|
2090
|
+
primaryModel: active.primary_model,
|
|
2091
|
+
perSource,
|
|
2092
|
+
primaryCapped: primaryConversationCap(active.primary_top5 ?? false) !== Infinity
|
|
1737
2093
|
}
|
|
1738
2094
|
),
|
|
1739
|
-
achievements.length > 0 && /* @__PURE__ */
|
|
1740
|
-
/* @__PURE__ */
|
|
2095
|
+
achievements.length > 0 && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", marginTop: 1, children: [
|
|
2096
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: "Achievements" }),
|
|
1741
2097
|
achievements.map(({ key, event }) => {
|
|
1742
2098
|
const description = describeAchievement(event, active);
|
|
1743
|
-
return /* @__PURE__ */
|
|
1744
|
-
/* @__PURE__ */
|
|
2099
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "row", children: [
|
|
2100
|
+
/* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
|
|
1745
2101
|
" ",
|
|
1746
2102
|
formatClockTime(event.at),
|
|
1747
2103
|
" "
|
|
1748
2104
|
] }),
|
|
1749
|
-
/* @__PURE__ */
|
|
2105
|
+
/* @__PURE__ */ jsxs6(Text8, { color: "yellow", bold: true, children: [
|
|
1750
2106
|
"+",
|
|
1751
2107
|
event.xp,
|
|
1752
2108
|
" XP "
|
|
1753
2109
|
] }),
|
|
1754
|
-
/* @__PURE__ */
|
|
1755
|
-
/* @__PURE__ */
|
|
2110
|
+
/* @__PURE__ */ jsx8(Text8, { children: event.name }),
|
|
2111
|
+
/* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
|
|
1756
2112
|
" \u2014 ",
|
|
1757
2113
|
description
|
|
1758
2114
|
] })
|
|
@@ -1778,25 +2134,52 @@ function raceViewFrom(resp) {
|
|
|
1778
2134
|
};
|
|
1779
2135
|
}
|
|
1780
2136
|
async function buildInitialState(args) {
|
|
1781
|
-
let
|
|
2137
|
+
let now = null;
|
|
1782
2138
|
try {
|
|
1783
|
-
|
|
2139
|
+
now = await readAllSources(args.active, args.active.primary_model);
|
|
1784
2140
|
} catch {
|
|
1785
|
-
diskNow = 0;
|
|
1786
2141
|
}
|
|
2142
|
+
const secondary = now?.secondary ?? { claude: 0, codex: 0, gemini: 0 };
|
|
2143
|
+
const primaryConvAcked = {};
|
|
2144
|
+
if (now) for (const [id, v] of now.primaryByConv) primaryConvAcked[id] = v;
|
|
1787
2145
|
return {
|
|
1788
|
-
initialState: {
|
|
2146
|
+
initialState: {
|
|
2147
|
+
acked: { ...secondary },
|
|
2148
|
+
lastGood: { ...secondary },
|
|
2149
|
+
primaryConvAcked,
|
|
2150
|
+
primaryCounted: 0,
|
|
2151
|
+
seq: args.serverLastSeq
|
|
2152
|
+
},
|
|
1789
2153
|
pendingMode: args.raceStatus === "pending"
|
|
1790
2154
|
};
|
|
1791
2155
|
}
|
|
1792
2156
|
|
|
1793
2157
|
// src/commands/join.ts
|
|
1794
|
-
|
|
2158
|
+
function parsePrimaryFlag(argv) {
|
|
2159
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2160
|
+
const a = argv[i];
|
|
2161
|
+
let value;
|
|
2162
|
+
if (a === "--primary") value = argv[i + 1];
|
|
2163
|
+
else if (a.startsWith("--primary=")) value = a.slice("--primary=".length);
|
|
2164
|
+
else continue;
|
|
2165
|
+
if (!isModelKey(value)) throw new Error(`--primary must be one of claude, codex, gemini (got ${value ?? ""})`);
|
|
2166
|
+
return value;
|
|
2167
|
+
}
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
async function joinCommand(joinCode, argv = []) {
|
|
1795
2171
|
if (!joinCode) {
|
|
1796
2172
|
console.error("Usage: token-derby join <join-code>");
|
|
1797
2173
|
return 2;
|
|
1798
2174
|
}
|
|
1799
2175
|
const code = joinCode.toUpperCase();
|
|
2176
|
+
let primaryFlag;
|
|
2177
|
+
try {
|
|
2178
|
+
primaryFlag = parsePrimaryFlag(argv);
|
|
2179
|
+
} catch (e) {
|
|
2180
|
+
console.error(e.message);
|
|
2181
|
+
return 2;
|
|
2182
|
+
}
|
|
1800
2183
|
const identity = await loadIdentity();
|
|
1801
2184
|
if (!identity) {
|
|
1802
2185
|
console.error("Run `token-derby init` to set up your identity.");
|
|
@@ -1860,9 +2243,14 @@ async function joinCommand(joinCode) {
|
|
|
1860
2243
|
chosenName = picked.name;
|
|
1861
2244
|
chosenColors = picked.colors;
|
|
1862
2245
|
}
|
|
2246
|
+
let chosenPrimary = "claude";
|
|
2247
|
+
if (!ownHorse) {
|
|
2248
|
+
if (primaryFlag) chosenPrimary = primaryFlag;
|
|
2249
|
+
else if (process.stdout.isTTY) chosenPrimary = await pickPrimary();
|
|
2250
|
+
}
|
|
1863
2251
|
let joinResp;
|
|
1864
2252
|
try {
|
|
1865
|
-
joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId });
|
|
2253
|
+
joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId, primary_model: chosenPrimary });
|
|
1866
2254
|
} catch (e) {
|
|
1867
2255
|
if (e instanceof ApiError) {
|
|
1868
2256
|
if (e.code === "RACE_FULL") console.error("This race is full.");
|
|
@@ -1887,22 +2275,28 @@ async function joinCommand(joinCode) {
|
|
|
1887
2275
|
horse_name: chosenName,
|
|
1888
2276
|
horse_colors: chosenColors,
|
|
1889
2277
|
joined_at: ownHorse?.joined_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1890
|
-
ackedReading: 0,
|
|
1891
|
-
lastGoodReading: 0,
|
|
1892
|
-
seq: ownHorse?.last_seq ?? 0,
|
|
1893
2278
|
last_heartbeat_at: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1894
|
-
|
|
2279
|
+
primary_model: joinResp.primary_model,
|
|
2280
|
+
score: {
|
|
2281
|
+
acked: { claude: 0, codex: 0, gemini: 0 },
|
|
2282
|
+
lastGood: { claude: 0, codex: 0, gemini: 0 },
|
|
2283
|
+
primaryConvAcked: {},
|
|
2284
|
+
primaryCounted: 0,
|
|
2285
|
+
seq: ownHorse?.last_seq ?? 0
|
|
2286
|
+
},
|
|
2287
|
+
...race.counts_input ? { counts_input: true } : {},
|
|
2288
|
+
...race.primary_top5 ? { primary_top5: true } : {}
|
|
1895
2289
|
};
|
|
1896
2290
|
await saveActiveRace(active);
|
|
1897
2291
|
const initial = await buildInitialState({ active, raceStatus: status, serverLastSeq: ownHorse?.last_seq ?? 0 });
|
|
1898
|
-
const app = render4(
|
|
2292
|
+
const app = render4(React9.createElement(RunRace, { active, initialState: initial.initialState, pendingMode: initial.pendingMode, ownUserName: identity.display_name }));
|
|
1899
2293
|
await app.waitUntilExit();
|
|
1900
2294
|
return 0;
|
|
1901
2295
|
}
|
|
1902
2296
|
async function pickHorse(horses) {
|
|
1903
2297
|
return new Promise((resolve) => {
|
|
1904
2298
|
const app = render4(
|
|
1905
|
-
|
|
2299
|
+
React9.createElement(HorsePicker, {
|
|
1906
2300
|
horses,
|
|
1907
2301
|
onPick: (h) => {
|
|
1908
2302
|
app.unmount();
|
|
@@ -1916,6 +2310,18 @@ async function pickHorse(horses) {
|
|
|
1916
2310
|
);
|
|
1917
2311
|
});
|
|
1918
2312
|
}
|
|
2313
|
+
async function pickPrimary() {
|
|
2314
|
+
return new Promise((resolve) => {
|
|
2315
|
+
const app = render4(
|
|
2316
|
+
React9.createElement(PrimaryPicker, {
|
|
2317
|
+
onPick: (m) => {
|
|
2318
|
+
app.unmount();
|
|
2319
|
+
resolve(m);
|
|
2320
|
+
}
|
|
2321
|
+
})
|
|
2322
|
+
);
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
1919
2325
|
|
|
1920
2326
|
// src/commands/end.ts
|
|
1921
2327
|
import * as readline3 from "readline/promises";
|
|
@@ -2114,30 +2520,30 @@ function runNpmUpgrade(spawnImpl) {
|
|
|
2114
2520
|
}
|
|
2115
2521
|
|
|
2116
2522
|
// src/commands/roll.ts
|
|
2117
|
-
import
|
|
2523
|
+
import React13 from "react";
|
|
2118
2524
|
import { render as render5 } from "ink";
|
|
2119
2525
|
|
|
2120
2526
|
// src/ui/RollReveal.tsx
|
|
2121
|
-
import { useState as
|
|
2122
|
-
import { Box as
|
|
2527
|
+
import { useState as useState7, useEffect as useEffect4, useMemo } from "react";
|
|
2528
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
2123
2529
|
|
|
2124
2530
|
// src/ui/HatSprite.tsx
|
|
2125
|
-
import { useEffect as useEffect3, useState as
|
|
2126
|
-
import { Box as
|
|
2127
|
-
import { jsx as
|
|
2531
|
+
import { useEffect as useEffect3, useState as useState6 } from "react";
|
|
2532
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
2533
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
2128
2534
|
function HatSprite({ hat, variant, centerIn }) {
|
|
2129
2535
|
const colors = hatColorsFor2(hat, variant ?? 0);
|
|
2130
2536
|
const grid = makeHatGrid(hat, colors, centerIn);
|
|
2131
2537
|
const lines = hexGridToHalfBlocks(grid);
|
|
2132
|
-
return /* @__PURE__ */
|
|
2538
|
+
return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx9(Text9, { children: line }, i)) });
|
|
2133
2539
|
}
|
|
2134
2540
|
function AnimatedHatSprite({ hat, variant, centerIn }) {
|
|
2135
2541
|
if (hat.rarity !== "legendary") {
|
|
2136
|
-
return /* @__PURE__ */
|
|
2542
|
+
return /* @__PURE__ */ jsx9(HatSprite, { hat, variant, centerIn });
|
|
2137
2543
|
}
|
|
2138
2544
|
const frames = hat.animation.frames;
|
|
2139
2545
|
const fps = hat.animation.fps;
|
|
2140
|
-
const [idx, setIdx] =
|
|
2546
|
+
const [idx, setIdx] = useState6(0);
|
|
2141
2547
|
useEffect3(() => {
|
|
2142
2548
|
if (frames.length <= 1) return;
|
|
2143
2549
|
const interval = setInterval(
|
|
@@ -2147,7 +2553,7 @@ function AnimatedHatSprite({ hat, variant, centerIn }) {
|
|
|
2147
2553
|
return () => clearInterval(interval);
|
|
2148
2554
|
}, [frames.length, fps]);
|
|
2149
2555
|
const framed = { ...hat, colors: { ...hat.colors, A: frames[idx] } };
|
|
2150
|
-
return /* @__PURE__ */
|
|
2556
|
+
return /* @__PURE__ */ jsx9(HatSprite, { hat: framed, variant, centerIn });
|
|
2151
2557
|
}
|
|
2152
2558
|
function hatColorsFor2(hat, variantIdx) {
|
|
2153
2559
|
if (hat.rarity === "legendary") return hat.colors;
|
|
@@ -2174,7 +2580,7 @@ function makeHatGrid(hat, colors, centerIn) {
|
|
|
2174
2580
|
}
|
|
2175
2581
|
|
|
2176
2582
|
// src/ui/RollReveal.tsx
|
|
2177
|
-
import { jsx as
|
|
2583
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
2178
2584
|
var RESET3 = "\x1B[0m";
|
|
2179
2585
|
var BOX_COLOR = "#E5C76B";
|
|
2180
2586
|
var TIER_PALETTE = {
|
|
@@ -2243,7 +2649,7 @@ var BOX_EMPTY = [
|
|
|
2243
2649
|
""
|
|
2244
2650
|
].map(pad);
|
|
2245
2651
|
function GiftBox({ frame, color }) {
|
|
2246
|
-
return /* @__PURE__ */
|
|
2652
|
+
return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx10(Text10, { children: line ? ansiFg(color) + line + RESET3 : line }, i)) });
|
|
2247
2653
|
}
|
|
2248
2654
|
function spawnParticles(tier, count, cx, cy) {
|
|
2249
2655
|
const palette = TIER_PALETTE[tier];
|
|
@@ -2266,7 +2672,7 @@ function ConfettiBurst({ tier }) {
|
|
|
2266
2672
|
const cx = Math.floor(SCENE_W / 2);
|
|
2267
2673
|
const cy = Math.floor(SCENE_H / 2);
|
|
2268
2674
|
const particles = useMemo(() => spawnParticles(tier, 36, cx, cy), [tier, cx, cy]);
|
|
2269
|
-
const [tick, setTick] =
|
|
2675
|
+
const [tick, setTick] = useState7(0);
|
|
2270
2676
|
useEffect4(() => {
|
|
2271
2677
|
const i = setInterval(() => setTick((t) => t + 1), 70);
|
|
2272
2678
|
return () => clearInterval(i);
|
|
@@ -2279,13 +2685,13 @@ function ConfettiBurst({ tier }) {
|
|
|
2279
2685
|
grid[y][x] = ansiFg(p.color) + p.char + RESET3;
|
|
2280
2686
|
}
|
|
2281
2687
|
}
|
|
2282
|
-
return /* @__PURE__ */
|
|
2688
|
+
return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: grid.map((row, y) => /* @__PURE__ */ jsx10(Text10, { children: row.join("") }, y)) });
|
|
2283
2689
|
}
|
|
2284
2690
|
var CLOSED_HOLD_MS = 3e3;
|
|
2285
2691
|
function RollReveal({ outcome, onDone }) {
|
|
2286
2692
|
const isNoHat = outcome.kind === "no_hat";
|
|
2287
2693
|
const isLegendary = outcome.kind !== "no_hat" && outcome.hat.rarity === "legendary";
|
|
2288
|
-
const [phase, setPhase] =
|
|
2694
|
+
const [phase, setPhase] = useState7("closed");
|
|
2289
2695
|
useEffect4(() => {
|
|
2290
2696
|
const timers = [];
|
|
2291
2697
|
timers.push(setTimeout(() => setPhase("open1"), CLOSED_HOLD_MS));
|
|
@@ -2300,24 +2706,24 @@ function RollReveal({ outcome, onDone }) {
|
|
|
2300
2706
|
}
|
|
2301
2707
|
return () => timers.forEach(clearTimeout);
|
|
2302
2708
|
}, [isNoHat, isLegendary, onDone]);
|
|
2303
|
-
if (phase === "closed") return /* @__PURE__ */
|
|
2304
|
-
if (phase === "open1") return /* @__PURE__ */
|
|
2305
|
-
if (phase === "open2") return /* @__PURE__ */
|
|
2306
|
-
if (phase === "empty") return /* @__PURE__ */
|
|
2709
|
+
if (phase === "closed") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_CLOSED, color: BOX_COLOR });
|
|
2710
|
+
if (phase === "open1") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_1, color: BOX_COLOR });
|
|
2711
|
+
if (phase === "open2") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_2, color: BOX_COLOR });
|
|
2712
|
+
if (phase === "empty") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
|
|
2307
2713
|
if (phase === "burst" && outcome.kind !== "no_hat") {
|
|
2308
|
-
return /* @__PURE__ */
|
|
2714
|
+
return /* @__PURE__ */ jsx10(ConfettiBurst, { tier: outcome.hat.rarity });
|
|
2309
2715
|
}
|
|
2310
|
-
if (outcome.kind === "no_hat") return /* @__PURE__ */
|
|
2311
|
-
return outcome.hat.rarity === "legendary" ? /* @__PURE__ */
|
|
2716
|
+
if (outcome.kind === "no_hat") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
|
|
2717
|
+
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
2718
|
}
|
|
2313
2719
|
|
|
2314
2720
|
// src/ui/RollHorsePicker.tsx
|
|
2315
|
-
import { useState as
|
|
2316
|
-
import { Box as
|
|
2317
|
-
import { jsx as
|
|
2721
|
+
import { useState as useState8 } from "react";
|
|
2722
|
+
import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
|
|
2723
|
+
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2318
2724
|
function RollHorsePicker({ horses, onPick, onCancel }) {
|
|
2319
|
-
const [idx, setIdx] =
|
|
2320
|
-
|
|
2725
|
+
const [idx, setIdx] = useState8(0);
|
|
2726
|
+
useInput4((input, key) => {
|
|
2321
2727
|
if (key.escape) {
|
|
2322
2728
|
onCancel();
|
|
2323
2729
|
return;
|
|
@@ -2336,33 +2742,33 @@ function RollHorsePicker({ horses, onPick, onCancel }) {
|
|
|
2336
2742
|
return;
|
|
2337
2743
|
}
|
|
2338
2744
|
});
|
|
2339
|
-
return /* @__PURE__ */
|
|
2340
|
-
/* @__PURE__ */
|
|
2341
|
-
horses.map((h, i) => /* @__PURE__ */
|
|
2342
|
-
/* @__PURE__ */
|
|
2745
|
+
return /* @__PURE__ */ jsxs7(Box11, { flexDirection: "column", children: [
|
|
2746
|
+
/* @__PURE__ */ jsx11(Text11, { children: "Pick a horse to roll for:" }),
|
|
2747
|
+
horses.map((h, i) => /* @__PURE__ */ jsxs7(Box11, { flexDirection: "column", children: [
|
|
2748
|
+
/* @__PURE__ */ jsx11(Box11, { flexDirection: "row", children: /* @__PURE__ */ jsxs7(Text11, { children: [
|
|
2343
2749
|
i === idx ? "\u25BA" : " ",
|
|
2344
2750
|
" ",
|
|
2345
2751
|
h.name,
|
|
2346
2752
|
" ",
|
|
2347
|
-
/* @__PURE__ */
|
|
2753
|
+
/* @__PURE__ */ jsxs7(Text11, { color: "cyan", children: [
|
|
2348
2754
|
"[Lvl. ",
|
|
2349
2755
|
levelFromXp(h.xp),
|
|
2350
2756
|
"]"
|
|
2351
2757
|
] }),
|
|
2352
2758
|
" ",
|
|
2353
|
-
/* @__PURE__ */
|
|
2759
|
+
/* @__PURE__ */ jsxs7(Text11, { color: "yellow", children: [
|
|
2354
2760
|
"\u2014 ",
|
|
2355
2761
|
h.pending,
|
|
2356
2762
|
" roll",
|
|
2357
2763
|
h.pending === 1 ? "" : "s"
|
|
2358
2764
|
] })
|
|
2359
2765
|
] }) }),
|
|
2360
|
-
/* @__PURE__ */
|
|
2361
|
-
/* @__PURE__ */
|
|
2362
|
-
/* @__PURE__ */
|
|
2766
|
+
/* @__PURE__ */ jsxs7(Box11, { flexDirection: "row", children: [
|
|
2767
|
+
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
2768
|
+
/* @__PURE__ */ jsx11(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
|
|
2363
2769
|
] })
|
|
2364
2770
|
] }, h.stable_horse_id)),
|
|
2365
|
-
/* @__PURE__ */
|
|
2771
|
+
/* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
|
|
2366
2772
|
] });
|
|
2367
2773
|
}
|
|
2368
2774
|
|
|
@@ -2391,7 +2797,7 @@ function resetStdinAfterInk() {
|
|
|
2391
2797
|
}
|
|
2392
2798
|
async function runReveal(outcome) {
|
|
2393
2799
|
await new Promise((resolve) => {
|
|
2394
|
-
const app = render5(
|
|
2800
|
+
const app = render5(React13.createElement(RollReveal, {
|
|
2395
2801
|
outcome,
|
|
2396
2802
|
onDone: () => {
|
|
2397
2803
|
app.unmount();
|
|
@@ -2417,7 +2823,7 @@ async function rollCommand() {
|
|
|
2417
2823
|
return 0;
|
|
2418
2824
|
}
|
|
2419
2825
|
const picked = await new Promise((resolve) => {
|
|
2420
|
-
const app = render5(
|
|
2826
|
+
const app = render5(React13.createElement(RollHorsePicker, {
|
|
2421
2827
|
horses: eligible,
|
|
2422
2828
|
onPick: (h) => {
|
|
2423
2829
|
app.unmount();
|
|
@@ -2682,7 +3088,7 @@ function flag(args, name) {
|
|
|
2682
3088
|
}
|
|
2683
3089
|
return void 0;
|
|
2684
3090
|
}
|
|
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]';
|
|
3091
|
+
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
3092
|
async function orgScheduleSetCommand(orgName, rest) {
|
|
2687
3093
|
if (!orgName) {
|
|
2688
3094
|
console.error(USAGE);
|
|
@@ -2695,6 +3101,7 @@ async function orgScheduleSetCommand(orgName, rest) {
|
|
|
2695
3101
|
const name = flag(rest, "--name");
|
|
2696
3102
|
const maxStr = flag(rest, "--max");
|
|
2697
3103
|
const counts_input = rest.includes("--counts-input");
|
|
3104
|
+
const primary_top5 = rest.includes("--primary-top5");
|
|
2698
3105
|
if (!daysSpec || !start || !end || !tz) {
|
|
2699
3106
|
console.error("Required flags: --days, --start, --end, --tz");
|
|
2700
3107
|
console.error(USAGE);
|
|
@@ -2721,10 +3128,12 @@ async function orgScheduleSetCommand(orgName, rest) {
|
|
|
2721
3128
|
tz,
|
|
2722
3129
|
...name ? { race_name: name } : {},
|
|
2723
3130
|
...max_participants !== void 0 ? { max_participants } : {},
|
|
2724
|
-
...counts_input ? { counts_input: true } : {}
|
|
3131
|
+
...counts_input ? { counts_input: true } : {},
|
|
3132
|
+
...primary_top5 ? { primary_top5: true } : {}
|
|
2725
3133
|
});
|
|
2726
3134
|
const s = resp.schedule;
|
|
2727
3135
|
console.log(`Schedule set for ${orgName}: days [${s.weekdays.join(",")}] ${s.start_local}\u2013${s.end_local} ${s.tz}`);
|
|
3136
|
+
if (s.primary_top5) console.log(" Primary score counts only each racer's top 5 conversations per beat.");
|
|
2728
3137
|
return 0;
|
|
2729
3138
|
} catch (e) {
|
|
2730
3139
|
if (e instanceof ApiError) {
|
|
@@ -2746,6 +3155,7 @@ async function orgScheduleGetCommand(orgName) {
|
|
|
2746
3155
|
if (resp.schedule) {
|
|
2747
3156
|
const s = resp.schedule;
|
|
2748
3157
|
console.log(`Schedule for ${orgName}: days [${s.weekdays.join(",")}] ${s.start_local}\u2013${s.end_local} ${s.tz}`);
|
|
3158
|
+
if (s.primary_top5) console.log(" Primary top-5 conversations cap: ON.");
|
|
2749
3159
|
} else {
|
|
2750
3160
|
console.log(`No schedule configured for ${orgName}.`);
|
|
2751
3161
|
}
|
|
@@ -2896,7 +3306,7 @@ async function main() {
|
|
|
2896
3306
|
const orgName = parseFlag(argv.slice(1), "--organisation");
|
|
2897
3307
|
return createRaceCommand(orgName);
|
|
2898
3308
|
}
|
|
2899
|
-
if (cmd === "join") return joinCommand(argv[1]);
|
|
3309
|
+
if (cmd === "join") return joinCommand(argv[1], argv.slice(2));
|
|
2900
3310
|
if (cmd === "end") return endCommand(argv[1]);
|
|
2901
3311
|
if (cmd === "roll") return rollCommand();
|
|
2902
3312
|
console.error(`Unknown command: ${cmd}`);
|