@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/README.md +21 -1
- package/dist/bin.js +570 -138
- 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.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,
|
|
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, stallReason, 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,57 @@ 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__ */ jsxs5(Text7, { color: "yellow", children: [
|
|
1508
|
+
"\u26A0 ",
|
|
1509
|
+
stallReason ?? "Can't read token usage",
|
|
1510
|
+
". Your race continues."
|
|
1511
|
+
] })
|
|
1445
1512
|
] }),
|
|
1446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1540
|
-
if (!
|
|
1541
|
-
await collectJsonl(projectDir,
|
|
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
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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`
|
|
1596
|
-
*
|
|
1597
|
-
* -
|
|
1893
|
+
* - `null` or a `{ stall }` reading → stall (warning), anchors untouched. A
|
|
1894
|
+
* stall reading also captures its cause for the UI.
|
|
1895
|
+
* - otherwise → secondaries 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1951
|
+
/** Pin anchors to the latest readings so the next deltas are 0 (pending race). */
|
|
1617
1952
|
reprime() {
|
|
1618
|
-
|
|
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 {
|
|
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
|
|
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] =
|
|
1633
|
-
const [lastHbAt, setLastHbAt] =
|
|
1634
|
-
const [lastHbOk, setLastHbOk] =
|
|
1635
|
-
const [tickNow, setTickNow] =
|
|
1636
|
-
const [fatalError, setFatalError] =
|
|
1637
|
-
const [achievements, setAchievements] =
|
|
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] =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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__ */
|
|
1721
|
-
/* @__PURE__ */
|
|
1722
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1726
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1740
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1744
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2125
|
+
/* @__PURE__ */ jsxs6(Text8, { color: "yellow", bold: true, children: [
|
|
1750
2126
|
"+",
|
|
1751
2127
|
event.xp,
|
|
1752
2128
|
" XP "
|
|
1753
2129
|
] }),
|
|
1754
|
-
/* @__PURE__ */
|
|
1755
|
-
/* @__PURE__ */
|
|
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
|
|
2157
|
+
let secondary = { claude: 0, codex: 0, gemini: 0 };
|
|
2158
|
+
const primaryConvAcked = {};
|
|
1782
2159
|
try {
|
|
1783
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
2122
|
-
import { Box as
|
|
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
|
|
2126
|
-
import { Box as
|
|
2127
|
-
import { jsx as
|
|
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__ */
|
|
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__ */
|
|
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] =
|
|
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__ */
|
|
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
|
|
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__ */
|
|
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] =
|
|
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__ */
|
|
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] =
|
|
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__ */
|
|
2304
|
-
if (phase === "open1") return /* @__PURE__ */
|
|
2305
|
-
if (phase === "open2") return /* @__PURE__ */
|
|
2306
|
-
if (phase === "empty") return /* @__PURE__ */
|
|
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__ */
|
|
2736
|
+
return /* @__PURE__ */ jsx10(ConfettiBurst, { tier: outcome.hat.rarity });
|
|
2309
2737
|
}
|
|
2310
|
-
if (outcome.kind === "no_hat") return /* @__PURE__ */
|
|
2311
|
-
return outcome.hat.rarity === "legendary" ? /* @__PURE__ */
|
|
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
|
|
2316
|
-
import { Box as
|
|
2317
|
-
import { jsx as
|
|
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] =
|
|
2320
|
-
|
|
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__ */
|
|
2340
|
-
/* @__PURE__ */
|
|
2341
|
-
horses.map((h, i) => /* @__PURE__ */
|
|
2342
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2775
|
+
/* @__PURE__ */ jsxs7(Text11, { color: "cyan", children: [
|
|
2348
2776
|
"[Lvl. ",
|
|
2349
2777
|
levelFromXp(h.xp),
|
|
2350
2778
|
"]"
|
|
2351
2779
|
] }),
|
|
2352
2780
|
" ",
|
|
2353
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2361
|
-
/* @__PURE__ */
|
|
2362
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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(
|
|
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(
|
|
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}`);
|