@mauricode/token-derby 3.1.3 → 4.0.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 +40 -8
- package/dist/bin.js +1537 -1012
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
// src/commands/stable-create.ts
|
|
19
|
-
import
|
|
19
|
+
import React2 from "react";
|
|
20
20
|
import { render } from "ink";
|
|
21
21
|
|
|
22
22
|
// src/ui/HorseCreator.tsx
|
|
@@ -25,10 +25,9 @@ import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
|
25
25
|
import TextInput from "ink-text-input";
|
|
26
26
|
|
|
27
27
|
// ../shared/dist/models.js
|
|
28
|
-
var
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return typeof v === "string" && MODEL_KEYS.includes(v);
|
|
28
|
+
var MODEL_FAMILIES = ["anthropic", "openai", "google"];
|
|
29
|
+
function zeroPerFamily() {
|
|
30
|
+
return { anthropic: 0, openai: 0, google: 0 };
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
// ../shared/dist/constants.js
|
|
@@ -102,51 +101,22 @@ var ACHIEVEMENT_DESCRIPTIONS = {
|
|
|
102
101
|
"Racer!": "Raced continuously for an hour",
|
|
103
102
|
"Overtake!": "Overtook another horse",
|
|
104
103
|
"Pacesetter!": "Led the race for an hour straight",
|
|
105
|
-
"Stampede!": "Gained
|
|
104
|
+
"Stampede!": "Gained 70,000+ tokens in a single minute",
|
|
106
105
|
"Took the lead!": "Charged into first place",
|
|
107
106
|
"Comeback!": "Climbed from last place to the top half",
|
|
108
|
-
"Pulled Away!": "Grew the lead by
|
|
107
|
+
"Pulled Away!": "Grew the lead by 50,000+ tokens in a minute"
|
|
109
108
|
};
|
|
110
109
|
function overtakeDescription(positionsClimbed) {
|
|
111
110
|
if (positionsClimbed <= 1)
|
|
112
111
|
return "Overtook another horse";
|
|
113
112
|
return `Overtook ${positionsClimbed} horses`;
|
|
114
113
|
}
|
|
115
|
-
|
|
116
|
-
function tokenMultiplier(race) {
|
|
117
|
-
return race.counts_input ? TOKEN_INPUT_MULTIPLIER : 1;
|
|
118
|
-
}
|
|
119
|
-
function describeAchievement(event, race) {
|
|
114
|
+
function describeAchievement(event) {
|
|
120
115
|
if (event.name === "Overtake!") {
|
|
121
116
|
return overtakeDescription(Math.floor(event.xp / 3));
|
|
122
117
|
}
|
|
123
|
-
const m = tokenMultiplier(race);
|
|
124
|
-
if (event.name === "Stampede!") {
|
|
125
|
-
return `Gained ${(MIDRACE_THRESHOLDS.stampede_tokens * m).toLocaleString("en-US")}+ tokens in a single minute`;
|
|
126
|
-
}
|
|
127
|
-
if (event.name === "Pulled Away!") {
|
|
128
|
-
return `Grew the lead by ${(MIDRACE_THRESHOLDS.pulled_away_gap * m).toLocaleString("en-US")}+ tokens in a minute`;
|
|
129
|
-
}
|
|
130
118
|
return ACHIEVEMENT_DESCRIPTIONS[event.name];
|
|
131
119
|
}
|
|
132
|
-
var MIDRACE_THRESHOLDS = {
|
|
133
|
-
warm_up_fraction: 0.08,
|
|
134
|
-
// first 8% of race time
|
|
135
|
-
streak_hour_ms: 36e5,
|
|
136
|
-
// 1 hour for Racer!/Pacesetter!
|
|
137
|
-
racer_dt_cap_ms: 9e4,
|
|
138
|
-
// single-tick credit cap for Racer!
|
|
139
|
-
stampede_tokens: 7e3,
|
|
140
|
-
// tokens-in-a-minute threshold
|
|
141
|
-
stampede_cooldown_ms: 72e5,
|
|
142
|
-
// 2 hours
|
|
143
|
-
pulled_away_gap: 5e3,
|
|
144
|
-
// gap-growth threshold per minute
|
|
145
|
-
pulled_away_cooldown_ms: 72e5,
|
|
146
|
-
// 2 hours
|
|
147
|
-
recent_events_retention_ms: 9e4
|
|
148
|
-
// sliding window for recent_events
|
|
149
|
-
};
|
|
150
120
|
|
|
151
121
|
// ../shared/dist/hats.js
|
|
152
122
|
function hatById(id) {
|
|
@@ -280,38 +250,132 @@ function toTag(c, y) {
|
|
|
280
250
|
}
|
|
281
251
|
}
|
|
282
252
|
|
|
253
|
+
// ../shared/dist/scoring/modifiers/stamina.js
|
|
254
|
+
var FULL_STAMINA = 100;
|
|
255
|
+
var RECOVER_TICK_CAP_MS = 9e4;
|
|
256
|
+
var STATE_KEY = "level";
|
|
257
|
+
var PARAMS = {
|
|
258
|
+
sustainable_pace: {
|
|
259
|
+
label: "Sustainable pace (tokens/min)",
|
|
260
|
+
min: 1e4,
|
|
261
|
+
max: 2e5,
|
|
262
|
+
step: 2500,
|
|
263
|
+
default: 4e4
|
|
264
|
+
},
|
|
265
|
+
drain_per_min: {
|
|
266
|
+
label: "Drain rate (stamina/min above pace)",
|
|
267
|
+
min: 1,
|
|
268
|
+
max: 12,
|
|
269
|
+
step: 1,
|
|
270
|
+
default: 4
|
|
271
|
+
},
|
|
272
|
+
max_drain_per_min: {
|
|
273
|
+
label: "Max drain rate (stamina/min)",
|
|
274
|
+
min: 2,
|
|
275
|
+
max: 20,
|
|
276
|
+
step: 1,
|
|
277
|
+
default: 6
|
|
278
|
+
},
|
|
279
|
+
recover_per_min: {
|
|
280
|
+
label: "Recovery rate (stamina/min at or below pace)",
|
|
281
|
+
min: 1,
|
|
282
|
+
max: 8,
|
|
283
|
+
step: 1,
|
|
284
|
+
default: 2
|
|
285
|
+
},
|
|
286
|
+
taper_floor: {
|
|
287
|
+
label: "Taper floor (stamina %)",
|
|
288
|
+
min: 10,
|
|
289
|
+
max: 60,
|
|
290
|
+
step: 5,
|
|
291
|
+
default: 25
|
|
292
|
+
},
|
|
293
|
+
tired_multiplier: {
|
|
294
|
+
label: "Tired multiplier",
|
|
295
|
+
min: 0.2,
|
|
296
|
+
max: 0.9,
|
|
297
|
+
step: 0.05,
|
|
298
|
+
default: 0.5
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
var fmt = (n) => Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(n);
|
|
302
|
+
var stamina = {
|
|
303
|
+
id: "stamina",
|
|
304
|
+
label: "Stamina",
|
|
305
|
+
description: "Horses running above a sustainable pace tire and score less until they recover.",
|
|
306
|
+
enabledByDefault: false,
|
|
307
|
+
params: PARAMS,
|
|
308
|
+
apply(ctx) {
|
|
309
|
+
const level = ctx.state[STATE_KEY] ?? FULL_STAMINA;
|
|
310
|
+
const minutes = ctx.dt_ms / 6e4;
|
|
311
|
+
if (minutes <= 0)
|
|
312
|
+
return { multiplier: 1, state: { [STATE_KEY]: level } };
|
|
313
|
+
const step = staminaStep({ level, pace: ctx.delta / minutes, minutes, params: ctx.params });
|
|
314
|
+
return { multiplier: step.multiplier, state: { [STATE_KEY]: step.level } };
|
|
315
|
+
},
|
|
316
|
+
preview(params) {
|
|
317
|
+
const p = params;
|
|
318
|
+
const drained = staminaStep({ level: FULL_STAMINA, pace: p.sustainable_pace * 2, minutes: 1, params: p });
|
|
319
|
+
const perMin = FULL_STAMINA - drained.level;
|
|
320
|
+
const spent = staminaStep({ level: 0, pace: 0, minutes: 1, params: p });
|
|
321
|
+
return [
|
|
322
|
+
{ label: "Draining begins above", value: `${fmt(p.sustainable_pace)} tokens/min` },
|
|
323
|
+
{ label: "At twice that pace, full stamina reaches the floor in", value: `${fmt((FULL_STAMINA - p.taper_floor) / perMin)} min` },
|
|
324
|
+
{ label: "A fully spent horse scores at", value: `${fmt(spent.multiplier * 100)}%` },
|
|
325
|
+
{ label: "Empty to full takes", value: `${fmt(FULL_STAMINA / spent.level)} min` }
|
|
326
|
+
];
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
function staminaStep(input) {
|
|
330
|
+
const { level, pace, minutes, params } = input;
|
|
331
|
+
const multiplier = level >= params.taper_floor ? 1 : params.tired_multiplier + (1 - params.tired_multiplier) * (level / params.taper_floor);
|
|
332
|
+
let next = level;
|
|
333
|
+
if (pace > params.sustainable_pace) {
|
|
334
|
+
const perMin = Math.min((pace / params.sustainable_pace - 1) * params.drain_per_min, params.max_drain_per_min);
|
|
335
|
+
next -= perMin * minutes;
|
|
336
|
+
} else {
|
|
337
|
+
const credited = Math.min(minutes, RECOVER_TICK_CAP_MS / 6e4);
|
|
338
|
+
next += params.recover_per_min * credited;
|
|
339
|
+
}
|
|
340
|
+
return { multiplier, level: Math.max(0, Math.min(FULL_STAMINA, next)) };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ../shared/dist/scoring/registry.js
|
|
344
|
+
var MODIFIERS = {
|
|
345
|
+
stamina
|
|
346
|
+
};
|
|
347
|
+
var MODIFIER_IDS = Object.keys(MODIFIERS);
|
|
348
|
+
|
|
349
|
+
// ../shared/dist/scoring/modifier.js
|
|
350
|
+
function resolveParams(modifier, overrides = {}) {
|
|
351
|
+
const out = {};
|
|
352
|
+
for (const [key, bound] of Object.entries(modifier.params)) {
|
|
353
|
+
const value = overrides[key];
|
|
354
|
+
out[key] = typeof value === "number" && Number.isFinite(value) ? value : bound.default;
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
|
|
283
359
|
// ../shared/dist/scoring.js
|
|
360
|
+
function settingFor(race, id) {
|
|
361
|
+
if (race.modifiers)
|
|
362
|
+
return race.modifiers[id];
|
|
363
|
+
if (id !== "stamina")
|
|
364
|
+
return void 0;
|
|
365
|
+
return race.stamina ? { enabled: true, params: race.stamina_config ?? {} } : { enabled: false };
|
|
366
|
+
}
|
|
367
|
+
function staminaOf(horse) {
|
|
368
|
+
return horse.modifier_states?.stamina?.[STATE_KEY] ?? FULL_STAMINA;
|
|
369
|
+
}
|
|
284
370
|
function scoredOf(horse) {
|
|
285
371
|
return horse.scored_tokens ?? horse.current_tokens;
|
|
286
372
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
MAX_DRAIN_PER_MIN: 6,
|
|
291
|
-
RECOVER_PER_MIN: 2,
|
|
292
|
-
RECOVER_TICK_CAP_MS: 9e4,
|
|
293
|
-
TAPER_FLOOR: 25,
|
|
294
|
-
TIRED_MULTIPLIER: 0.5
|
|
295
|
-
};
|
|
373
|
+
function resolveModifierParams(race, id) {
|
|
374
|
+
return resolveParams(MODIFIERS[id], settingFor(race, id)?.params ?? {});
|
|
375
|
+
}
|
|
296
376
|
function resolveStaminaConfig(race) {
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
sustainable_pace: c.sustainable_pace ?? STAMINA.SUSTAINABLE_PACE,
|
|
300
|
-
drain_per_min: c.drain_per_min ?? STAMINA.DRAIN_PER_MIN,
|
|
301
|
-
max_drain_per_min: c.max_drain_per_min ?? STAMINA.MAX_DRAIN_PER_MIN,
|
|
302
|
-
recover_per_min: c.recover_per_min ?? STAMINA.RECOVER_PER_MIN,
|
|
303
|
-
taper_floor: c.taper_floor ?? STAMINA.TAPER_FLOOR,
|
|
304
|
-
tired_multiplier: c.tired_multiplier ?? STAMINA.TIRED_MULTIPLIER
|
|
305
|
-
};
|
|
377
|
+
return resolveModifierParams(race, "stamina");
|
|
306
378
|
}
|
|
307
|
-
var STAMINA_PARAM_BOUNDS = {
|
|
308
|
-
sustainable_pace: { min: 1e3, max: 2e4, step: 250, default: STAMINA.SUSTAINABLE_PACE },
|
|
309
|
-
drain_per_min: { min: 1, max: 12, step: 1, default: STAMINA.DRAIN_PER_MIN },
|
|
310
|
-
max_drain_per_min: { min: 2, max: 20, step: 1, default: STAMINA.MAX_DRAIN_PER_MIN },
|
|
311
|
-
recover_per_min: { min: 1, max: 8, step: 1, default: STAMINA.RECOVER_PER_MIN },
|
|
312
|
-
taper_floor: { min: 10, max: 60, step: 5, default: STAMINA.TAPER_FLOOR },
|
|
313
|
-
tired_multiplier: { min: 0.2, max: 0.9, step: 0.05, default: STAMINA.TIRED_MULTIPLIER }
|
|
314
|
-
};
|
|
315
379
|
|
|
316
380
|
// ../shared/dist/devices.js
|
|
317
381
|
var DEVICE_CODE_LENGTH = Math.ceil(SECRET_TOKEN_BYTES * 4 / 3);
|
|
@@ -857,13 +921,13 @@ function apiBase() {
|
|
|
857
921
|
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
858
922
|
var SCAN_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 0.75;
|
|
859
923
|
var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
|
|
860
|
-
var
|
|
924
|
+
var SILENT_THRESHOLD = 10;
|
|
861
925
|
|
|
862
926
|
// src/version.ts
|
|
863
927
|
import { createRequire } from "module";
|
|
864
928
|
function readVersion() {
|
|
865
|
-
if ("
|
|
866
|
-
return "
|
|
929
|
+
if ("4.0.0".length > 0) {
|
|
930
|
+
return "4.0.0";
|
|
867
931
|
}
|
|
868
932
|
try {
|
|
869
933
|
const req = createRequire(import.meta.url);
|
|
@@ -877,7 +941,6 @@ var CLI_VERSION = readVersion();
|
|
|
877
941
|
|
|
878
942
|
// src/identity/identity.ts
|
|
879
943
|
import { promises as fs } from "fs";
|
|
880
|
-
import * as path3 from "path";
|
|
881
944
|
|
|
882
945
|
// src/paths.ts
|
|
883
946
|
import * as os2 from "os";
|
|
@@ -891,6 +954,9 @@ function homeDir() {
|
|
|
891
954
|
function identityFile() {
|
|
892
955
|
return path2.join(homeDir(), "identity.json");
|
|
893
956
|
}
|
|
957
|
+
function prefsFile() {
|
|
958
|
+
return path2.join(homeDir(), "prefs.json");
|
|
959
|
+
}
|
|
894
960
|
function activeRaceFile(joinCode) {
|
|
895
961
|
return path2.join(homeDir(), "active-races", `${joinCode}.json`);
|
|
896
962
|
}
|
|
@@ -910,6 +976,9 @@ function codexSessionsDir() {
|
|
|
910
976
|
function geminiTmpDir() {
|
|
911
977
|
return process.env.TOKEN_DERBY_GEMINI_DIR ?? path2.join(os2.homedir(), ".gemini", "tmp");
|
|
912
978
|
}
|
|
979
|
+
function piSessionsDir() {
|
|
980
|
+
return process.env.TOKEN_DERBY_PI_DIR ?? path2.join(os2.homedir(), ".pi", "agent", "sessions");
|
|
981
|
+
}
|
|
913
982
|
function logDir() {
|
|
914
983
|
return path2.join(homeDir(), "logs");
|
|
915
984
|
}
|
|
@@ -1204,7 +1273,7 @@ function logoutDevice() {
|
|
|
1204
1273
|
async function stableCreateCommand() {
|
|
1205
1274
|
let exitCode = 0;
|
|
1206
1275
|
const app = render(
|
|
1207
|
-
|
|
1276
|
+
React2.createElement(HorseCreator, {
|
|
1208
1277
|
onSubmit: async (name, colors) => {
|
|
1209
1278
|
try {
|
|
1210
1279
|
await createStableHorse({ name, colors });
|
|
@@ -1236,7 +1305,7 @@ async function stableCreateCommand() {
|
|
|
1236
1305
|
}
|
|
1237
1306
|
|
|
1238
1307
|
// src/commands/stable-list.tsx
|
|
1239
|
-
import
|
|
1308
|
+
import React3 from "react";
|
|
1240
1309
|
import { render as render2, Box as Box4, Text as Text4 } from "ink";
|
|
1241
1310
|
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1242
1311
|
async function stableListCommand() {
|
|
@@ -1256,13 +1325,13 @@ async function stableListCommand() {
|
|
|
1256
1325
|
return 0;
|
|
1257
1326
|
}
|
|
1258
1327
|
const app = render2(
|
|
1259
|
-
|
|
1328
|
+
React3.createElement(StableList, { horses })
|
|
1260
1329
|
);
|
|
1261
1330
|
await app.waitUntilExit();
|
|
1262
1331
|
return 0;
|
|
1263
1332
|
}
|
|
1264
1333
|
function StableList({ horses }) {
|
|
1265
|
-
|
|
1334
|
+
React3.useEffect(() => {
|
|
1266
1335
|
setImmediate(() => process.exit(0));
|
|
1267
1336
|
}, []);
|
|
1268
1337
|
return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", children: [
|
|
@@ -1331,7 +1400,7 @@ async function stableDeleteCommand(name) {
|
|
|
1331
1400
|
}
|
|
1332
1401
|
|
|
1333
1402
|
// src/commands/stable-edit.ts
|
|
1334
|
-
import
|
|
1403
|
+
import React4 from "react";
|
|
1335
1404
|
import { render as render3 } from "ink";
|
|
1336
1405
|
|
|
1337
1406
|
// src/ui/HorsePicker.tsx
|
|
@@ -1388,235 +1457,798 @@ function HorsePicker({ horses, onPick, onCancel, prompt = "Pick a horse to race:
|
|
|
1388
1457
|
] });
|
|
1389
1458
|
}
|
|
1390
1459
|
|
|
1391
|
-
// src/
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
if (existing === "cancelled") {
|
|
1405
|
-
console.log("Cancelled.");
|
|
1406
|
-
return 1;
|
|
1460
|
+
// src/stable/prefs.ts
|
|
1461
|
+
import * as fs7 from "fs/promises";
|
|
1462
|
+
|
|
1463
|
+
// src/tokens/harnesses/claude-code/index.ts
|
|
1464
|
+
import * as fs3 from "fs/promises";
|
|
1465
|
+
import * as path3 from "path";
|
|
1466
|
+
|
|
1467
|
+
// src/tokens/source-root.ts
|
|
1468
|
+
var SourceRootMissing = class extends Error {
|
|
1469
|
+
constructor(dir) {
|
|
1470
|
+
super(`No history directory at ${dir}`);
|
|
1471
|
+
this.dir = dir;
|
|
1472
|
+
this.name = "SourceRootMissing";
|
|
1407
1473
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
React6.createElement(HorseCreator, {
|
|
1412
|
-
initialColors: existing.colors,
|
|
1413
|
-
initialName: existing.name,
|
|
1414
|
-
lockName: true,
|
|
1415
|
-
initialLevel: levelFromXp(existing.xp),
|
|
1416
|
-
hats: existing.hats,
|
|
1417
|
-
initialEquipped,
|
|
1418
|
-
onSubmit: async (_name, colors, hatChoice) => {
|
|
1419
|
-
const colorsChanged = !sameColors(colors, existing.colors);
|
|
1420
|
-
const hatChanged = hatChoice !== void 0 && hatChoice !== initialEquipped;
|
|
1421
|
-
try {
|
|
1422
|
-
if (colorsChanged) await updateStableHorse(existing.stable_horse_id, { colors });
|
|
1423
|
-
if (hatChanged) await equipHat(existing.stable_horse_id, { hat_index: hatChoice });
|
|
1424
|
-
app.unmount();
|
|
1425
|
-
if (!colorsChanged && !hatChanged) {
|
|
1426
|
-
console.log(`No changes for "${existing.name}".`);
|
|
1427
|
-
} else {
|
|
1428
|
-
const parts = [];
|
|
1429
|
-
if (colorsChanged) parts.push("colors");
|
|
1430
|
-
if (hatChanged) parts.push(hatChoice === null ? "hat unequipped" : "hat equipped");
|
|
1431
|
-
console.log(`\u2713 Updated "${existing.name}" (${parts.join(", ")}).`);
|
|
1432
|
-
}
|
|
1433
|
-
} catch (e) {
|
|
1434
|
-
app.unmount();
|
|
1435
|
-
if (e instanceof ApiError) {
|
|
1436
|
-
console.error(`Error: ${e.code} ${e.message}`);
|
|
1437
|
-
exitCode = 1;
|
|
1438
|
-
return;
|
|
1439
|
-
}
|
|
1440
|
-
throw e;
|
|
1441
|
-
}
|
|
1442
|
-
},
|
|
1443
|
-
onCancel: () => {
|
|
1444
|
-
app.unmount();
|
|
1445
|
-
console.log("Cancelled.");
|
|
1446
|
-
exitCode = 1;
|
|
1447
|
-
}
|
|
1448
|
-
})
|
|
1449
|
-
);
|
|
1450
|
-
await app.waitUntilExit();
|
|
1451
|
-
return exitCode;
|
|
1452
|
-
}
|
|
1453
|
-
function sameColors(a, b) {
|
|
1454
|
-
return a.body === b.body && a.mane === b.mane && a.tail === b.tail && a.saddle === b.saddle;
|
|
1455
|
-
}
|
|
1456
|
-
async function fetchStable() {
|
|
1474
|
+
dir;
|
|
1475
|
+
};
|
|
1476
|
+
async function readRoot(dir, read) {
|
|
1457
1477
|
try {
|
|
1458
|
-
|
|
1459
|
-
return resp.horses;
|
|
1478
|
+
return await read();
|
|
1460
1479
|
} catch (e) {
|
|
1461
|
-
if (e
|
|
1462
|
-
console.error(`Error: ${e.code} ${e.message}`);
|
|
1463
|
-
return null;
|
|
1464
|
-
}
|
|
1480
|
+
if (e?.code === "ENOENT") throw new SourceRootMissing(dir);
|
|
1465
1481
|
throw e;
|
|
1466
1482
|
}
|
|
1467
1483
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
},
|
|
1482
|
-
onCancel: () => {
|
|
1483
|
-
app.unmount();
|
|
1484
|
-
resolve(null);
|
|
1485
|
-
}
|
|
1486
|
-
})
|
|
1487
|
-
);
|
|
1488
|
-
});
|
|
1489
|
-
return picked ?? "cancelled";
|
|
1484
|
+
|
|
1485
|
+
// src/tokens/harnesses/harness.ts
|
|
1486
|
+
function incremental(fold, families, notices) {
|
|
1487
|
+
return { mode: "incremental", fold, families, ...notices ? { notices } : {} };
|
|
1488
|
+
}
|
|
1489
|
+
function custom(read) {
|
|
1490
|
+
return { mode: "custom", read };
|
|
1491
|
+
}
|
|
1492
|
+
function wholeFile(parse2) {
|
|
1493
|
+
return { mode: "whole-file", parse: parse2 };
|
|
1494
|
+
}
|
|
1495
|
+
function constant(family) {
|
|
1496
|
+
return (state) => ({ [family]: { input: state.input, output: state.output } });
|
|
1490
1497
|
}
|
|
1491
1498
|
|
|
1492
|
-
// src/
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
var
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
return 1;
|
|
1515
|
-
}
|
|
1516
|
-
const end = new Date(new Date(start).getTime() + durationHours * 36e5).toISOString();
|
|
1517
|
-
const tz = (await rl.question(`Time zone [${DEFAULT_TZ}]: `)).trim() || DEFAULT_TZ;
|
|
1518
|
-
const maxRaw = (await rl.question("Max participants [30]: ")).trim();
|
|
1519
|
-
const max = maxRaw ? parseInt(maxRaw, 10) : void 0;
|
|
1520
|
-
if (max !== void 0 && (!Number.isFinite(max) || max < 1)) {
|
|
1521
|
-
console.error("Max participants must be a positive number.");
|
|
1522
|
-
return 1;
|
|
1523
|
-
}
|
|
1524
|
-
let org = organisationName;
|
|
1525
|
-
if (org === void 0) {
|
|
1526
|
-
const raw = (await rl.question("Organisation (blank for none): ")).trim();
|
|
1527
|
-
if (raw) org = raw;
|
|
1528
|
-
}
|
|
1529
|
-
if (org !== void 0 && !ORG_NAME_PATTERN.test(org)) {
|
|
1530
|
-
console.error("Organisation name must be 1\u201312 alphanumeric characters.");
|
|
1531
|
-
return 1;
|
|
1532
|
-
}
|
|
1533
|
-
const countInputRaw = (await rl.question("Count input tokens (fresh input + cache creation) toward race totals? [y/N]: ")).trim().toLowerCase();
|
|
1534
|
-
const counts_input = countInputRaw === "y" || countInputRaw === "yes";
|
|
1535
|
-
const top5Raw = (await rl.question("Count only each racer's 5 most-active conversations toward their primary model's score? [y/N]: ")).trim().toLowerCase();
|
|
1536
|
-
const primary_top5 = top5Raw === "y" || top5Raw === "yes";
|
|
1537
|
-
const staminaRaw = (await rl.question("Stamina \u2014 horses that run flat out tire and score less until they recover? [y/N]: ")).trim().toLowerCase();
|
|
1538
|
-
const stamina = staminaRaw === "y" || staminaRaw === "yes";
|
|
1539
|
-
const resp = await createRace({
|
|
1540
|
-
name,
|
|
1541
|
-
start_time: start,
|
|
1542
|
-
end_time: end,
|
|
1543
|
-
tz,
|
|
1544
|
-
...max !== void 0 ? { max_participants: max } : {},
|
|
1545
|
-
...org ? { organisation_name: org } : {},
|
|
1546
|
-
...counts_input ? { counts_input: true } : {},
|
|
1547
|
-
...primary_top5 ? { primary_top5: true } : {},
|
|
1548
|
-
...stamina ? { stamina: true } : {}
|
|
1549
|
-
});
|
|
1550
|
-
console.log("");
|
|
1551
|
-
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");
|
|
1552
|
-
console.log(` \u2551 JOIN CODE: ${resp.join_code.padEnd(23)}\u2551`);
|
|
1553
|
-
console.log(" \u255A\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\u255D");
|
|
1554
|
-
console.log("");
|
|
1555
|
-
console.log(` Admin code: ${resp.admin_code}`);
|
|
1556
|
-
console.log(" \u26A0 Save the admin code \u2014 you need it to end the race early.");
|
|
1557
|
-
console.log("");
|
|
1558
|
-
if (org) {
|
|
1559
|
-
console.log(` Restricted to organisation: ${org}`);
|
|
1560
|
-
}
|
|
1561
|
-
if (counts_input) {
|
|
1562
|
-
console.log(" Counting input + output tokens (excluding cache reads).");
|
|
1563
|
-
}
|
|
1564
|
-
if (primary_top5) {
|
|
1565
|
-
console.log(" Primary score counts only each racer's top 5 conversations per beat.");
|
|
1499
|
+
// src/tokens/harnesses/claude-code/index.ts
|
|
1500
|
+
var MAX_PROJECT_DEPTH = 8;
|
|
1501
|
+
var TRANSCRIPT_EXT = ".jsonl";
|
|
1502
|
+
var CLAUDE_FOLD = {
|
|
1503
|
+
empty: () => ({ input: 0, output: 0 }),
|
|
1504
|
+
append: (acc, lines) => {
|
|
1505
|
+
let { input, output, last } = acc;
|
|
1506
|
+
for (const line of lines) {
|
|
1507
|
+
if (!line.trim()) continue;
|
|
1508
|
+
let parsed;
|
|
1509
|
+
try {
|
|
1510
|
+
parsed = JSON.parse(line);
|
|
1511
|
+
} catch {
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
const usage = parsed?.message?.usage;
|
|
1515
|
+
if (!usage) continue;
|
|
1516
|
+
const id = parsed?.requestId ?? parsed?.message?.id ?? void 0;
|
|
1517
|
+
if (id !== void 0 && id === last) continue;
|
|
1518
|
+
last = id;
|
|
1519
|
+
input += addNum(usage.input_tokens) + addNum(usage.cache_creation_input_tokens);
|
|
1520
|
+
output += addNum(usage.output_tokens);
|
|
1566
1521
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1522
|
+
return last === void 0 ? { input, output } : { input, output, last };
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
function addNum(value) {
|
|
1526
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1527
|
+
}
|
|
1528
|
+
var claudeCode = {
|
|
1529
|
+
id: "claude-code",
|
|
1530
|
+
label: "Claude Code",
|
|
1531
|
+
enabledByDefault: true,
|
|
1532
|
+
// counted since before harnesses were configurable
|
|
1533
|
+
overrideVar: "TOKEN_DERBY_CLAUDE_DIR",
|
|
1534
|
+
hints: [
|
|
1535
|
+
`If CLAUDE_CONFIG_DIR relocated your config, Token Derby follows it \u2014`,
|
|
1536
|
+
`check it points at the config root, not the projects directory.`
|
|
1537
|
+
],
|
|
1538
|
+
root: claudeProjectsDir,
|
|
1539
|
+
counting: incremental(CLAUDE_FOLD, constant("anthropic")),
|
|
1540
|
+
async discover(root) {
|
|
1541
|
+
const entries = await readRoot(root, () => fs3.readdir(root, { withFileTypes: true }));
|
|
1542
|
+
const out = [];
|
|
1543
|
+
for (const entry of entries) {
|
|
1544
|
+
if (!await isDirectory(entry, root)) continue;
|
|
1545
|
+
await collect(path3.join(root, entry.name), MAX_PROJECT_DEPTH, out);
|
|
1569
1546
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1547
|
+
return out;
|
|
1548
|
+
},
|
|
1549
|
+
// A "conversation" is one top-level session: <project>/<session>. The main
|
|
1550
|
+
// session transcript and everything nested under <session>/subagents/** roll
|
|
1551
|
+
// up into the same id.
|
|
1552
|
+
conversationId(file, root) {
|
|
1553
|
+
const rel = path3.relative(root, file);
|
|
1554
|
+
const [project, session] = rel.split(path3.sep);
|
|
1555
|
+
if (project === void 0 || session === void 0) return rel.replace(/\.jsonl$/, "");
|
|
1556
|
+
return `${project}/${session.replace(/\.jsonl$/, "")}`;
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
async function collect(dir, depth, out) {
|
|
1560
|
+
if (depth <= 0) return;
|
|
1561
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1562
|
+
for (const entry of entries) {
|
|
1563
|
+
if (entry.name.endsWith(TRANSCRIPT_EXT)) {
|
|
1564
|
+
out.push(path3.join(dir, entry.name));
|
|
1565
|
+
} else if (depth > 1 && await isDirectory(entry, dir)) {
|
|
1566
|
+
await collect(path3.join(dir, entry.name), depth - 1, out);
|
|
1576
1567
|
}
|
|
1577
|
-
throw e;
|
|
1578
|
-
} finally {
|
|
1579
|
-
rl.close();
|
|
1580
1568
|
}
|
|
1581
1569
|
}
|
|
1582
|
-
function
|
|
1583
|
-
if (
|
|
1584
|
-
|
|
1585
|
-
return
|
|
1570
|
+
async function isDirectory(entry, parent) {
|
|
1571
|
+
if (entry.isDirectory()) return true;
|
|
1572
|
+
if (!entry.isSymbolicLink()) return false;
|
|
1573
|
+
return fs3.stat(path3.join(parent, entry.name)).then((st) => st.isDirectory()).catch(() => false);
|
|
1586
1574
|
}
|
|
1587
1575
|
|
|
1588
|
-
// src/
|
|
1589
|
-
import
|
|
1590
|
-
import
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1576
|
+
// src/tokens/harnesses/codex-cli/index.ts
|
|
1577
|
+
import * as fs4 from "fs/promises";
|
|
1578
|
+
import * as path4 from "path";
|
|
1579
|
+
var SESSION_DIRS = ["sessions", "archived_sessions"];
|
|
1580
|
+
var ROLLOUT_PREFIX = "rollout-";
|
|
1581
|
+
var ROLLOUT_EXT = ".jsonl";
|
|
1582
|
+
var CODEX_FOLD = {
|
|
1583
|
+
empty: () => ({ input: 0, output: 0 }),
|
|
1584
|
+
append: (acc, lines) => {
|
|
1585
|
+
let usage = null;
|
|
1586
|
+
for (const line of lines) {
|
|
1587
|
+
if (!line.trim()) continue;
|
|
1588
|
+
let parsed;
|
|
1589
|
+
try {
|
|
1590
|
+
parsed = JSON.parse(line);
|
|
1591
|
+
} catch {
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
if (parsed?.payload?.type === "token_count" && parsed.payload.info?.total_token_usage) {
|
|
1595
|
+
usage = parsed.payload.info.total_token_usage;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (!usage) return acc;
|
|
1599
|
+
return {
|
|
1600
|
+
input: Math.max(0, num(usage.input_tokens) - num(usage.cached_input_tokens)),
|
|
1601
|
+
output: num(usage.output_tokens)
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
function num(v) {
|
|
1606
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1607
|
+
}
|
|
1608
|
+
var codexCli = {
|
|
1609
|
+
id: "codex-cli",
|
|
1610
|
+
label: "Codex CLI",
|
|
1611
|
+
enabledByDefault: true,
|
|
1612
|
+
// counted since before harnesses were configurable
|
|
1613
|
+
overrideVar: "TOKEN_DERBY_CODEX_DIR",
|
|
1614
|
+
root: codexSessionsDir,
|
|
1615
|
+
counting: incremental(CODEX_FOLD, constant("openai")),
|
|
1616
|
+
async discover(root) {
|
|
1617
|
+
await readRoot(root, () => fs4.stat(root));
|
|
1618
|
+
const out = [];
|
|
1619
|
+
for (const dir of SESSION_DIRS) out.push(...await collect2(path4.join(root, dir)));
|
|
1620
|
+
return out;
|
|
1621
|
+
},
|
|
1622
|
+
/** One rollout file is one conversation. */
|
|
1623
|
+
conversationId(file) {
|
|
1624
|
+
return file;
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
async function collect2(dir) {
|
|
1628
|
+
let entries;
|
|
1629
|
+
try {
|
|
1630
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1631
|
+
} catch (e) {
|
|
1632
|
+
if (e?.code === "ENOENT") return [];
|
|
1633
|
+
throw e;
|
|
1634
|
+
}
|
|
1635
|
+
const out = [];
|
|
1636
|
+
for (const entry of entries) {
|
|
1637
|
+
const full = path4.join(dir, entry.name);
|
|
1638
|
+
if (entry.isDirectory()) out.push(...await collect2(full));
|
|
1639
|
+
else if (entry.name.startsWith(ROLLOUT_PREFIX) && entry.name.endsWith(ROLLOUT_EXT)) out.push(full);
|
|
1640
|
+
}
|
|
1641
|
+
return out;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// src/tokens/harnesses/gemini-cli/index.ts
|
|
1645
|
+
import * as fs5 from "fs/promises";
|
|
1646
|
+
import * as path5 from "path";
|
|
1647
|
+
var CHATS_DIR = "chats";
|
|
1648
|
+
var CHAT_EXTS = [".json", ".jsonl"];
|
|
1649
|
+
function num2(v) {
|
|
1650
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1651
|
+
}
|
|
1652
|
+
var geminiCli = {
|
|
1653
|
+
id: "gemini-cli",
|
|
1654
|
+
label: "Gemini CLI",
|
|
1655
|
+
enabledByDefault: true,
|
|
1656
|
+
// counted since before harnesses were configurable
|
|
1657
|
+
overrideVar: "TOKEN_DERBY_GEMINI_DIR",
|
|
1658
|
+
root: geminiTmpDir,
|
|
1659
|
+
// Gemini chats are rewritten whole rather than appended to, so there is no
|
|
1660
|
+
// offset to resume from — the cache gates on mtime+size and recomputes in full.
|
|
1661
|
+
counting: wholeFile((raw, file) => ({ families: { google: sumRaw(file, raw) } })),
|
|
1662
|
+
async discover(root) {
|
|
1663
|
+
const entries = await readRoot(root, () => fs5.readdir(root));
|
|
1664
|
+
const out = [];
|
|
1665
|
+
for (const entry of entries) {
|
|
1666
|
+
const chatsDir = path5.join(root, entry, CHATS_DIR);
|
|
1667
|
+
let files;
|
|
1668
|
+
try {
|
|
1669
|
+
files = await fs5.readdir(chatsDir);
|
|
1670
|
+
} catch {
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
for (const f of files) {
|
|
1674
|
+
if (CHAT_EXTS.some((ext) => f.endsWith(ext))) out.push(path5.join(chatsDir, f));
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return out;
|
|
1678
|
+
},
|
|
1679
|
+
/** One chat file is one conversation. */
|
|
1680
|
+
conversationId(file) {
|
|
1681
|
+
return file;
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
function sumRaw(file, raw) {
|
|
1685
|
+
const messages = file.endsWith(".jsonl") ? parseJsonl(raw) : parseJson(raw);
|
|
1686
|
+
let input = 0;
|
|
1687
|
+
let output = 0;
|
|
1688
|
+
for (const m of messages) {
|
|
1689
|
+
const tk = m?.tokens;
|
|
1690
|
+
if (!tk || typeof tk !== "object") continue;
|
|
1691
|
+
input += Math.max(0, num2(tk.input) - num2(tk.cached));
|
|
1692
|
+
output += num2(tk.output);
|
|
1693
|
+
}
|
|
1694
|
+
return { input, output };
|
|
1695
|
+
}
|
|
1696
|
+
function parseJson(raw) {
|
|
1697
|
+
try {
|
|
1698
|
+
const data = JSON.parse(raw);
|
|
1699
|
+
return Array.isArray(data?.messages) ? data.messages : [];
|
|
1700
|
+
} catch {
|
|
1701
|
+
return [];
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function parseJsonl(raw) {
|
|
1705
|
+
const out = [];
|
|
1706
|
+
for (const line of raw.split("\n")) {
|
|
1707
|
+
if (!line.trim()) continue;
|
|
1708
|
+
try {
|
|
1709
|
+
out.push(JSON.parse(line));
|
|
1710
|
+
} catch {
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return out;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/tokens/harnesses/pi/index.ts
|
|
1717
|
+
import * as fs6 from "fs/promises";
|
|
1718
|
+
import * as path6 from "path";
|
|
1719
|
+
|
|
1720
|
+
// src/tokens/pool.ts
|
|
1721
|
+
var SCAN_CONCURRENCY = 12;
|
|
1722
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1723
|
+
const out = new Array(items.length);
|
|
1724
|
+
let next = 0;
|
|
1725
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1726
|
+
while (true) {
|
|
1727
|
+
const i = next++;
|
|
1728
|
+
if (i >= items.length) return;
|
|
1729
|
+
out[i] = await fn(items[i], i);
|
|
1730
|
+
}
|
|
1603
1731
|
});
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1732
|
+
await Promise.all(workers);
|
|
1733
|
+
return out;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// src/tokens/harnesses/pi/entries.ts
|
|
1737
|
+
import { createHash } from "crypto";
|
|
1738
|
+
function num3(value) {
|
|
1739
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
1740
|
+
}
|
|
1741
|
+
function modelOf(provider, modelId) {
|
|
1742
|
+
return typeof provider === "string" && typeof modelId === "string" && provider && modelId ? { provider, modelId } : null;
|
|
1743
|
+
}
|
|
1744
|
+
function fingerprint(entry, rawLine) {
|
|
1745
|
+
if (typeof entry?.id === "string" && typeof entry?.timestamp === "string") {
|
|
1746
|
+
const role = entry?.message?.role ?? "";
|
|
1747
|
+
return [entry.id, entry.timestamp, entry.type ?? "", role].join("\0");
|
|
1748
|
+
}
|
|
1749
|
+
return createHash("sha256").update(rawLine).digest("base64url");
|
|
1750
|
+
}
|
|
1751
|
+
var PI_FOLD = {
|
|
1752
|
+
empty: () => ({ isSession: false, modelByEntry: {}, events: [] }),
|
|
1753
|
+
append: (acc, lines) => {
|
|
1754
|
+
let isSession = acc.isSession;
|
|
1755
|
+
const modelByEntry = { ...acc.modelByEntry };
|
|
1756
|
+
const events = [...acc.events];
|
|
1757
|
+
for (const line of lines) {
|
|
1758
|
+
if (!line.trim()) continue;
|
|
1759
|
+
let entry;
|
|
1760
|
+
try {
|
|
1761
|
+
entry = JSON.parse(line);
|
|
1762
|
+
} catch {
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
if (entry?.type === "session") {
|
|
1766
|
+
isSession = entry.version === 3;
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
if (!isSession) continue;
|
|
1770
|
+
const inherited = typeof entry?.parentId === "string" ? modelByEntry[entry.parentId] ?? null : null;
|
|
1771
|
+
let active = inherited;
|
|
1772
|
+
if (entry?.type === "model_change") {
|
|
1773
|
+
active = modelOf(entry.provider, entry.modelId);
|
|
1774
|
+
} else if (entry?.type === "message" && entry.message?.role === "assistant") {
|
|
1775
|
+
active = modelOf(entry.message.provider, entry.message.model) ?? inherited;
|
|
1776
|
+
}
|
|
1777
|
+
if (typeof entry?.id === "string") modelByEntry[entry.id] = active;
|
|
1778
|
+
let usage = null;
|
|
1779
|
+
let usageModel = active;
|
|
1780
|
+
if (entry?.type === "message" && (entry.message?.role === "assistant" || entry.message?.role === "toolResult")) {
|
|
1781
|
+
usage = entry.message.usage;
|
|
1782
|
+
} else if ((entry?.type === "compaction" || entry?.type === "branch_summary") && entry.usage) {
|
|
1783
|
+
usage = entry.usage;
|
|
1784
|
+
if (entry.type === "branch_summary" && typeof entry.fromId === "string") {
|
|
1785
|
+
usageModel = modelByEntry[entry.fromId] ?? active;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (!usage) continue;
|
|
1789
|
+
const input = num3(usage.input) + num3(usage.cacheWrite);
|
|
1790
|
+
const output = num3(usage.output);
|
|
1791
|
+
if (input > 0 || output > 0) {
|
|
1792
|
+
events.push({ fingerprint: fingerprint(entry, line), model: usageModel, input, output });
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return { isSession, modelByEntry, events };
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
// src/tokens/harnesses/pi/providers.ts
|
|
1800
|
+
var DIRECT = {
|
|
1801
|
+
anthropic: "anthropic",
|
|
1802
|
+
openai: "openai",
|
|
1803
|
+
"azure-openai-responses": "openai",
|
|
1804
|
+
google: "google"
|
|
1805
|
+
};
|
|
1806
|
+
var GATEWAYS = /* @__PURE__ */ new Set([
|
|
1807
|
+
"amazon-bedrock",
|
|
1808
|
+
"openrouter",
|
|
1809
|
+
"cloudflare-ai-gateway",
|
|
1810
|
+
"vercel-ai-gateway",
|
|
1811
|
+
"radius"
|
|
1812
|
+
]);
|
|
1813
|
+
function resolveProvider(provider) {
|
|
1814
|
+
const normalised = provider.trim().toLowerCase();
|
|
1815
|
+
const family = DIRECT[normalised];
|
|
1816
|
+
if (family) return { kind: "family", family };
|
|
1817
|
+
if (GATEWAYS.has(normalised)) return { kind: "gateway", provider: normalised };
|
|
1818
|
+
return { kind: "other", provider: normalised };
|
|
1819
|
+
}
|
|
1820
|
+
function describeUncounted(uncounted) {
|
|
1821
|
+
const gateways = /* @__PURE__ */ new Set();
|
|
1822
|
+
const others = /* @__PURE__ */ new Set();
|
|
1823
|
+
for (const r of uncounted) {
|
|
1824
|
+
if (r.kind === "gateway") gateways.add(r.provider);
|
|
1825
|
+
else if (r.kind === "other") others.add(r.provider);
|
|
1826
|
+
}
|
|
1827
|
+
const lines = [];
|
|
1828
|
+
if (gateways.size > 0) {
|
|
1829
|
+
lines.push(
|
|
1830
|
+
`Pi usage on ${[...gateways].sort().join(", ")} not counted \u2014 can serve models we score, but not yet identifiable.`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
if (others.size > 0) {
|
|
1834
|
+
lines.push(
|
|
1835
|
+
`Pi usage on ${[...others].sort().join(", ")} not counted \u2014 not one of the model families we score.`
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
return lines;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/tokens/harnesses/pi/index.ts
|
|
1842
|
+
var SESSION_EXT = ".jsonl";
|
|
1843
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set(["subagent-artifacts"]);
|
|
1844
|
+
var pi = {
|
|
1845
|
+
id: "pi",
|
|
1846
|
+
label: "Pi",
|
|
1847
|
+
// Off until asked for: Pi arrived after the others, and adding a harness must
|
|
1848
|
+
// never start counting someone's history behind their back.
|
|
1849
|
+
enabledByDefault: false,
|
|
1850
|
+
overrideVar: "TOKEN_DERBY_PI_DIR",
|
|
1851
|
+
root: piSessionsDir,
|
|
1852
|
+
async discover(root) {
|
|
1853
|
+
await readRoot(root, () => fs6.stat(root));
|
|
1854
|
+
const out = [];
|
|
1855
|
+
await collect3(root, out);
|
|
1856
|
+
return out.sort();
|
|
1857
|
+
},
|
|
1858
|
+
// Unused for a whole-history harness: it groups its own conversations, since
|
|
1859
|
+
// one session's work can be spread across a file and its clones.
|
|
1860
|
+
conversationId(file) {
|
|
1861
|
+
return file;
|
|
1862
|
+
},
|
|
1863
|
+
counting: custom(readAll)
|
|
1864
|
+
};
|
|
1865
|
+
async function collect3(dir, out) {
|
|
1866
|
+
const entries = await fs6.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1867
|
+
for (const entry of entries) {
|
|
1868
|
+
const full = path6.join(dir, entry.name);
|
|
1869
|
+
if (entry.isDirectory()) {
|
|
1870
|
+
if (!IGNORED_DIRS.has(entry.name)) await collect3(full, out);
|
|
1871
|
+
} else if (entry.isFile() && entry.name.endsWith(SESSION_EXT)) {
|
|
1872
|
+
out.push(full);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async function readAll(cache, files, root) {
|
|
1877
|
+
const states = await mapWithConcurrency(files, SCAN_CONCURRENCY, (f) => cache.readIncremental(f, PI_FOLD));
|
|
1878
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
1879
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1880
|
+
const uncounted = [];
|
|
1881
|
+
files.forEach((file, i) => {
|
|
1882
|
+
const state = states[i];
|
|
1883
|
+
if (!state.isSession) return;
|
|
1884
|
+
const conversation = conversationOf(file, root);
|
|
1885
|
+
for (const event of state.events) {
|
|
1886
|
+
if (seen.has(event.fingerprint)) continue;
|
|
1887
|
+
seen.add(event.fingerprint);
|
|
1888
|
+
if (!event.model) continue;
|
|
1889
|
+
const resolved = resolveProvider(event.model.provider);
|
|
1890
|
+
if (resolved.kind !== "family") {
|
|
1891
|
+
uncounted.push(resolved);
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
addTo(byConversation, conversation, resolved.family, event.input, event.output);
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
return { byConversation, notices: describeUncounted(uncounted) };
|
|
1898
|
+
}
|
|
1899
|
+
function addTo(byConversation, conversation, family, input, output) {
|
|
1900
|
+
const families = byConversation.get(conversation) ?? {};
|
|
1901
|
+
const totals = families[family] ?? { input: 0, output: 0 };
|
|
1902
|
+
totals.input += input;
|
|
1903
|
+
totals.output += output;
|
|
1904
|
+
families[family] = totals;
|
|
1905
|
+
byConversation.set(conversation, families);
|
|
1906
|
+
}
|
|
1907
|
+
function conversationOf(file, root) {
|
|
1908
|
+
return path6.relative(root, file).replace(/\.jsonl$/, "");
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// src/tokens/harnesses/registry.ts
|
|
1912
|
+
var HARNESSES = {
|
|
1913
|
+
"claude-code": claudeCode,
|
|
1914
|
+
"codex-cli": codexCli,
|
|
1915
|
+
"gemini-cli": geminiCli,
|
|
1916
|
+
pi
|
|
1917
|
+
};
|
|
1918
|
+
var HARNESS_KEYS = Object.keys(HARNESSES);
|
|
1919
|
+
|
|
1920
|
+
// src/stable/prefs.ts
|
|
1921
|
+
async function loadPrefs() {
|
|
1922
|
+
let raw;
|
|
1923
|
+
try {
|
|
1924
|
+
raw = await fs7.readFile(prefsFile(), "utf8");
|
|
1925
|
+
} catch {
|
|
1926
|
+
return {};
|
|
1927
|
+
}
|
|
1928
|
+
let parsed;
|
|
1929
|
+
try {
|
|
1930
|
+
parsed = JSON.parse(raw);
|
|
1931
|
+
} catch {
|
|
1932
|
+
return {};
|
|
1933
|
+
}
|
|
1934
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
1935
|
+
const obj = parsed;
|
|
1936
|
+
const id = obj.default_stable_horse_id;
|
|
1937
|
+
const harnesses = readHarnessChoices(obj);
|
|
1938
|
+
return {
|
|
1939
|
+
...typeof id === "string" && id !== "" ? { default_stable_horse_id: id } : {},
|
|
1940
|
+
...Object.keys(harnesses).length > 0 ? { harnesses } : {}
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
function readHarnessChoices(obj) {
|
|
1944
|
+
const raw = obj.harnesses;
|
|
1945
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
1946
|
+
const out = {};
|
|
1947
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1948
|
+
if (typeof value === "boolean" && HARNESS_KEYS.includes(key)) {
|
|
1949
|
+
out[key] = value;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
return out;
|
|
1953
|
+
}
|
|
1954
|
+
function isHarnessEnabled(prefs, key) {
|
|
1955
|
+
return prefs.harnesses?.[key] ?? HARNESSES[key].enabledByDefault;
|
|
1956
|
+
}
|
|
1957
|
+
function enabledHarnesses(prefs) {
|
|
1958
|
+
return HARNESS_KEYS.filter((key) => isHarnessEnabled(prefs, key));
|
|
1959
|
+
}
|
|
1960
|
+
async function setHarnessEnabled(key, enabled) {
|
|
1961
|
+
const raw = await loadRaw();
|
|
1962
|
+
const stored = typeof raw.harnesses === "object" && raw.harnesses !== null && !Array.isArray(raw.harnesses) ? raw.harnesses : {};
|
|
1963
|
+
await savePrefs({ harnesses: { ...stored, [key]: enabled } });
|
|
1964
|
+
}
|
|
1965
|
+
async function savePrefs(patch) {
|
|
1966
|
+
const merged = { ...await loadRaw(), ...patch };
|
|
1967
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
1968
|
+
if (v === void 0) delete merged[k];
|
|
1969
|
+
}
|
|
1970
|
+
await fs7.mkdir(homeDir(), { recursive: true });
|
|
1971
|
+
await fs7.writeFile(prefsFile(), JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
1972
|
+
}
|
|
1973
|
+
async function loadRaw() {
|
|
1974
|
+
try {
|
|
1975
|
+
const parsed = JSON.parse(await fs7.readFile(prefsFile(), "utf8"));
|
|
1976
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
1977
|
+
return parsed;
|
|
1978
|
+
} catch {
|
|
1979
|
+
return {};
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
async function setDefaultHorse(stableHorseId) {
|
|
1983
|
+
await savePrefs({ default_stable_horse_id: stableHorseId });
|
|
1984
|
+
}
|
|
1985
|
+
async function clearDefaultHorse() {
|
|
1986
|
+
await savePrefs({ default_stable_horse_id: void 0 });
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// src/stable/resolve-horse.ts
|
|
1990
|
+
async function resolveHorse(horses, opts = {}) {
|
|
1991
|
+
if (horses.length === 0) return { kind: "empty" };
|
|
1992
|
+
if (opts.name !== void 0) {
|
|
1993
|
+
const found = horses.find((h) => h.name === opts.name);
|
|
1994
|
+
return found ? { kind: "resolved", horse: found, via: "flag" } : { kind: "not_found", name: opts.name };
|
|
1995
|
+
}
|
|
1996
|
+
if (opts.pick) return interactive() ? { kind: "pick" } : { kind: "no_tty" };
|
|
1997
|
+
if (opts.autoSelect !== false) {
|
|
1998
|
+
const { default_stable_horse_id } = await loadPrefs();
|
|
1999
|
+
if (default_stable_horse_id !== void 0) {
|
|
2000
|
+
const found = horses.find((h) => h.stable_horse_id === default_stable_horse_id);
|
|
2001
|
+
if (found) return { kind: "resolved", horse: found, via: "default" };
|
|
2002
|
+
}
|
|
2003
|
+
if (horses.length === 1) return { kind: "resolved", horse: horses[0], via: "only" };
|
|
2004
|
+
}
|
|
2005
|
+
return interactive() ? { kind: "pick" } : { kind: "no_tty" };
|
|
2006
|
+
}
|
|
2007
|
+
function interactive() {
|
|
2008
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2009
|
+
}
|
|
2010
|
+
function describeHorse(horse) {
|
|
2011
|
+
return `${horse.name} [Lvl. ${levelFromXp(horse.xp)}]`;
|
|
2012
|
+
}
|
|
2013
|
+
function noticeFor(choice) {
|
|
2014
|
+
if (choice.via === "flag") return null;
|
|
2015
|
+
const which = choice.via === "default" ? "your default horse" : "your only horse";
|
|
2016
|
+
return `Using ${which}: ${describeHorse(choice.horse)}
|
|
2017
|
+
(--horse <name> to pick another, --pick to choose)`;
|
|
2018
|
+
}
|
|
2019
|
+
function noTtyMessage(command) {
|
|
2020
|
+
return [
|
|
2021
|
+
`\`${command}\` needs to know which horse, and this is not an interactive terminal.`,
|
|
2022
|
+
"Pass --horse <name>, or set a default with `token-derby stable default <name>`."
|
|
2023
|
+
].join("\n");
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// src/commands/stable-edit.ts
|
|
2027
|
+
async function stableEditCommand(name) {
|
|
2028
|
+
if (!interactive()) {
|
|
2029
|
+
console.error("`token-derby stable edit` needs an interactive terminal.");
|
|
2030
|
+
return 1;
|
|
2031
|
+
}
|
|
2032
|
+
const horses = await fetchStable();
|
|
2033
|
+
if (!horses) return 1;
|
|
2034
|
+
const existing = await pickHorseToEdit(horses, name);
|
|
2035
|
+
if (existing === "not_found") {
|
|
2036
|
+
console.error(`No horse named "${name}" in your stable.`);
|
|
2037
|
+
return 1;
|
|
2038
|
+
}
|
|
2039
|
+
if (existing === "empty") {
|
|
2040
|
+
console.log("No horses in your stable. Run `token-derby stable create` to make one.");
|
|
2041
|
+
return 0;
|
|
2042
|
+
}
|
|
2043
|
+
if (existing === "cancelled") {
|
|
2044
|
+
console.log("Cancelled.");
|
|
2045
|
+
return 1;
|
|
2046
|
+
}
|
|
2047
|
+
if (existing === "no_tty") {
|
|
2048
|
+
console.error(noTtyMessage("token-derby stable edit"));
|
|
2049
|
+
return 1;
|
|
2050
|
+
}
|
|
2051
|
+
const initialEquipped = existing.equipped_hat ?? null;
|
|
2052
|
+
let exitCode = 0;
|
|
2053
|
+
const app = render3(
|
|
2054
|
+
React4.createElement(HorseCreator, {
|
|
2055
|
+
initialColors: existing.colors,
|
|
2056
|
+
initialName: existing.name,
|
|
2057
|
+
lockName: true,
|
|
2058
|
+
initialLevel: levelFromXp(existing.xp),
|
|
2059
|
+
hats: existing.hats,
|
|
2060
|
+
initialEquipped,
|
|
2061
|
+
onSubmit: async (_name, colors, hatChoice) => {
|
|
2062
|
+
const colorsChanged = !sameColors(colors, existing.colors);
|
|
2063
|
+
const hatChanged = hatChoice !== void 0 && hatChoice !== initialEquipped;
|
|
2064
|
+
try {
|
|
2065
|
+
if (colorsChanged) await updateStableHorse(existing.stable_horse_id, { colors });
|
|
2066
|
+
if (hatChanged) await equipHat(existing.stable_horse_id, { hat_index: hatChoice });
|
|
2067
|
+
app.unmount();
|
|
2068
|
+
if (!colorsChanged && !hatChanged) {
|
|
2069
|
+
console.log(`No changes for "${existing.name}".`);
|
|
2070
|
+
} else {
|
|
2071
|
+
const parts = [];
|
|
2072
|
+
if (colorsChanged) parts.push("colors");
|
|
2073
|
+
if (hatChanged) parts.push(hatChoice === null ? "hat unequipped" : "hat equipped");
|
|
2074
|
+
console.log(`\u2713 Updated "${existing.name}" (${parts.join(", ")}).`);
|
|
2075
|
+
}
|
|
2076
|
+
} catch (e) {
|
|
2077
|
+
app.unmount();
|
|
2078
|
+
if (e instanceof ApiError) {
|
|
2079
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
2080
|
+
exitCode = 1;
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
throw e;
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
onCancel: () => {
|
|
2087
|
+
app.unmount();
|
|
2088
|
+
console.log("Cancelled.");
|
|
2089
|
+
exitCode = 1;
|
|
2090
|
+
}
|
|
2091
|
+
})
|
|
2092
|
+
);
|
|
2093
|
+
await app.waitUntilExit();
|
|
2094
|
+
return exitCode;
|
|
2095
|
+
}
|
|
2096
|
+
function sameColors(a, b) {
|
|
2097
|
+
return a.body === b.body && a.mane === b.mane && a.tail === b.tail && a.saddle === b.saddle;
|
|
2098
|
+
}
|
|
2099
|
+
async function fetchStable() {
|
|
2100
|
+
try {
|
|
2101
|
+
const resp = await listStable();
|
|
2102
|
+
return resp.horses;
|
|
2103
|
+
} catch (e) {
|
|
2104
|
+
if (e instanceof ApiError) {
|
|
2105
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
throw e;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
async function pickHorseToEdit(horses, name) {
|
|
2112
|
+
const choice = await resolveHorse(horses, { name });
|
|
2113
|
+
if (choice.kind === "empty") return "empty";
|
|
2114
|
+
if (choice.kind === "not_found") return "not_found";
|
|
2115
|
+
if (choice.kind === "no_tty") return "no_tty";
|
|
2116
|
+
if (choice.kind === "resolved") {
|
|
2117
|
+
const notice = noticeFor(choice);
|
|
2118
|
+
if (notice) console.log(notice);
|
|
2119
|
+
return choice.horse;
|
|
2120
|
+
}
|
|
2121
|
+
const picked = await new Promise((resolve) => {
|
|
2122
|
+
const app = render3(
|
|
2123
|
+
React4.createElement(HorsePicker, {
|
|
2124
|
+
horses,
|
|
2125
|
+
onPick: (h) => {
|
|
2126
|
+
app.unmount();
|
|
2127
|
+
resolve(h);
|
|
2128
|
+
},
|
|
2129
|
+
onCancel: () => {
|
|
2130
|
+
app.unmount();
|
|
2131
|
+
resolve(null);
|
|
2132
|
+
}
|
|
2133
|
+
})
|
|
2134
|
+
);
|
|
2135
|
+
});
|
|
2136
|
+
return picked ?? "cancelled";
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// src/commands/create.ts
|
|
2140
|
+
import * as readline2 from "readline/promises";
|
|
2141
|
+
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
2142
|
+
var DEFAULT_TZ = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
2143
|
+
async function createRaceCommand(organisationName) {
|
|
2144
|
+
const rl = readline2.createInterface({ input: stdin2, output: stdout2 });
|
|
2145
|
+
try {
|
|
2146
|
+
const name = (await rl.question("Race name: ")).trim();
|
|
2147
|
+
if (!name) {
|
|
2148
|
+
console.error("Name required.");
|
|
2149
|
+
return 1;
|
|
2150
|
+
}
|
|
2151
|
+
const startRaw = (await rl.question("Start time (ISO 8601, blank = now): ")).trim();
|
|
2152
|
+
const start = startRaw ? startRaw : (/* @__PURE__ */ new Date()).toISOString();
|
|
2153
|
+
if (!isIso(start)) {
|
|
2154
|
+
console.error("Invalid start time.");
|
|
2155
|
+
return 1;
|
|
2156
|
+
}
|
|
2157
|
+
const durationRaw = (await rl.question("Race duration (hours): ")).trim();
|
|
2158
|
+
const durationHours = parseFloat(durationRaw);
|
|
2159
|
+
if (!Number.isFinite(durationHours) || durationHours <= 0) {
|
|
2160
|
+
console.error("Duration must be a positive number of hours.");
|
|
2161
|
+
return 1;
|
|
2162
|
+
}
|
|
2163
|
+
const end = new Date(new Date(start).getTime() + durationHours * 36e5).toISOString();
|
|
2164
|
+
const tz = (await rl.question(`Time zone [${DEFAULT_TZ}]: `)).trim() || DEFAULT_TZ;
|
|
2165
|
+
const maxRaw = (await rl.question("Max participants [30]: ")).trim();
|
|
2166
|
+
const max = maxRaw ? parseInt(maxRaw, 10) : void 0;
|
|
2167
|
+
if (max !== void 0 && (!Number.isFinite(max) || max < 1)) {
|
|
2168
|
+
console.error("Max participants must be a positive number.");
|
|
2169
|
+
return 1;
|
|
2170
|
+
}
|
|
2171
|
+
let org = organisationName;
|
|
2172
|
+
if (org === void 0) {
|
|
2173
|
+
const raw = (await rl.question("Organisation (blank for none): ")).trim();
|
|
2174
|
+
if (raw) org = raw;
|
|
2175
|
+
}
|
|
2176
|
+
if (org !== void 0 && !ORG_NAME_PATTERN.test(org)) {
|
|
2177
|
+
console.error("Organisation name must be 1\u201312 alphanumeric characters.");
|
|
2178
|
+
return 1;
|
|
2179
|
+
}
|
|
2180
|
+
const modifiers = [];
|
|
2181
|
+
if (!org) {
|
|
2182
|
+
for (const id of MODIFIER_IDS) {
|
|
2183
|
+
const m = MODIFIERS[id];
|
|
2184
|
+
const raw = (await rl.question(`${m.label} \u2014 ${m.description} [y/N]: `)).trim().toLowerCase();
|
|
2185
|
+
if (raw === "y" || raw === "yes") modifiers.push(id);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
const resp = await createRace({
|
|
2189
|
+
name,
|
|
2190
|
+
start_time: start,
|
|
2191
|
+
end_time: end,
|
|
2192
|
+
tz,
|
|
2193
|
+
...max !== void 0 ? { max_participants: max } : {},
|
|
2194
|
+
...org ? { organisation_name: org } : {},
|
|
2195
|
+
...modifiers.length > 0 ? { modifiers } : {}
|
|
2196
|
+
});
|
|
2197
|
+
console.log("");
|
|
2198
|
+
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");
|
|
2199
|
+
console.log(` \u2551 JOIN CODE: ${resp.join_code.padEnd(23)}\u2551`);
|
|
2200
|
+
console.log(" \u255A\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\u255D");
|
|
2201
|
+
console.log("");
|
|
2202
|
+
console.log(` Admin code: ${resp.admin_code}`);
|
|
2203
|
+
console.log(" \u26A0 Save the admin code \u2014 you need it to end the race early.");
|
|
2204
|
+
console.log("");
|
|
2205
|
+
if (org) {
|
|
2206
|
+
console.log(` Restricted to organisation: ${org}`);
|
|
2207
|
+
}
|
|
2208
|
+
for (const id of modifiers) {
|
|
2209
|
+
console.log(` ${MODIFIERS[id].label} on \u2014 ${MODIFIERS[id].description}`);
|
|
2210
|
+
}
|
|
2211
|
+
console.log(` Share with participants: token-derby join ${resp.join_code}`);
|
|
2212
|
+
return 0;
|
|
2213
|
+
} catch (e) {
|
|
2214
|
+
if (e instanceof ApiError) {
|
|
2215
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
2216
|
+
return 1;
|
|
2217
|
+
}
|
|
2218
|
+
throw e;
|
|
2219
|
+
} finally {
|
|
2220
|
+
rl.close();
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
function isIso(s) {
|
|
2224
|
+
if (!s) return false;
|
|
2225
|
+
const d = new Date(s);
|
|
2226
|
+
return !Number.isNaN(d.getTime());
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// src/commands/join.ts
|
|
2230
|
+
import React5 from "react";
|
|
2231
|
+
import { render as render4 } from "ink";
|
|
2232
|
+
|
|
2233
|
+
// src/args.ts
|
|
2234
|
+
function parseFlag(args, flag) {
|
|
2235
|
+
for (let i = 0; i < args.length; i++) {
|
|
2236
|
+
if (args[i] === flag) return args[i + 1];
|
|
2237
|
+
const eq = `${flag}=`;
|
|
2238
|
+
if (args[i]?.startsWith(eq)) return args[i].slice(eq.length);
|
|
2239
|
+
}
|
|
2240
|
+
return void 0;
|
|
2241
|
+
}
|
|
2242
|
+
function hasFlag(args, flag) {
|
|
2243
|
+
return args.some((a) => a === flag || a.startsWith(`${flag}=`));
|
|
1612
2244
|
}
|
|
1613
2245
|
|
|
1614
2246
|
// src/stable/active-race.ts
|
|
1615
|
-
import * as
|
|
1616
|
-
import * as
|
|
2247
|
+
import * as fs8 from "fs/promises";
|
|
2248
|
+
import * as path7 from "path";
|
|
1617
2249
|
async function saveActiveRace(active) {
|
|
1618
|
-
await
|
|
1619
|
-
await
|
|
2250
|
+
await fs8.mkdir(activeRacesDir(), { recursive: true });
|
|
2251
|
+
await fs8.writeFile(
|
|
1620
2252
|
activeRaceFile(active.join_code),
|
|
1621
2253
|
JSON.stringify(active, null, 2) + "\n",
|
|
1622
2254
|
"utf8"
|
|
@@ -1624,29 +2256,30 @@ async function saveActiveRace(active) {
|
|
|
1624
2256
|
}
|
|
1625
2257
|
|
|
1626
2258
|
// src/runtime/run-race.tsx
|
|
1627
|
-
import { useEffect as useEffect2, useRef, useState as
|
|
1628
|
-
import { Box as
|
|
2259
|
+
import { useEffect as useEffect2, useRef, useState as useState4 } from "react";
|
|
2260
|
+
import { Box as Box7, Text as Text7, useApp } from "ink";
|
|
1629
2261
|
|
|
1630
2262
|
// src/ui/StatusScreen.tsx
|
|
1631
|
-
import { Box as
|
|
1632
|
-
import { Fragment as Fragment2, jsx as
|
|
1633
|
-
var MODEL_LABELS = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
|
|
2263
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
2264
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1634
2265
|
function ModelList(props) {
|
|
1635
|
-
const
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
MODEL_KEYS.map((m, i) => /* @__PURE__ */ jsxs5(Text7, { children: [
|
|
2266
|
+
const off = new Set(props.disabled ?? []);
|
|
2267
|
+
return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text6, { children: [
|
|
2268
|
+
"Counting: ",
|
|
2269
|
+
HARNESS_KEYS.map((key, i) => /* @__PURE__ */ jsxs4(Text6, { children: [
|
|
1640
2270
|
i > 0 ? " \xB7 " : "",
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
2271
|
+
off.has(key) ? /* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
|
|
2272
|
+
HARNESSES[key].label,
|
|
2273
|
+
" (off)"
|
|
2274
|
+
] }) : HARNESSES[key].label
|
|
2275
|
+
] }, key)),
|
|
2276
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " (all count the same)" })
|
|
1644
2277
|
] }) });
|
|
1645
2278
|
}
|
|
1646
2279
|
function StatusScreen(props) {
|
|
1647
|
-
const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, stallReason,
|
|
2280
|
+
const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, stallReason, sourcesSilent, degraded, notices, disabledHarnesses } = props;
|
|
1648
2281
|
if (!race) {
|
|
1649
|
-
return /* @__PURE__ */
|
|
2282
|
+
return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: /* @__PURE__ */ jsx6(Text6, { children: "Joining race\u2026" }) });
|
|
1650
2283
|
}
|
|
1651
2284
|
const own = race.horses.find((h) => h.horse_id === ownHorseId);
|
|
1652
2285
|
const leader = race.horses[0];
|
|
@@ -1672,36 +2305,36 @@ function StatusScreen(props) {
|
|
|
1672
2305
|
},
|
|
1673
2306
|
{
|
|
1674
2307
|
label: "Last heartbeat:",
|
|
1675
|
-
value: /* @__PURE__ */
|
|
2308
|
+
value: /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
1676
2309
|
lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
|
|
1677
2310
|
" ",
|
|
1678
|
-
/* @__PURE__ */
|
|
2311
|
+
/* @__PURE__ */ jsx6(Text6, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
|
|
1679
2312
|
] })
|
|
1680
2313
|
}
|
|
1681
2314
|
];
|
|
1682
|
-
return /* @__PURE__ */
|
|
1683
|
-
/* @__PURE__ */
|
|
2315
|
+
return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
2316
|
+
/* @__PURE__ */ jsxs4(Text6, { children: [
|
|
1684
2317
|
"\u{1F3C7} TOKEN DERBY \u2500\u2500\u2500 ",
|
|
1685
|
-
/* @__PURE__ */
|
|
2318
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, children: race.name }),
|
|
1686
2319
|
" \u2500\u2500\u2500 status: ",
|
|
1687
|
-
/* @__PURE__ */
|
|
2320
|
+
/* @__PURE__ */ jsx6(Text6, { color: statusColor(race.status), children: race.status })
|
|
1688
2321
|
] }),
|
|
1689
|
-
/* @__PURE__ */
|
|
1690
|
-
/* @__PURE__ */
|
|
1691
|
-
/* @__PURE__ */
|
|
1692
|
-
/* @__PURE__ */
|
|
2322
|
+
/* @__PURE__ */ jsxs4(Box6, { marginTop: 1, flexDirection: "row", children: [
|
|
2323
|
+
/* @__PURE__ */ jsx6(HorseSprite, { sprite: MINI_SPRITE, colors: ownColors }),
|
|
2324
|
+
/* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
|
|
2325
|
+
/* @__PURE__ */ jsxs4(Text6, { children: [
|
|
1693
2326
|
" ",
|
|
1694
2327
|
ownHorseName,
|
|
1695
2328
|
" ",
|
|
1696
|
-
/* @__PURE__ */
|
|
2329
|
+
/* @__PURE__ */ jsxs4(Text6, { color: "cyan", children: [
|
|
1697
2330
|
"[Lvl. ",
|
|
1698
2331
|
lvl.level,
|
|
1699
2332
|
"]"
|
|
1700
2333
|
] })
|
|
1701
2334
|
] }),
|
|
1702
|
-
/* @__PURE__ */
|
|
2335
|
+
/* @__PURE__ */ jsxs4(Text6, { children: [
|
|
1703
2336
|
" ",
|
|
1704
|
-
/* @__PURE__ */
|
|
2337
|
+
/* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
|
|
1705
2338
|
"(",
|
|
1706
2339
|
ownUserName,
|
|
1707
2340
|
")"
|
|
@@ -1709,30 +2342,39 @@ function StatusScreen(props) {
|
|
|
1709
2342
|
] })
|
|
1710
2343
|
] })
|
|
1711
2344
|
] }),
|
|
1712
|
-
/* @__PURE__ */
|
|
1713
|
-
/* @__PURE__ */
|
|
1714
|
-
stalled && /* @__PURE__ */
|
|
2345
|
+
/* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", marginTop: 1, children: [
|
|
2346
|
+
/* @__PURE__ */ jsx6(StatLines, { rows }),
|
|
2347
|
+
stalled && /* @__PURE__ */ jsxs4(Text6, { color: "yellow", children: [
|
|
1715
2348
|
"\u26A0 ",
|
|
1716
2349
|
stallReason ?? "Can't read token usage",
|
|
1717
2350
|
". Your race continues."
|
|
1718
2351
|
] }),
|
|
1719
|
-
!stalled &&
|
|
1720
|
-
"\u26A0
|
|
1721
|
-
|
|
1722
|
-
"
|
|
1723
|
-
|
|
1724
|
-
"
|
|
1725
|
-
|
|
1726
|
-
"
|
|
2352
|
+
!stalled && (degraded?.length ?? 0) > 0 && degraded.map((d) => /* @__PURE__ */ jsxs4(Text6, { color: "yellow", children: [
|
|
2353
|
+
"\u26A0 ",
|
|
2354
|
+
d.label,
|
|
2355
|
+
" not counted this beat \u2014 ",
|
|
2356
|
+
d.message,
|
|
2357
|
+
". Your other sources still count, and ",
|
|
2358
|
+
d.label,
|
|
2359
|
+
" catches up once it can be read."
|
|
2360
|
+
] }, d.harness)),
|
|
2361
|
+
!stalled && (notices?.length ?? 0) > 0 && notices.map((n) => /* @__PURE__ */ jsxs4(Text6, { color: "yellow", children: [
|
|
2362
|
+
"\u26A0 ",
|
|
2363
|
+
n
|
|
2364
|
+
] }, n)),
|
|
2365
|
+
!stalled && (degraded?.length ?? 0) === 0 && sourcesSilent && /* @__PURE__ */ jsxs4(Text6, { color: "yellow", children: [
|
|
2366
|
+
"\u26A0 No transcripts from any coding agent in ",
|
|
2367
|
+
SILENT_THRESHOLD,
|
|
2368
|
+
" beats. Your race continues, but your horse cannot move until they can be read."
|
|
1727
2369
|
] })
|
|
1728
2370
|
] }),
|
|
1729
|
-
|
|
1730
|
-
/* @__PURE__ */
|
|
2371
|
+
/* @__PURE__ */ jsx6(ModelList, { disabled: disabledHarnesses }),
|
|
2372
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
|
|
1731
2373
|
] });
|
|
1732
2374
|
}
|
|
1733
2375
|
function StatLines(props) {
|
|
1734
2376
|
const width = Math.max(...props.rows.map((r) => r.label.length)) + 1;
|
|
1735
|
-
return /* @__PURE__ */
|
|
2377
|
+
return /* @__PURE__ */ jsx6(Fragment2, { children: props.rows.map((r) => /* @__PURE__ */ jsxs4(Text6, { children: [
|
|
1736
2378
|
r.label.padEnd(width),
|
|
1737
2379
|
r.value
|
|
1738
2380
|
] }, r.label)) });
|
|
@@ -1754,17 +2396,17 @@ function bar(pct, width) {
|
|
|
1754
2396
|
return "\u2593".repeat(filled) + "\u2591".repeat(width - filled);
|
|
1755
2397
|
}
|
|
1756
2398
|
function staminaLine(own, race) {
|
|
1757
|
-
const
|
|
2399
|
+
const stamina2 = staminaOf(own ?? {});
|
|
1758
2400
|
const cfg = resolveStaminaConfig(race);
|
|
1759
2401
|
const floor = cfg.taper_floor;
|
|
1760
|
-
const band =
|
|
2402
|
+
const band = stamina2 > 50 ? "green" : stamina2 >= floor ? "amber" : "red";
|
|
1761
2403
|
const color = band === "green" ? "green" : band === "amber" ? "yellow" : "red";
|
|
1762
|
-
const pct = Math.max(0, Math.min(1,
|
|
1763
|
-
const multiplier = band === "red" ? cfg.tired_multiplier + (1 - cfg.tired_multiplier) * (
|
|
2404
|
+
const pct = Math.max(0, Math.min(1, stamina2 / 100));
|
|
2405
|
+
const multiplier = band === "red" ? cfg.tired_multiplier + (1 - cfg.tired_multiplier) * (stamina2 / floor) : null;
|
|
1764
2406
|
return {
|
|
1765
2407
|
label: "Stamina:",
|
|
1766
|
-
value: /* @__PURE__ */
|
|
1767
|
-
`${Math.round(
|
|
2408
|
+
value: /* @__PURE__ */ jsxs4(Text6, { color, children: [
|
|
2409
|
+
`${Math.round(stamina2)}% ${bar(pct, 20)}`,
|
|
1768
2410
|
multiplier !== null ? ` \xD7${multiplier.toFixed(2)}` : ""
|
|
1769
2411
|
] })
|
|
1770
2412
|
};
|
|
@@ -1838,30 +2480,13 @@ function runHeartbeatLoop(opts) {
|
|
|
1838
2480
|
schedule(0);
|
|
1839
2481
|
}
|
|
1840
2482
|
|
|
1841
|
-
// src/tokens/
|
|
1842
|
-
import * as
|
|
1843
|
-
import * as path6 from "path";
|
|
1844
|
-
|
|
1845
|
-
// src/tokens/pool.ts
|
|
1846
|
-
var SCAN_CONCURRENCY = 12;
|
|
1847
|
-
async function mapWithConcurrency(items, limit, fn) {
|
|
1848
|
-
const out = new Array(items.length);
|
|
1849
|
-
let next = 0;
|
|
1850
|
-
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1851
|
-
while (true) {
|
|
1852
|
-
const i = next++;
|
|
1853
|
-
if (i >= items.length) return;
|
|
1854
|
-
out[i] = await fn(items[i], i);
|
|
1855
|
-
}
|
|
1856
|
-
});
|
|
1857
|
-
await Promise.all(workers);
|
|
1858
|
-
return out;
|
|
1859
|
-
}
|
|
2483
|
+
// src/tokens/harnesses/engine.ts
|
|
2484
|
+
import * as fs10 from "fs/promises";
|
|
1860
2485
|
|
|
1861
2486
|
// src/tokens/scan-cache.ts
|
|
1862
|
-
import * as
|
|
1863
|
-
import * as
|
|
1864
|
-
var CACHE_VERSION =
|
|
2487
|
+
import * as fs9 from "fs/promises";
|
|
2488
|
+
import * as path8 from "path";
|
|
2489
|
+
var CACHE_VERSION = 2;
|
|
1865
2490
|
function isEntry(v) {
|
|
1866
2491
|
const e = v;
|
|
1867
2492
|
return !!e && typeof e.mtimeMs === "number" && typeof e.size === "number" && typeof e.offset === "number";
|
|
@@ -1897,7 +2522,7 @@ var ScanCache = class _ScanCache {
|
|
|
1897
2522
|
* re-reading that line once the writer completes it.
|
|
1898
2523
|
*/
|
|
1899
2524
|
async readIncremental(file, fold) {
|
|
1900
|
-
const st = await
|
|
2525
|
+
const st = await fs9.stat(file);
|
|
1901
2526
|
const prev = this.entries.get(file);
|
|
1902
2527
|
this.touched.add(file);
|
|
1903
2528
|
if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
|
|
@@ -1914,11 +2539,11 @@ var ScanCache = class _ScanCache {
|
|
|
1914
2539
|
* chats). Gated on mtime+size, recomputed in full whenever either moves.
|
|
1915
2540
|
*/
|
|
1916
2541
|
async readWhenChanged(file, compute) {
|
|
1917
|
-
const st = await
|
|
2542
|
+
const st = await fs9.stat(file);
|
|
1918
2543
|
const prev = this.entries.get(file);
|
|
1919
2544
|
this.touched.add(file);
|
|
1920
2545
|
if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
|
|
1921
|
-
const value = await compute(await
|
|
2546
|
+
const value = await compute(await fs9.readFile(file, "utf8"));
|
|
1922
2547
|
this.entries.set(file, { mtimeMs: st.mtimeMs, size: st.size, offset: st.size, value });
|
|
1923
2548
|
return value;
|
|
1924
2549
|
}
|
|
@@ -1930,20 +2555,20 @@ var ScanCache = class _ScanCache {
|
|
|
1930
2555
|
const target = cacheFile(this.source);
|
|
1931
2556
|
const tmp = `${target}.tmp`;
|
|
1932
2557
|
try {
|
|
1933
|
-
await
|
|
1934
|
-
await
|
|
1935
|
-
await
|
|
2558
|
+
await fs9.mkdir(path8.dirname(target), { recursive: true });
|
|
2559
|
+
await fs9.writeFile(tmp, JSON.stringify({ version: CACHE_VERSION, files: Object.fromEntries(this.entries) }));
|
|
2560
|
+
await fs9.rename(tmp, target);
|
|
1936
2561
|
} catch {
|
|
1937
2562
|
}
|
|
1938
2563
|
}
|
|
1939
2564
|
};
|
|
1940
2565
|
function cacheFile(source) {
|
|
1941
|
-
return
|
|
2566
|
+
return path8.join(homeDir(), "scan-cache", `${source}.json`);
|
|
1942
2567
|
}
|
|
1943
2568
|
async function loadEntries(source) {
|
|
1944
2569
|
let parsed;
|
|
1945
2570
|
try {
|
|
1946
|
-
parsed = JSON.parse(await
|
|
2571
|
+
parsed = JSON.parse(await fs9.readFile(cacheFile(source), "utf8"));
|
|
1947
2572
|
} catch {
|
|
1948
2573
|
return /* @__PURE__ */ new Map();
|
|
1949
2574
|
}
|
|
@@ -1958,284 +2583,82 @@ async function loadEntries(source) {
|
|
|
1958
2583
|
}
|
|
1959
2584
|
async function readCompleteLines(file, start, end) {
|
|
1960
2585
|
if (end <= start) return { lines: [], tail: null, consumedTo: start };
|
|
1961
|
-
const fh = await
|
|
1962
|
-
try {
|
|
1963
|
-
const buf = Buffer.allocUnsafe(end - start);
|
|
1964
|
-
const { bytesRead } = await fh.read(buf, 0, end - start, start);
|
|
1965
|
-
const chunk = buf.subarray(0, bytesRead);
|
|
1966
|
-
const lastNl = chunk.lastIndexOf(10);
|
|
1967
|
-
if (lastNl === -1) {
|
|
1968
|
-
return { lines: [], tail: chunk.toString("utf8") || null, consumedTo: start };
|
|
1969
|
-
}
|
|
1970
|
-
const tail = chunk.subarray(lastNl + 1).toString("utf8");
|
|
1971
|
-
return {
|
|
1972
|
-
lines: chunk.subarray(0, lastNl).toString("utf8").split("\n"),
|
|
1973
|
-
tail: tail === "" ? null : tail,
|
|
1974
|
-
consumedTo: start + lastNl + 1
|
|
1975
|
-
};
|
|
1976
|
-
} finally {
|
|
1977
|
-
await fh.close();
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
// src/tokens/source-root.ts
|
|
1982
|
-
var SourceRootMissing = class extends Error {
|
|
1983
|
-
constructor(dir) {
|
|
1984
|
-
super(`No history directory at ${dir}`);
|
|
1985
|
-
this.dir = dir;
|
|
1986
|
-
this.name = "SourceRootMissing";
|
|
1987
|
-
}
|
|
1988
|
-
dir;
|
|
1989
|
-
};
|
|
1990
|
-
async function readRoot(dir, read) {
|
|
1991
|
-
try {
|
|
1992
|
-
return await read();
|
|
1993
|
-
} catch (e) {
|
|
1994
|
-
if (e?.code === "ENOENT") throw new SourceRootMissing(dir);
|
|
1995
|
-
throw e;
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
// src/tokens/transcripts.ts
|
|
2000
|
-
var MAX_PROJECT_DEPTH = 8;
|
|
2001
|
-
function conversationId(file, root) {
|
|
2002
|
-
const rel = path6.relative(root, file);
|
|
2003
|
-
const [project, session] = rel.split(path6.sep);
|
|
2004
|
-
if (project === void 0 || session === void 0) return rel.replace(/\.jsonl$/, "");
|
|
2005
|
-
return `${project}/${session.replace(/\.jsonl$/, "")}`;
|
|
2006
|
-
}
|
|
2007
|
-
async function sumTokensByConversation() {
|
|
2008
|
-
const root = claudeProjectsDir();
|
|
2009
|
-
const files = await listJsonlFiles(root);
|
|
2010
|
-
const cache = await ScanCache.open("claude");
|
|
2011
|
-
const totals = await mapWithConcurrency(files, SCAN_CONCURRENCY, (f) => cache.readIncremental(f, CLAUDE_FOLD));
|
|
2012
|
-
await cache.save();
|
|
2013
|
-
const byConv = /* @__PURE__ */ new Map();
|
|
2014
|
-
files.forEach((file, i) => {
|
|
2015
|
-
const t = totals[i];
|
|
2016
|
-
const id = conversationId(file, root);
|
|
2017
|
-
const acc = byConv.get(id) ?? { input: 0, output: 0 };
|
|
2018
|
-
acc.input += t.input;
|
|
2019
|
-
acc.output += t.output;
|
|
2020
|
-
byConv.set(id, acc);
|
|
2021
|
-
});
|
|
2022
|
-
return byConv;
|
|
2023
|
-
}
|
|
2024
|
-
async function sumTokens() {
|
|
2025
|
-
const byConv = await sumTokensByConversation();
|
|
2026
|
-
let input = 0;
|
|
2027
|
-
let output = 0;
|
|
2028
|
-
for (const t of byConv.values()) {
|
|
2029
|
-
input += t.input;
|
|
2030
|
-
output += t.output;
|
|
2031
|
-
}
|
|
2032
|
-
return { input, output };
|
|
2033
|
-
}
|
|
2034
|
-
async function listJsonlFiles(root) {
|
|
2035
|
-
const entries = await readEntries(root, true);
|
|
2036
|
-
const out = [];
|
|
2037
|
-
for (const entry of entries) {
|
|
2038
|
-
if (!await isDirectory(entry, root)) continue;
|
|
2039
|
-
await collectJsonl(path6.join(root, entry.name), MAX_PROJECT_DEPTH, out);
|
|
2040
|
-
}
|
|
2041
|
-
return out;
|
|
2042
|
-
}
|
|
2043
|
-
async function collectJsonl(dir, depth, out) {
|
|
2044
|
-
if (depth <= 0) return;
|
|
2045
|
-
for (const entry of await readEntries(dir, false)) {
|
|
2046
|
-
if (entry.name.endsWith(".jsonl")) {
|
|
2047
|
-
out.push(path6.join(dir, entry.name));
|
|
2048
|
-
} else if (depth > 1 && await isDirectory(entry, dir)) {
|
|
2049
|
-
await collectJsonl(path6.join(dir, entry.name), depth - 1, out);
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
async function readEntries(dir, failLoud) {
|
|
2054
|
-
if (failLoud) return readRoot(dir, () => fs5.readdir(dir, { withFileTypes: true }));
|
|
2055
|
-
return fs5.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2056
|
-
}
|
|
2057
|
-
async function isDirectory(entry, parent) {
|
|
2058
|
-
if (entry.isDirectory()) return true;
|
|
2059
|
-
if (!entry.isSymbolicLink()) return false;
|
|
2060
|
-
return fs5.stat(path6.join(parent, entry.name)).then((st) => st.isDirectory()).catch(() => false);
|
|
2061
|
-
}
|
|
2062
|
-
function addNum(value) {
|
|
2063
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2064
|
-
}
|
|
2065
|
-
var CLAUDE_FOLD = {
|
|
2066
|
-
empty: () => ({ input: 0, output: 0 }),
|
|
2067
|
-
append: (acc, lines) => {
|
|
2068
|
-
let { input, output } = acc;
|
|
2069
|
-
for (const line of lines) {
|
|
2070
|
-
if (!line.trim()) continue;
|
|
2071
|
-
let parsed;
|
|
2072
|
-
try {
|
|
2073
|
-
parsed = JSON.parse(line);
|
|
2074
|
-
} catch {
|
|
2075
|
-
continue;
|
|
2076
|
-
}
|
|
2077
|
-
const usage = parsed?.message?.usage;
|
|
2078
|
-
if (!usage) continue;
|
|
2079
|
-
input += addNum(usage.input_tokens) + addNum(usage.cache_creation_input_tokens);
|
|
2080
|
-
output += addNum(usage.output_tokens);
|
|
2081
|
-
}
|
|
2082
|
-
return { input, output };
|
|
2083
|
-
}
|
|
2084
|
-
};
|
|
2085
|
-
|
|
2086
|
-
// src/tokens/codex.ts
|
|
2087
|
-
import * as fs6 from "fs/promises";
|
|
2088
|
-
import * as path7 from "path";
|
|
2089
|
-
function num(v) {
|
|
2090
|
-
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
2091
|
-
}
|
|
2092
|
-
async function sumCodexByConversation() {
|
|
2093
|
-
const root = codexSessionsDir();
|
|
2094
|
-
await readRoot(root, () => fs6.stat(root));
|
|
2095
|
-
const files = await listCodexRollouts(root);
|
|
2096
|
-
const cache = await ScanCache.open("codex");
|
|
2097
|
-
const totals = await mapWithConcurrency(
|
|
2098
|
-
files,
|
|
2099
|
-
SCAN_CONCURRENCY,
|
|
2100
|
-
(f) => cache.readIncremental(f, CODEX_FOLD).catch(() => ({ input: 0, output: 0 }))
|
|
2101
|
-
);
|
|
2102
|
-
await cache.save();
|
|
2103
|
-
const byConv = /* @__PURE__ */ new Map();
|
|
2104
|
-
files.forEach((file, i) => byConv.set(file, totals[i]));
|
|
2105
|
-
return byConv;
|
|
2106
|
-
}
|
|
2107
|
-
async function sumCodexTokens() {
|
|
2108
|
-
const byConv = await sumCodexByConversation();
|
|
2109
|
-
let input = 0;
|
|
2110
|
-
let output = 0;
|
|
2111
|
-
for (const t of byConv.values()) {
|
|
2112
|
-
input += t.input;
|
|
2113
|
-
output += t.output;
|
|
2114
|
-
}
|
|
2115
|
-
return { input, output };
|
|
2116
|
-
}
|
|
2117
|
-
async function listCodexRollouts(root) {
|
|
2118
|
-
return [
|
|
2119
|
-
...await collectRollouts(path7.join(root, "sessions")),
|
|
2120
|
-
...await collectRollouts(path7.join(root, "archived_sessions"))
|
|
2121
|
-
];
|
|
2122
|
-
}
|
|
2123
|
-
async function collectRollouts(dir) {
|
|
2124
|
-
let entries;
|
|
2125
|
-
try {
|
|
2126
|
-
entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
2127
|
-
} catch (e) {
|
|
2128
|
-
if (e?.code === "ENOENT") return [];
|
|
2129
|
-
throw e;
|
|
2130
|
-
}
|
|
2131
|
-
const out = [];
|
|
2132
|
-
for (const entry of entries) {
|
|
2133
|
-
const full = path7.join(dir, entry.name);
|
|
2134
|
-
if (entry.isDirectory()) out.push(...await collectRollouts(full));
|
|
2135
|
-
else if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) out.push(full);
|
|
2136
|
-
}
|
|
2137
|
-
return out;
|
|
2138
|
-
}
|
|
2139
|
-
var CODEX_FOLD = {
|
|
2140
|
-
empty: () => ({ input: 0, output: 0 }),
|
|
2141
|
-
append: (acc, lines) => {
|
|
2142
|
-
let usage = null;
|
|
2143
|
-
for (const line of lines) {
|
|
2144
|
-
if (!line.trim()) continue;
|
|
2145
|
-
let parsed;
|
|
2146
|
-
try {
|
|
2147
|
-
parsed = JSON.parse(line);
|
|
2148
|
-
} catch {
|
|
2149
|
-
continue;
|
|
2150
|
-
}
|
|
2151
|
-
if (parsed?.payload?.type === "token_count" && parsed.payload.info?.total_token_usage) {
|
|
2152
|
-
usage = parsed.payload.info.total_token_usage;
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
if (!usage) return acc;
|
|
2156
|
-
return {
|
|
2157
|
-
input: Math.max(0, num(usage.input_tokens) - num(usage.cached_input_tokens)),
|
|
2158
|
-
output: num(usage.output_tokens)
|
|
2159
|
-
};
|
|
2160
|
-
}
|
|
2161
|
-
};
|
|
2162
|
-
|
|
2163
|
-
// src/tokens/gemini.ts
|
|
2164
|
-
import * as fs7 from "fs/promises";
|
|
2165
|
-
import * as path8 from "path";
|
|
2166
|
-
function num2(v) {
|
|
2167
|
-
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
2168
|
-
}
|
|
2169
|
-
async function sumGeminiByConversation() {
|
|
2170
|
-
const files = await listChatFiles(geminiTmpDir());
|
|
2171
|
-
const cache = await ScanCache.open("gemini");
|
|
2172
|
-
const totals = await mapWithConcurrency(
|
|
2173
|
-
files,
|
|
2174
|
-
SCAN_CONCURRENCY,
|
|
2175
|
-
(f) => cache.readWhenChanged(f, async (raw) => sumGeminiRaw(f, raw)).catch(() => ({ input: 0, output: 0 }))
|
|
2176
|
-
);
|
|
2177
|
-
await cache.save();
|
|
2178
|
-
const byConv = /* @__PURE__ */ new Map();
|
|
2179
|
-
files.forEach((file, i) => byConv.set(file, totals[i]));
|
|
2180
|
-
return byConv;
|
|
2181
|
-
}
|
|
2182
|
-
async function sumGeminiTokens() {
|
|
2183
|
-
const byConv = await sumGeminiByConversation();
|
|
2184
|
-
let input = 0;
|
|
2185
|
-
let output = 0;
|
|
2186
|
-
for (const t of byConv.values()) {
|
|
2187
|
-
input += t.input;
|
|
2188
|
-
output += t.output;
|
|
2189
|
-
}
|
|
2190
|
-
return { input, output };
|
|
2191
|
-
}
|
|
2192
|
-
async function listChatFiles(root) {
|
|
2193
|
-
const entries = await readRoot(root, () => fs7.readdir(root));
|
|
2194
|
-
const out = [];
|
|
2195
|
-
for (const entry of entries) {
|
|
2196
|
-
const chatsDir = path8.join(root, entry, "chats");
|
|
2197
|
-
let files;
|
|
2198
|
-
try {
|
|
2199
|
-
files = await fs7.readdir(chatsDir);
|
|
2200
|
-
} catch {
|
|
2201
|
-
continue;
|
|
2202
|
-
}
|
|
2203
|
-
for (const f of files) {
|
|
2204
|
-
if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(path8.join(chatsDir, f));
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
return out;
|
|
2208
|
-
}
|
|
2209
|
-
function sumGeminiRaw(file, raw) {
|
|
2210
|
-
const messages = file.endsWith(".jsonl") ? parseJsonl(raw) : parseJson(raw);
|
|
2211
|
-
let input = 0;
|
|
2212
|
-
let output = 0;
|
|
2213
|
-
for (const m of messages) {
|
|
2214
|
-
const tk = m?.tokens;
|
|
2215
|
-
if (!tk || typeof tk !== "object") continue;
|
|
2216
|
-
input += Math.max(0, num2(tk.input) - num2(tk.cached));
|
|
2217
|
-
output += num2(tk.output);
|
|
2218
|
-
}
|
|
2219
|
-
return { input, output };
|
|
2220
|
-
}
|
|
2221
|
-
function parseJson(raw) {
|
|
2222
|
-
try {
|
|
2223
|
-
const data = JSON.parse(raw);
|
|
2224
|
-
return Array.isArray(data?.messages) ? data.messages : [];
|
|
2225
|
-
} catch {
|
|
2226
|
-
return [];
|
|
2586
|
+
const fh = await fs9.open(file, "r");
|
|
2587
|
+
try {
|
|
2588
|
+
const buf = Buffer.allocUnsafe(end - start);
|
|
2589
|
+
const { bytesRead } = await fh.read(buf, 0, end - start, start);
|
|
2590
|
+
const chunk = buf.subarray(0, bytesRead);
|
|
2591
|
+
const lastNl = chunk.lastIndexOf(10);
|
|
2592
|
+
if (lastNl === -1) {
|
|
2593
|
+
return { lines: [], tail: chunk.toString("utf8") || null, consumedTo: start };
|
|
2594
|
+
}
|
|
2595
|
+
const tail = chunk.subarray(lastNl + 1).toString("utf8");
|
|
2596
|
+
return {
|
|
2597
|
+
lines: chunk.subarray(0, lastNl).toString("utf8").split("\n"),
|
|
2598
|
+
tail: tail === "" ? null : tail,
|
|
2599
|
+
consumedTo: start + lastNl + 1
|
|
2600
|
+
};
|
|
2601
|
+
} finally {
|
|
2602
|
+
await fh.close();
|
|
2227
2603
|
}
|
|
2228
2604
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2605
|
+
|
|
2606
|
+
// src/tokens/harnesses/engine.ts
|
|
2607
|
+
async function readFile4(harness, cache, file) {
|
|
2608
|
+
const counting = harness.counting;
|
|
2609
|
+
if (counting.mode === "custom") throw new Error("custom counting is read whole-history, not per file");
|
|
2610
|
+
if (counting.mode === "whole-file") {
|
|
2611
|
+
return cache.readWhenChanged(file, async (raw) => counting.parse(raw, file));
|
|
2612
|
+
}
|
|
2613
|
+
const state = await cache.readIncremental(file, counting.fold);
|
|
2614
|
+
const notices = counting.notices?.(state) ?? [];
|
|
2615
|
+
return { families: counting.families(state), ...notices.length > 0 ? { notices } : {} };
|
|
2616
|
+
}
|
|
2617
|
+
async function count(harness) {
|
|
2618
|
+
const root = harness.root();
|
|
2619
|
+
const files = await harness.discover(root);
|
|
2620
|
+
const cache = await ScanCache.open(harness.id);
|
|
2621
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
2622
|
+
const notices = /* @__PURE__ */ new Set();
|
|
2623
|
+
const add = (id, families) => {
|
|
2624
|
+
const prefixed = `${harness.id}:${id}`;
|
|
2625
|
+
for (const [family, totals] of Object.entries(families)) {
|
|
2626
|
+
if (!totals) continue;
|
|
2627
|
+
const conversations = byFamily.get(family) ?? /* @__PURE__ */ new Map();
|
|
2628
|
+
const acc = conversations.get(prefixed) ?? { input: 0, output: 0 };
|
|
2629
|
+
acc.input += totals.input;
|
|
2630
|
+
acc.output += totals.output;
|
|
2631
|
+
conversations.set(prefixed, acc);
|
|
2632
|
+
byFamily.set(family, conversations);
|
|
2236
2633
|
}
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2634
|
+
};
|
|
2635
|
+
if (harness.counting.mode === "custom") {
|
|
2636
|
+
const reading = await harness.counting.read(cache, files, root);
|
|
2637
|
+
await cache.save();
|
|
2638
|
+
for (const notice of reading.notices ?? []) notices.add(notice);
|
|
2639
|
+
for (const [id, families] of reading.byConversation) add(id, families);
|
|
2640
|
+
return { byFamily, notices: [...notices] };
|
|
2641
|
+
}
|
|
2642
|
+
const readings = await mapWithConcurrency(files, SCAN_CONCURRENCY, (f) => readFile4(harness, cache, f));
|
|
2643
|
+
await cache.save();
|
|
2644
|
+
files.forEach((file, i) => {
|
|
2645
|
+
const reading = readings[i];
|
|
2646
|
+
for (const notice of reading.notices ?? []) notices.add(notice);
|
|
2647
|
+
add(harness.conversationId(file, root), reading.families);
|
|
2648
|
+
});
|
|
2649
|
+
return { byFamily, notices: [...notices] };
|
|
2650
|
+
}
|
|
2651
|
+
async function probe(harness) {
|
|
2652
|
+
const dir = harness.root();
|
|
2653
|
+
const exists = await fs10.stat(dir).then((st) => st.isDirectory()).catch(() => false);
|
|
2654
|
+
if (!exists) return { harness, dir, exists: false, projects: 0, transcripts: 0 };
|
|
2655
|
+
const entries = await fs10.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2656
|
+
const projects = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).length;
|
|
2657
|
+
const files = await harness.discover(dir).catch((e) => {
|
|
2658
|
+
if (e instanceof SourceRootMissing) return [];
|
|
2659
|
+
return [];
|
|
2660
|
+
});
|
|
2661
|
+
return { harness, dir, exists: true, projects, transcripts: files.length };
|
|
2239
2662
|
}
|
|
2240
2663
|
|
|
2241
2664
|
// src/tokens/race-tokens.ts
|
|
@@ -2270,56 +2693,48 @@ async function scanWithTimeout(scan, timeoutMs, describeTimeout) {
|
|
|
2270
2693
|
clearTimeout(timer);
|
|
2271
2694
|
}
|
|
2272
2695
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
return race.counts_input ? t.input + t.output : t.output;
|
|
2285
|
-
}
|
|
2286
|
-
async function readAllSources(race, primary, progress) {
|
|
2287
|
-
progress?.begin(primary);
|
|
2288
|
-
const primaryScan = BY_CONVERSATION_READERS[primary]().then(
|
|
2289
|
-
(map) => ({ ok: true, map }),
|
|
2290
|
-
(err) => ({ ok: false, err })
|
|
2291
|
-
).finally(() => progress?.end(primary));
|
|
2292
|
-
const secondaryKeys = MODEL_KEYS.filter((k) => k !== primary);
|
|
2293
|
-
const secondaryScans = secondaryKeys.map((k) => {
|
|
2294
|
-
progress?.begin(k);
|
|
2295
|
-
return SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch((err) => {
|
|
2296
|
-
if (!(err instanceof SourceRootMissing)) {
|
|
2297
|
-
logWarn("scan.source.err", { source: k, message: err?.message ?? String(err) });
|
|
2298
|
-
}
|
|
2299
|
-
return 0;
|
|
2300
|
-
}).finally(() => progress?.end(k));
|
|
2301
|
-
});
|
|
2302
|
-
const [primaryResult, secondaryValues] = await Promise.all([
|
|
2303
|
-
primaryScan,
|
|
2304
|
-
Promise.all(secondaryScans)
|
|
2305
|
-
]);
|
|
2306
|
-
const primaryByConv = /* @__PURE__ */ new Map();
|
|
2307
|
-
if (primaryResult.ok) {
|
|
2308
|
-
for (const [id, totals] of primaryResult.map) primaryByConv.set(id, scoreFor(race, totals));
|
|
2309
|
-
} else if (!(primaryResult.err instanceof SourceRootMissing)) {
|
|
2310
|
-
const err = primaryResult.err;
|
|
2311
|
-
return { stall: `Can't read ${primary} token usage: ${err?.message ?? String(err)}` };
|
|
2312
|
-
}
|
|
2313
|
-
const secondary = { claude: 0, codex: 0, gemini: 0 };
|
|
2314
|
-
secondaryKeys.forEach((k, i) => {
|
|
2315
|
-
secondary[k] = secondaryValues[i] ?? 0;
|
|
2696
|
+
function emptyByFamily() {
|
|
2697
|
+
return { anthropic: /* @__PURE__ */ new Map(), openai: /* @__PURE__ */ new Map(), google: /* @__PURE__ */ new Map() };
|
|
2698
|
+
}
|
|
2699
|
+
function scoreFor(t) {
|
|
2700
|
+
return t.input + t.output;
|
|
2701
|
+
}
|
|
2702
|
+
async function readAllSources(progress) {
|
|
2703
|
+
const enabled = enabledHarnesses(await loadPrefs());
|
|
2704
|
+
const scans = enabled.map((key) => {
|
|
2705
|
+
progress?.begin(key);
|
|
2706
|
+
return count(HARNESSES[key]).then((result) => ({ ok: true, key, result })).catch((err) => ({ ok: false, key, err })).finally(() => progress?.end(key));
|
|
2316
2707
|
});
|
|
2317
|
-
|
|
2708
|
+
const results = await Promise.all(scans);
|
|
2709
|
+
const byFamily = emptyByFamily();
|
|
2710
|
+
const degraded = [];
|
|
2711
|
+
const notices = /* @__PURE__ */ new Set();
|
|
2712
|
+
for (const outcome of results) {
|
|
2713
|
+
const harness = HARNESSES[outcome.key];
|
|
2714
|
+
if (!outcome.ok) {
|
|
2715
|
+
if (outcome.err instanceof SourceRootMissing) continue;
|
|
2716
|
+
const message = outcome.err?.message ?? String(outcome.err);
|
|
2717
|
+
logWarn("scan.source.err", { harness: outcome.key, message });
|
|
2718
|
+
degraded.push({ harness: outcome.key, label: harness.label, message });
|
|
2719
|
+
continue;
|
|
2720
|
+
}
|
|
2721
|
+
mergeInto(byFamily, outcome.result);
|
|
2722
|
+
for (const notice of outcome.result.notices) notices.add(notice);
|
|
2723
|
+
}
|
|
2724
|
+
return { byFamily, degraded, notices: [...notices] };
|
|
2725
|
+
}
|
|
2726
|
+
function mergeInto(byFamily, result) {
|
|
2727
|
+
for (const family of MODEL_FAMILIES) {
|
|
2728
|
+
const conversations = result.byFamily.get(family);
|
|
2729
|
+
if (!conversations) continue;
|
|
2730
|
+
const target = byFamily[family];
|
|
2731
|
+
for (const [id, totals] of conversations) target.set(id, scoreFor(totals));
|
|
2732
|
+
}
|
|
2318
2733
|
}
|
|
2319
2734
|
|
|
2320
2735
|
// src/tokens/scan-progress.ts
|
|
2321
2736
|
function skipVar(key) {
|
|
2322
|
-
return
|
|
2737
|
+
return HARNESSES[key].overrideVar;
|
|
2323
2738
|
}
|
|
2324
2739
|
function formatBytes(bytes) {
|
|
2325
2740
|
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
|
|
@@ -2353,45 +2768,30 @@ async function diagnoseScanTimeout(timeoutMs, progress) {
|
|
|
2353
2768
|
return describeScanTimeout(timeoutMs, outstanding);
|
|
2354
2769
|
}
|
|
2355
2770
|
|
|
2356
|
-
// src/tokens/primary-cap.ts
|
|
2357
|
-
var PRIMARY_TOP_CONVERSATIONS = 5;
|
|
2358
|
-
function primaryConversationCap(enabled) {
|
|
2359
|
-
return enabled ? PRIMARY_TOP_CONVERSATIONS : Infinity;
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
2771
|
// src/tokens/race-score.ts
|
|
2363
2772
|
var STALL_THRESHOLD = 5;
|
|
2364
|
-
function
|
|
2365
|
-
return {
|
|
2773
|
+
function cloneAnchors(a) {
|
|
2774
|
+
return { anthropic: { ...a.anthropic }, openai: { ...a.openai }, google: { ...a.google } };
|
|
2366
2775
|
}
|
|
2367
2776
|
var RaceScoreTracker = class {
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
primaryConvAcked;
|
|
2371
|
-
primaryConvLast;
|
|
2777
|
+
convAcked;
|
|
2778
|
+
convLast;
|
|
2372
2779
|
counted;
|
|
2373
2780
|
seq;
|
|
2374
2781
|
stalls = 0;
|
|
2375
2782
|
lastStall = null;
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
this.
|
|
2381
|
-
this.lastGood = { ...init.lastGood };
|
|
2382
|
-
this.primaryConvAcked = { ...init.primaryConvAcked };
|
|
2383
|
-
this.primaryConvLast = { ...init.primaryConvAcked };
|
|
2384
|
-
this.counted = init.primaryCounted;
|
|
2783
|
+
emptyBeats = 0;
|
|
2784
|
+
constructor(init) {
|
|
2785
|
+
this.convAcked = cloneAnchors(init.convAcked);
|
|
2786
|
+
this.convLast = cloneAnchors(init.convAcked);
|
|
2787
|
+
this.counted = { ...init.counted };
|
|
2385
2788
|
this.seq = init.seq;
|
|
2386
|
-
this.primary = primary;
|
|
2387
|
-
this.primaryTop5 = primaryTop5;
|
|
2388
2789
|
}
|
|
2389
2790
|
/**
|
|
2390
2791
|
* Record a scan result.
|
|
2391
2792
|
* - `null` or a `{ stall }` reading → stall (warning), anchors untouched. A
|
|
2392
2793
|
* stall reading also captures its cause for the UI.
|
|
2393
|
-
* - otherwise →
|
|
2394
|
-
* primary's per-conversation latest readings are updated (monotonic).
|
|
2794
|
+
* - otherwise → every model's per-conversation readings advance (monotonic).
|
|
2395
2795
|
*/
|
|
2396
2796
|
recordReading(reading) {
|
|
2397
2797
|
if (reading === null || isStall(reading)) {
|
|
@@ -2401,181 +2801,91 @@ var RaceScoreTracker = class {
|
|
|
2401
2801
|
}
|
|
2402
2802
|
this.stalls = 0;
|
|
2403
2803
|
this.lastStall = null;
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
const prev = this.primaryConvLast[id] ?? 0;
|
|
2412
|
-
if (v > prev) this.primaryConvLast[id] = v;
|
|
2804
|
+
const anyConversations = MODEL_FAMILIES.some((family) => reading.byFamily[family].size > 0);
|
|
2805
|
+
this.emptyBeats = anyConversations ? 0 : this.emptyBeats + 1;
|
|
2806
|
+
for (const family of MODEL_FAMILIES) {
|
|
2807
|
+
for (const [id, value] of reading.byFamily[family]) {
|
|
2808
|
+
const prev = this.convLast[family][id] ?? 0;
|
|
2809
|
+
if (value > prev) this.convLast[family][id] = value;
|
|
2810
|
+
}
|
|
2413
2811
|
}
|
|
2414
2812
|
}
|
|
2415
2813
|
/** Frozen payload for the next heartbeat. Pure — call repeatedly for retries. */
|
|
2416
2814
|
nextBeat() {
|
|
2417
|
-
const components =
|
|
2418
|
-
for (const
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
const d = Math.max(0, last - (this.primaryConvAcked[id] ?? 0));
|
|
2425
|
-
if (d > 0) pending.push(d);
|
|
2815
|
+
const components = zeroPerFamily();
|
|
2816
|
+
for (const family of MODEL_FAMILIES) {
|
|
2817
|
+
let sum = 0;
|
|
2818
|
+
for (const [id, last] of Object.entries(this.convLast[family])) {
|
|
2819
|
+
sum += Math.max(0, last - (this.convAcked[family][id] ?? 0));
|
|
2820
|
+
}
|
|
2821
|
+
components[family] = sum;
|
|
2426
2822
|
}
|
|
2427
|
-
pending.sort((a, b) => b - a);
|
|
2428
|
-
const cap = primaryConversationCap(this.primaryTop5);
|
|
2429
|
-
const take = cap === Infinity ? pending.length : Math.min(cap, pending.length);
|
|
2430
|
-
let primarySum = 0;
|
|
2431
|
-
for (const d of pending.slice(0, take)) primarySum += d;
|
|
2432
|
-
components[this.primary] = primarySum;
|
|
2433
2823
|
return {
|
|
2434
2824
|
seq: this.seq + 1,
|
|
2435
2825
|
components,
|
|
2436
|
-
|
|
2437
|
-
primaryConvReadings: { ...this.primaryConvLast }
|
|
2826
|
+
convReadings: cloneAnchors(this.convLast)
|
|
2438
2827
|
};
|
|
2439
2828
|
}
|
|
2440
2829
|
/** Commit a heartbeat the server accepted. `serverLastSeq` self-heals drift. */
|
|
2441
2830
|
ack(snapshot, serverLastSeq) {
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
this.acked[key] = snapshot.readings[key];
|
|
2445
|
-
}
|
|
2446
|
-
this.primaryConvAcked = { ...snapshot.primaryConvReadings };
|
|
2447
|
-
this.counted += snapshot.components[this.primary];
|
|
2831
|
+
this.convAcked = cloneAnchors(snapshot.convReadings);
|
|
2832
|
+
for (const family of MODEL_FAMILIES) this.counted[family] += snapshot.components[family];
|
|
2448
2833
|
this.seq = Math.max(snapshot.seq, serverLastSeq);
|
|
2449
2834
|
}
|
|
2450
2835
|
/** Pin anchors to the latest readings so the next deltas are 0 (pending race). */
|
|
2451
2836
|
reprime() {
|
|
2452
|
-
|
|
2453
|
-
if (key === this.primary) continue;
|
|
2454
|
-
this.acked[key] = this.lastGood[key];
|
|
2455
|
-
}
|
|
2456
|
-
this.primaryConvAcked = { ...this.primaryConvLast };
|
|
2837
|
+
this.convAcked = cloneAnchors(this.convLast);
|
|
2457
2838
|
}
|
|
2458
2839
|
get stalled() {
|
|
2459
2840
|
return this.stalls >= STALL_THRESHOLD;
|
|
2460
2841
|
}
|
|
2461
|
-
/**
|
|
2462
|
-
get
|
|
2463
|
-
return this.
|
|
2842
|
+
/** No source has produced any conversations for long enough to be worth saying. */
|
|
2843
|
+
get sourcesSilent() {
|
|
2844
|
+
return this.emptyBeats >= SILENT_THRESHOLD;
|
|
2464
2845
|
}
|
|
2465
2846
|
/** Human-readable cause of the most recent stall (null once a good read recovers). */
|
|
2466
2847
|
get stallReason() {
|
|
2467
2848
|
return this.lastStall;
|
|
2468
2849
|
}
|
|
2469
|
-
/** Cumulative
|
|
2470
|
-
|
|
2471
|
-
return this.counted;
|
|
2850
|
+
/** Cumulative tokens credited per family since joining (for the UI's model rows). */
|
|
2851
|
+
countedPerFamily() {
|
|
2852
|
+
return { ...this.counted };
|
|
2472
2853
|
}
|
|
2473
|
-
/**
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
for (const
|
|
2477
|
-
|
|
2478
|
-
out[key] = Math.max(0, this.lastGood[key] - baseline[key]);
|
|
2479
|
-
}
|
|
2480
|
-
return out;
|
|
2854
|
+
/** Cumulative tokens credited across every family. */
|
|
2855
|
+
countedTotal() {
|
|
2856
|
+
let total = 0;
|
|
2857
|
+
for (const family of MODEL_FAMILIES) total += this.counted[family];
|
|
2858
|
+
return total;
|
|
2481
2859
|
}
|
|
2482
2860
|
toState() {
|
|
2483
2861
|
return {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
primaryConvAcked: { ...this.primaryConvAcked },
|
|
2487
|
-
primaryCounted: this.counted,
|
|
2862
|
+
convAcked: cloneAnchors(this.convAcked),
|
|
2863
|
+
counted: { ...this.counted },
|
|
2488
2864
|
seq: this.seq
|
|
2489
2865
|
};
|
|
2490
2866
|
}
|
|
2491
2867
|
};
|
|
2492
2868
|
|
|
2493
|
-
// src/tokens/source-probe.ts
|
|
2494
|
-
import * as fs8 from "fs/promises";
|
|
2495
|
-
var ROOTS = {
|
|
2496
|
-
claude: claudeProjectsDir,
|
|
2497
|
-
codex: codexSessionsDir,
|
|
2498
|
-
gemini: geminiTmpDir
|
|
2499
|
-
};
|
|
2500
|
-
var LISTERS = {
|
|
2501
|
-
claude: listJsonlFiles,
|
|
2502
|
-
codex: listCodexRollouts,
|
|
2503
|
-
gemini: listChatFiles
|
|
2504
|
-
};
|
|
2505
|
-
var LABELS2 = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
|
|
2506
|
-
function sourceDir(key) {
|
|
2507
|
-
return ROOTS[key]();
|
|
2508
|
-
}
|
|
2509
|
-
async function probeSource(key) {
|
|
2510
|
-
const dir = ROOTS[key]();
|
|
2511
|
-
const exists = await fs8.stat(dir).then((st) => st.isDirectory()).catch(() => false);
|
|
2512
|
-
if (!exists) return { key, dir, exists: false, projects: 0, transcripts: 0 };
|
|
2513
|
-
const entries = await fs8.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2514
|
-
const projects = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).length;
|
|
2515
|
-
const files = await LISTERS[key](dir).catch(() => []);
|
|
2516
|
-
return { key, dir, exists: true, projects, transcripts: files.length };
|
|
2517
|
-
}
|
|
2518
|
-
function overrideVar(key) {
|
|
2519
|
-
return `TOKEN_DERBY_${key.toUpperCase()}_DIR`;
|
|
2520
|
-
}
|
|
2521
|
-
async function confirmEmptySource(opts) {
|
|
2522
|
-
if (opts.probe.transcripts > 0) return true;
|
|
2523
|
-
opts.warn(describeEmptySource(opts.probe));
|
|
2524
|
-
if (!opts.interactive) return true;
|
|
2525
|
-
return opts.ask();
|
|
2526
|
-
}
|
|
2527
|
-
function describeEmptySource(probe) {
|
|
2528
|
-
const label = LABELS2[probe.key];
|
|
2529
|
-
const populated = probe.exists && probe.projects > 0;
|
|
2530
|
-
const reason = !probe.exists ? "does not exist" : populated ? `holds ${probe.projects} project ${probe.projects === 1 ? "directory" : "directories"}, none of which could be read` : "exists, but holds no transcripts";
|
|
2531
|
-
const lines = [
|
|
2532
|
-
`\u26A0 No ${label} transcripts found \u2014 your horse will not move.`,
|
|
2533
|
-
``,
|
|
2534
|
-
` Looked in: ${probe.dir}`,
|
|
2535
|
-
` (${reason})`,
|
|
2536
|
-
``
|
|
2537
|
-
];
|
|
2538
|
-
if (populated) {
|
|
2539
|
-
lines.push(
|
|
2540
|
-
` The directory is there and has history in it, so this is usually a`,
|
|
2541
|
-
` dangling symlink or a permissions problem on one of those projects.`,
|
|
2542
|
-
` To find dangling links:`,
|
|
2543
|
-
` find ${probe.dir} -type l ! -exec test -e {} \\; -print`,
|
|
2544
|
-
``
|
|
2545
|
-
);
|
|
2546
|
-
}
|
|
2547
|
-
lines.push(
|
|
2548
|
-
` Token Derby counts ${label} usage from this machine's own filesystem.`,
|
|
2549
|
-
` If ${label} runs in a container, over SSH, or on another machine, join`,
|
|
2550
|
-
` the race from there instead.`
|
|
2551
|
-
);
|
|
2552
|
-
if (probe.key === "claude") {
|
|
2553
|
-
lines.push(
|
|
2554
|
-
` If CLAUDE_CONFIG_DIR relocated your config, Token Derby follows it \u2014`,
|
|
2555
|
-
` check it points at the config root, not the projects directory.`
|
|
2556
|
-
);
|
|
2557
|
-
}
|
|
2558
|
-
lines.push(``, ` To read them from somewhere else: export ${overrideVar(probe.key)}=<dir>`);
|
|
2559
|
-
return lines.join("\n");
|
|
2560
|
-
}
|
|
2561
|
-
|
|
2562
2869
|
// src/runtime/run-race.tsx
|
|
2563
|
-
import { jsx as
|
|
2870
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2564
2871
|
function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
2565
2872
|
const { exit } = useApp();
|
|
2566
|
-
const [race, setRace] =
|
|
2567
|
-
const [lastHbAt, setLastHbAt] =
|
|
2568
|
-
const [lastHbOk, setLastHbOk] =
|
|
2569
|
-
const [tickNow, setTickNow] =
|
|
2570
|
-
const [fatalError, setFatalError] =
|
|
2571
|
-
const [achievements, setAchievements] =
|
|
2873
|
+
const [race, setRace] = useState4(null);
|
|
2874
|
+
const [lastHbAt, setLastHbAt] = useState4(null);
|
|
2875
|
+
const [lastHbOk, setLastHbOk] = useState4(true);
|
|
2876
|
+
const [tickNow, setTickNow] = useState4(/* @__PURE__ */ new Date());
|
|
2877
|
+
const [fatalError, setFatalError] = useState4(null);
|
|
2878
|
+
const [achievements, setAchievements] = useState4([]);
|
|
2572
2879
|
const shownAchievementAtRef = useRef(0);
|
|
2573
|
-
const trackerRef = useRef(new RaceScoreTracker(initialState
|
|
2880
|
+
const trackerRef = useRef(new RaceScoreTracker(initialState));
|
|
2574
2881
|
const pendingRef = useRef(pendingMode);
|
|
2575
2882
|
const ctrl = useRef(new AbortController());
|
|
2576
|
-
const [stalled, setStalled] =
|
|
2577
|
-
const [stallReason, setStallReason] =
|
|
2578
|
-
const [
|
|
2883
|
+
const [stalled, setStalled] = useState4(false);
|
|
2884
|
+
const [stallReason, setStallReason] = useState4(null);
|
|
2885
|
+
const [sourcesSilent, setSourcesSilent] = useState4(false);
|
|
2886
|
+
const [degraded, setDegraded] = useState4([]);
|
|
2887
|
+
const [notices, setNotices] = useState4([]);
|
|
2888
|
+
const [disabledHarnesses, setDisabledHarnesses] = useState4([]);
|
|
2579
2889
|
useEffect2(() => {
|
|
2580
2890
|
const t = setInterval(() => setTickNow(/* @__PURE__ */ new Date()), 1e3);
|
|
2581
2891
|
return () => clearInterval(t);
|
|
@@ -2592,7 +2902,7 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2592
2902
|
const progress = new ScanProgress();
|
|
2593
2903
|
try {
|
|
2594
2904
|
return await scanWithTimeout(
|
|
2595
|
-
() => readAllSources(
|
|
2905
|
+
() => readAllSources(progress),
|
|
2596
2906
|
SCAN_TIMEOUT_MS,
|
|
2597
2907
|
() => diagnoseScanTimeout(SCAN_TIMEOUT_MS, progress)
|
|
2598
2908
|
);
|
|
@@ -2607,7 +2917,11 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2607
2917
|
if (pendingRef.current && !isStall(reading)) tracker.reprime();
|
|
2608
2918
|
setStalled(tracker.stalled);
|
|
2609
2919
|
setStallReason(tracker.stalled ? tracker.stallReason : null);
|
|
2610
|
-
|
|
2920
|
+
setSourcesSilent(tracker.sourcesSilent);
|
|
2921
|
+
setDegraded(isStall(reading) ? [] : reading.degraded);
|
|
2922
|
+
setNotices(isStall(reading) ? [] : reading.notices);
|
|
2923
|
+
const prefs = await loadPrefs();
|
|
2924
|
+
setDisabledHarnesses(HARNESS_KEYS.filter((k) => !isHarnessEnabled(prefs, k)));
|
|
2611
2925
|
return tracker.nextBeat();
|
|
2612
2926
|
},
|
|
2613
2927
|
sendBeat: async (snapshot) => {
|
|
@@ -2657,13 +2971,13 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2657
2971
|
}, []);
|
|
2658
2972
|
const lastHeartbeatAgoSec = lastHbAt ? Math.max(0, Math.floor((tickNow.getTime() - lastHbAt.getTime()) / 1e3)) : null;
|
|
2659
2973
|
if (fatalError) {
|
|
2660
|
-
return /* @__PURE__ */
|
|
2661
|
-
/* @__PURE__ */
|
|
2662
|
-
/* @__PURE__ */
|
|
2974
|
+
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", padding: 1, children: [
|
|
2975
|
+
/* @__PURE__ */ jsx7(Text7, { color: "red", bold: true, children: "CLI version mismatch \u2014 disconnected" }),
|
|
2976
|
+
/* @__PURE__ */ jsx7(Text7, { children: fatalError })
|
|
2663
2977
|
] });
|
|
2664
2978
|
}
|
|
2665
|
-
return /* @__PURE__ */
|
|
2666
|
-
/* @__PURE__ */
|
|
2979
|
+
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
|
|
2980
|
+
/* @__PURE__ */ jsx7(
|
|
2667
2981
|
StatusScreen,
|
|
2668
2982
|
{
|
|
2669
2983
|
race,
|
|
@@ -2675,28 +2989,29 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2675
2989
|
lastHeartbeatOk: lastHbOk,
|
|
2676
2990
|
stalled,
|
|
2677
2991
|
stallReason,
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2992
|
+
sourcesSilent,
|
|
2993
|
+
degraded,
|
|
2994
|
+
notices,
|
|
2995
|
+
disabledHarnesses
|
|
2681
2996
|
}
|
|
2682
2997
|
),
|
|
2683
|
-
achievements.length > 0 && /* @__PURE__ */
|
|
2684
|
-
/* @__PURE__ */
|
|
2998
|
+
achievements.length > 0 && /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
2999
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, children: "Achievements" }),
|
|
2685
3000
|
achievements.map(({ key, event }) => {
|
|
2686
|
-
const description = describeAchievement(event
|
|
2687
|
-
return /* @__PURE__ */
|
|
2688
|
-
/* @__PURE__ */
|
|
3001
|
+
const description = describeAchievement(event);
|
|
3002
|
+
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "row", children: [
|
|
3003
|
+
/* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
|
|
2689
3004
|
" ",
|
|
2690
3005
|
formatClockTime(event.at),
|
|
2691
3006
|
" "
|
|
2692
3007
|
] }),
|
|
2693
|
-
/* @__PURE__ */
|
|
3008
|
+
/* @__PURE__ */ jsxs5(Text7, { color: "yellow", bold: true, children: [
|
|
2694
3009
|
"+",
|
|
2695
3010
|
event.xp,
|
|
2696
3011
|
" XP "
|
|
2697
3012
|
] }),
|
|
2698
|
-
/* @__PURE__ */
|
|
2699
|
-
/* @__PURE__ */
|
|
3013
|
+
/* @__PURE__ */ jsx7(Text7, { children: event.name }),
|
|
3014
|
+
/* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
|
|
2700
3015
|
" \u2014 ",
|
|
2701
3016
|
description
|
|
2702
3017
|
] })
|
|
@@ -2722,28 +3037,81 @@ function raceViewFrom(resp) {
|
|
|
2722
3037
|
};
|
|
2723
3038
|
}
|
|
2724
3039
|
async function buildInitialState(args) {
|
|
2725
|
-
|
|
2726
|
-
const primaryConvAcked = {};
|
|
3040
|
+
const convAcked = { anthropic: {}, openai: {}, google: {} };
|
|
2727
3041
|
try {
|
|
2728
|
-
const now = await readAllSources(
|
|
3042
|
+
const now = await readAllSources();
|
|
2729
3043
|
if (!isStall(now)) {
|
|
2730
|
-
|
|
2731
|
-
|
|
3044
|
+
for (const family of MODEL_FAMILIES) {
|
|
3045
|
+
for (const [id, value] of now.byFamily[family]) convAcked[family][id] = value;
|
|
3046
|
+
}
|
|
2732
3047
|
}
|
|
2733
3048
|
} catch {
|
|
2734
3049
|
}
|
|
2735
3050
|
return {
|
|
2736
3051
|
initialState: {
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
primaryConvAcked,
|
|
2740
|
-
primaryCounted: 0,
|
|
3052
|
+
convAcked,
|
|
3053
|
+
counted: zeroPerFamily(),
|
|
2741
3054
|
seq: args.serverLastSeq
|
|
2742
3055
|
},
|
|
2743
3056
|
pendingMode: args.raceStatus === "pending"
|
|
2744
3057
|
};
|
|
2745
3058
|
}
|
|
2746
3059
|
|
|
3060
|
+
// src/tokens/source-probe.ts
|
|
3061
|
+
async function probeAll() {
|
|
3062
|
+
const enabled = enabledHarnesses(await loadPrefs());
|
|
3063
|
+
return Promise.all(enabled.map((key) => probe(HARNESSES[key])));
|
|
3064
|
+
}
|
|
3065
|
+
async function confirmNoSources(opts) {
|
|
3066
|
+
if (opts.probes.some((p) => p.transcripts > 0)) return true;
|
|
3067
|
+
opts.warn(opts.probes.length === 0 ? describeAllDisabled() : describeNoSources(opts.probes));
|
|
3068
|
+
if (!opts.interactive) return true;
|
|
3069
|
+
return opts.ask();
|
|
3070
|
+
}
|
|
3071
|
+
function describeNoSources(probes) {
|
|
3072
|
+
const lines = [
|
|
3073
|
+
`\u26A0 No transcripts found for any coding agent \u2014 your horse will not move.`,
|
|
3074
|
+
``
|
|
3075
|
+
];
|
|
3076
|
+
for (const p of probes) {
|
|
3077
|
+
const label = p.harness.label;
|
|
3078
|
+
lines.push(` ${label}: ${p.dir}`, ` ${" ".repeat(label.length)} (${reasonFor(p)})`);
|
|
3079
|
+
if (p.exists && p.projects > 0) {
|
|
3080
|
+
lines.push(
|
|
3081
|
+
` Has history in it, so this is usually a dangling symlink or a`,
|
|
3082
|
+
` permissions problem. To find dangling links:`,
|
|
3083
|
+
` find ${p.dir} -type l ! -exec test -e {} \\; -print`
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
3086
|
+
for (const hint of p.harness.hints ?? []) lines.push(` ${hint}`);
|
|
3087
|
+
}
|
|
3088
|
+
lines.push(
|
|
3089
|
+
``,
|
|
3090
|
+
` Token Derby counts usage from this machine's own filesystem. If your`,
|
|
3091
|
+
` coding agent runs in a container, over SSH, or on another machine, join`,
|
|
3092
|
+
` the race from there instead.`,
|
|
3093
|
+
``,
|
|
3094
|
+
` To read them from somewhere else, set the matching directory override:`,
|
|
3095
|
+
` ${HARNESS_KEYS.map((k) => HARNESSES[k].overrideVar).join(" ")}`
|
|
3096
|
+
);
|
|
3097
|
+
return lines.join("\n");
|
|
3098
|
+
}
|
|
3099
|
+
function describeAllDisabled() {
|
|
3100
|
+
return [
|
|
3101
|
+
`\u26A0 Every coding agent is turned off \u2014 your horse will not move.`,
|
|
3102
|
+
``,
|
|
3103
|
+
` Turn one back on with: token-derby harness enable <id>`,
|
|
3104
|
+
` See what is available: token-derby harness list`
|
|
3105
|
+
].join("\n");
|
|
3106
|
+
}
|
|
3107
|
+
function reasonFor(probe2) {
|
|
3108
|
+
if (!probe2.exists) return "does not exist";
|
|
3109
|
+
if (probe2.projects > 0) {
|
|
3110
|
+
return `holds ${probe2.projects} project ${probe2.projects === 1 ? "directory" : "directories"}, none of which could be read`;
|
|
3111
|
+
}
|
|
3112
|
+
return "exists, but holds no transcripts";
|
|
3113
|
+
}
|
|
3114
|
+
|
|
2747
3115
|
// src/ui/prompt.ts
|
|
2748
3116
|
async function promptYesNo(question, opts = {}) {
|
|
2749
3117
|
const input = opts.input ?? process.stdin;
|
|
@@ -2767,31 +3135,12 @@ function resetStdinAfterInk() {
|
|
|
2767
3135
|
}
|
|
2768
3136
|
|
|
2769
3137
|
// src/commands/join.ts
|
|
2770
|
-
function parsePrimaryFlag(argv) {
|
|
2771
|
-
for (let i = 0; i < argv.length; i++) {
|
|
2772
|
-
const a = argv[i];
|
|
2773
|
-
let value;
|
|
2774
|
-
if (a === "--primary") value = argv[i + 1];
|
|
2775
|
-
else if (a.startsWith("--primary=")) value = a.slice("--primary=".length);
|
|
2776
|
-
else continue;
|
|
2777
|
-
if (!isModelKey(value)) throw new Error(`--primary must be one of claude, codex, gemini (got ${value ?? ""})`);
|
|
2778
|
-
return value;
|
|
2779
|
-
}
|
|
2780
|
-
return null;
|
|
2781
|
-
}
|
|
2782
3138
|
async function joinCommand(joinCode, argv = []) {
|
|
2783
3139
|
if (!joinCode) {
|
|
2784
3140
|
console.error("Usage: token-derby join <join-code>");
|
|
2785
3141
|
return 2;
|
|
2786
3142
|
}
|
|
2787
3143
|
const code = joinCode.toUpperCase();
|
|
2788
|
-
let primaryFlag;
|
|
2789
|
-
try {
|
|
2790
|
-
primaryFlag = parsePrimaryFlag(argv);
|
|
2791
|
-
} catch (e) {
|
|
2792
|
-
console.error(e.message);
|
|
2793
|
-
return 2;
|
|
2794
|
-
}
|
|
2795
3144
|
const identity = await loadIdentity();
|
|
2796
3145
|
if (!identity) {
|
|
2797
3146
|
console.error("Run `token-derby login` to set up your identity.");
|
|
@@ -2846,7 +3195,27 @@ async function joinCommand(joinCode, argv = []) {
|
|
|
2846
3195
|
console.error("Your stable is empty. Run `token-derby stable create` first.");
|
|
2847
3196
|
return 1;
|
|
2848
3197
|
}
|
|
2849
|
-
const
|
|
3198
|
+
const choice = await resolveHorse(horses, {
|
|
3199
|
+
name: parseFlag(argv, "--horse"),
|
|
3200
|
+
pick: hasFlag(argv, "--pick")
|
|
3201
|
+
});
|
|
3202
|
+
if (choice.kind === "not_found") {
|
|
3203
|
+
console.error(`No horse named "${choice.name}" in your stable.`);
|
|
3204
|
+
console.error(`Your stable: ${horses.map((h) => h.name).join(", ")}`);
|
|
3205
|
+
return 1;
|
|
3206
|
+
}
|
|
3207
|
+
if (choice.kind === "no_tty") {
|
|
3208
|
+
console.error(noTtyMessage("token-derby join"));
|
|
3209
|
+
return 1;
|
|
3210
|
+
}
|
|
3211
|
+
let picked;
|
|
3212
|
+
if (choice.kind === "resolved") {
|
|
3213
|
+
picked = choice.horse;
|
|
3214
|
+
const notice = noticeFor(choice);
|
|
3215
|
+
if (notice) console.log(notice);
|
|
3216
|
+
} else {
|
|
3217
|
+
picked = await pickHorse(horses);
|
|
3218
|
+
}
|
|
2850
3219
|
if (!picked) {
|
|
2851
3220
|
console.log("Cancelled.");
|
|
2852
3221
|
return 1;
|
|
@@ -2855,14 +3224,8 @@ async function joinCommand(joinCode, argv = []) {
|
|
|
2855
3224
|
chosenName = picked.name;
|
|
2856
3225
|
chosenColors = picked.colors;
|
|
2857
3226
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
if (primaryFlag) chosenPrimary = primaryFlag;
|
|
2861
|
-
else if (process.stdout.isTTY) chosenPrimary = await pickPrimary();
|
|
2862
|
-
}
|
|
2863
|
-
const effectivePrimary = ownHorse?.primary_model ?? chosenPrimary;
|
|
2864
|
-
const proceed = await confirmEmptySource({
|
|
2865
|
-
probe: await probeSource(effectivePrimary),
|
|
3227
|
+
const proceed = await confirmNoSources({
|
|
3228
|
+
probes: await probeAll(),
|
|
2866
3229
|
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
2867
3230
|
warn: (text) => console.error(`
|
|
2868
3231
|
${text}
|
|
@@ -2875,7 +3238,7 @@ ${text}
|
|
|
2875
3238
|
}
|
|
2876
3239
|
let joinResp;
|
|
2877
3240
|
try {
|
|
2878
|
-
joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId
|
|
3241
|
+
joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId });
|
|
2879
3242
|
} catch (e) {
|
|
2880
3243
|
if (e instanceof ApiError) {
|
|
2881
3244
|
if (e.code === "RACE_FULL") console.error("This race is full.");
|
|
@@ -2901,27 +3264,22 @@ ${text}
|
|
|
2901
3264
|
horse_colors: chosenColors,
|
|
2902
3265
|
joined_at: ownHorse?.joined_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2903
3266
|
last_heartbeat_at: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2904
|
-
primary_model: joinResp.primary_model,
|
|
2905
3267
|
score: {
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
primaryConvAcked: {},
|
|
2909
|
-
primaryCounted: 0,
|
|
3268
|
+
convAcked: { anthropic: {}, openai: {}, google: {} },
|
|
3269
|
+
counted: { anthropic: 0, openai: 0, google: 0 },
|
|
2910
3270
|
seq: ownHorse?.last_seq ?? 0
|
|
2911
|
-
}
|
|
2912
|
-
...race.counts_input ? { counts_input: true } : {},
|
|
2913
|
-
...race.primary_top5 ? { primary_top5: true } : {}
|
|
3271
|
+
}
|
|
2914
3272
|
};
|
|
2915
3273
|
await saveActiveRace(active);
|
|
2916
3274
|
const initial = await buildInitialState({ active, raceStatus: status, serverLastSeq: ownHorse?.last_seq ?? 0 });
|
|
2917
|
-
const app = render4(
|
|
3275
|
+
const app = render4(React5.createElement(RunRace, { active, initialState: initial.initialState, pendingMode: initial.pendingMode, ownUserName: identity.display_name }));
|
|
2918
3276
|
await app.waitUntilExit();
|
|
2919
3277
|
return 0;
|
|
2920
3278
|
}
|
|
2921
3279
|
async function pickHorse(horses) {
|
|
2922
3280
|
return new Promise((resolve) => {
|
|
2923
3281
|
const app = render4(
|
|
2924
|
-
|
|
3282
|
+
React5.createElement(HorsePicker, {
|
|
2925
3283
|
horses,
|
|
2926
3284
|
onPick: (h) => {
|
|
2927
3285
|
app.unmount();
|
|
@@ -2935,18 +3293,6 @@ async function pickHorse(horses) {
|
|
|
2935
3293
|
);
|
|
2936
3294
|
});
|
|
2937
3295
|
}
|
|
2938
|
-
async function pickPrimary() {
|
|
2939
|
-
return new Promise((resolve) => {
|
|
2940
|
-
const app = render4(
|
|
2941
|
-
React9.createElement(PrimaryPicker, {
|
|
2942
|
-
onPick: (m) => {
|
|
2943
|
-
app.unmount();
|
|
2944
|
-
resolve(m);
|
|
2945
|
-
}
|
|
2946
|
-
})
|
|
2947
|
-
);
|
|
2948
|
-
});
|
|
2949
|
-
}
|
|
2950
3296
|
|
|
2951
3297
|
// src/commands/end.ts
|
|
2952
3298
|
import * as readline3 from "readline/promises";
|
|
@@ -3671,16 +4017,16 @@ function runNpmUpgrade(spawnImpl) {
|
|
|
3671
4017
|
}
|
|
3672
4018
|
|
|
3673
4019
|
// src/commands/roll.ts
|
|
3674
|
-
import
|
|
4020
|
+
import React7 from "react";
|
|
3675
4021
|
import { render as render6 } from "ink";
|
|
3676
4022
|
|
|
3677
4023
|
// src/ui/RollHorsePicker.tsx
|
|
3678
|
-
import { useState as
|
|
3679
|
-
import { Box as
|
|
3680
|
-
import { jsx as
|
|
4024
|
+
import { useState as useState5 } from "react";
|
|
4025
|
+
import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
|
|
4026
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3681
4027
|
function RollHorsePicker({ horses, onPick, onCancel }) {
|
|
3682
|
-
const [idx, setIdx] =
|
|
3683
|
-
|
|
4028
|
+
const [idx, setIdx] = useState5(0);
|
|
4029
|
+
useInput3((input, key) => {
|
|
3684
4030
|
if (key.escape) {
|
|
3685
4031
|
onCancel();
|
|
3686
4032
|
return;
|
|
@@ -3699,61 +4045,61 @@ function RollHorsePicker({ horses, onPick, onCancel }) {
|
|
|
3699
4045
|
return;
|
|
3700
4046
|
}
|
|
3701
4047
|
});
|
|
3702
|
-
return /* @__PURE__ */
|
|
3703
|
-
/* @__PURE__ */
|
|
3704
|
-
horses.map((h, i) => /* @__PURE__ */
|
|
3705
|
-
/* @__PURE__ */
|
|
4048
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
|
|
4049
|
+
/* @__PURE__ */ jsx8(Text8, { children: "Pick a horse to roll for:" }),
|
|
4050
|
+
horses.map((h, i) => /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
|
|
4051
|
+
/* @__PURE__ */ jsx8(Box8, { flexDirection: "row", children: /* @__PURE__ */ jsxs6(Text8, { children: [
|
|
3706
4052
|
i === idx ? "\u25BA" : " ",
|
|
3707
4053
|
" ",
|
|
3708
4054
|
h.name,
|
|
3709
4055
|
" ",
|
|
3710
|
-
/* @__PURE__ */
|
|
4056
|
+
/* @__PURE__ */ jsxs6(Text8, { color: "cyan", children: [
|
|
3711
4057
|
"[Lvl. ",
|
|
3712
4058
|
levelFromXp(h.xp),
|
|
3713
4059
|
"]"
|
|
3714
4060
|
] }),
|
|
3715
4061
|
" ",
|
|
3716
|
-
/* @__PURE__ */
|
|
4062
|
+
/* @__PURE__ */ jsxs6(Text8, { color: "yellow", children: [
|
|
3717
4063
|
"\u2014 ",
|
|
3718
4064
|
h.pending,
|
|
3719
4065
|
" roll",
|
|
3720
4066
|
h.pending === 1 ? "" : "s"
|
|
3721
4067
|
] })
|
|
3722
4068
|
] }) }),
|
|
3723
|
-
/* @__PURE__ */
|
|
3724
|
-
/* @__PURE__ */
|
|
3725
|
-
/* @__PURE__ */
|
|
4069
|
+
/* @__PURE__ */ jsxs6(Box8, { flexDirection: "row", children: [
|
|
4070
|
+
/* @__PURE__ */ jsx8(Text8, { children: " " }),
|
|
4071
|
+
/* @__PURE__ */ jsx8(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
|
|
3726
4072
|
] })
|
|
3727
4073
|
] }, h.stable_horse_id)),
|
|
3728
|
-
/* @__PURE__ */
|
|
4074
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
|
|
3729
4075
|
] });
|
|
3730
4076
|
}
|
|
3731
4077
|
|
|
3732
4078
|
// src/ui/reveal.ts
|
|
3733
|
-
import
|
|
4079
|
+
import React6 from "react";
|
|
3734
4080
|
import { render as render5 } from "ink";
|
|
3735
4081
|
|
|
3736
4082
|
// src/ui/RollReveal.tsx
|
|
3737
|
-
import { useState as
|
|
3738
|
-
import { Box as
|
|
4083
|
+
import { useState as useState7, useEffect as useEffect4, useMemo } from "react";
|
|
4084
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
3739
4085
|
|
|
3740
4086
|
// src/ui/HatSprite.tsx
|
|
3741
|
-
import { useEffect as useEffect3, useState as
|
|
3742
|
-
import { Box as
|
|
3743
|
-
import { jsx as
|
|
4087
|
+
import { useEffect as useEffect3, useState as useState6 } from "react";
|
|
4088
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
4089
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
3744
4090
|
function HatSprite({ hat, variant, centerIn }) {
|
|
3745
4091
|
const colors = hatColors(hat, variant ?? 0);
|
|
3746
4092
|
const grid = makeHatGrid(hat, colors, centerIn);
|
|
3747
4093
|
const lines = hexGridToHalfBlocks(grid);
|
|
3748
|
-
return /* @__PURE__ */
|
|
4094
|
+
return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx9(Text9, { children: line }, i)) });
|
|
3749
4095
|
}
|
|
3750
4096
|
function AnimatedHatSprite({ hat, variant, centerIn }) {
|
|
3751
4097
|
if (!isAnimatedHat(hat)) {
|
|
3752
|
-
return /* @__PURE__ */
|
|
4098
|
+
return /* @__PURE__ */ jsx9(HatSprite, { hat, variant, centerIn });
|
|
3753
4099
|
}
|
|
3754
4100
|
const frames = hat.animation.frames;
|
|
3755
4101
|
const fps = hat.animation.fps;
|
|
3756
|
-
const [idx, setIdx] =
|
|
4102
|
+
const [idx, setIdx] = useState6(0);
|
|
3757
4103
|
useEffect3(() => {
|
|
3758
4104
|
if (frames.length <= 1) return;
|
|
3759
4105
|
const interval = setInterval(
|
|
@@ -3763,7 +4109,7 @@ function AnimatedHatSprite({ hat, variant, centerIn }) {
|
|
|
3763
4109
|
return () => clearInterval(interval);
|
|
3764
4110
|
}, [frames.length, fps]);
|
|
3765
4111
|
const framed = { ...hat, colors: { ...hat.colors, A: frames[idx] } };
|
|
3766
|
-
return /* @__PURE__ */
|
|
4112
|
+
return /* @__PURE__ */ jsx9(HatSprite, { hat: framed, variant, centerIn });
|
|
3767
4113
|
}
|
|
3768
4114
|
function makeHatGrid(hat, colors, centerIn) {
|
|
3769
4115
|
const w = centerIn?.w ?? hat.width;
|
|
@@ -3786,7 +4132,7 @@ function makeHatGrid(hat, colors, centerIn) {
|
|
|
3786
4132
|
}
|
|
3787
4133
|
|
|
3788
4134
|
// src/ui/RollReveal.tsx
|
|
3789
|
-
import { jsx as
|
|
4135
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
3790
4136
|
var RESET3 = "\x1B[0m";
|
|
3791
4137
|
var BOX_COLOR = "#E5C76B";
|
|
3792
4138
|
var TIER_PALETTE = {
|
|
@@ -3856,13 +4202,13 @@ var BOX_EMPTY = [
|
|
|
3856
4202
|
""
|
|
3857
4203
|
].map(pad);
|
|
3858
4204
|
function GiftBox({ frame, color }) {
|
|
3859
|
-
return /* @__PURE__ */
|
|
4205
|
+
return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx10(Text10, { children: line ? ansiFg(color) + line + RESET3 : line }, i)) });
|
|
3860
4206
|
}
|
|
3861
|
-
function spawnParticles(tier,
|
|
4207
|
+
function spawnParticles(tier, count2, cx, cy) {
|
|
3862
4208
|
const palette = TIER_PALETTE[tier];
|
|
3863
4209
|
const out = [];
|
|
3864
|
-
for (let i = 0; i <
|
|
3865
|
-
const angle = i /
|
|
4210
|
+
for (let i = 0; i < count2; i++) {
|
|
4211
|
+
const angle = i / count2 * Math.PI * 2 + (Math.random() - 0.5) * 0.6;
|
|
3866
4212
|
const speed = 0.8 + Math.random() * 1.6;
|
|
3867
4213
|
out.push({
|
|
3868
4214
|
x: cx,
|
|
@@ -3879,7 +4225,7 @@ function ConfettiBurst({ tier }) {
|
|
|
3879
4225
|
const cx = Math.floor(SCENE_W / 2);
|
|
3880
4226
|
const cy = Math.floor(SCENE_H / 2);
|
|
3881
4227
|
const particles = useMemo(() => spawnParticles(tier, 36, cx, cy), [tier, cx, cy]);
|
|
3882
|
-
const [tick, setTick] =
|
|
4228
|
+
const [tick, setTick] = useState7(0);
|
|
3883
4229
|
useEffect4(() => {
|
|
3884
4230
|
const i = setInterval(() => setTick((t) => t + 1), 70);
|
|
3885
4231
|
return () => clearInterval(i);
|
|
@@ -3892,13 +4238,13 @@ function ConfettiBurst({ tier }) {
|
|
|
3892
4238
|
grid[y][x] = ansiFg(p.color) + p.char + RESET3;
|
|
3893
4239
|
}
|
|
3894
4240
|
}
|
|
3895
|
-
return /* @__PURE__ */
|
|
4241
|
+
return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: grid.map((row, y) => /* @__PURE__ */ jsx10(Text10, { children: row.join("") }, y)) });
|
|
3896
4242
|
}
|
|
3897
4243
|
var CLOSED_HOLD_MS = 3e3;
|
|
3898
4244
|
function RollReveal({ outcome, onDone }) {
|
|
3899
4245
|
const isNoHat = outcome.kind === "no_hat";
|
|
3900
4246
|
const isShowpiece = outcome.kind !== "no_hat" && (outcome.hat.rarity === "legendary" || outcome.hat.rarity === "limited");
|
|
3901
|
-
const [phase, setPhase] =
|
|
4247
|
+
const [phase, setPhase] = useState7("closed");
|
|
3902
4248
|
useEffect4(() => {
|
|
3903
4249
|
const timers = [];
|
|
3904
4250
|
timers.push(setTimeout(() => setPhase("open1"), CLOSED_HOLD_MS));
|
|
@@ -3913,21 +4259,21 @@ function RollReveal({ outcome, onDone }) {
|
|
|
3913
4259
|
}
|
|
3914
4260
|
return () => timers.forEach(clearTimeout);
|
|
3915
4261
|
}, [isNoHat, isShowpiece, onDone]);
|
|
3916
|
-
if (phase === "closed") return /* @__PURE__ */
|
|
3917
|
-
if (phase === "open1") return /* @__PURE__ */
|
|
3918
|
-
if (phase === "open2") return /* @__PURE__ */
|
|
3919
|
-
if (phase === "empty") return /* @__PURE__ */
|
|
4262
|
+
if (phase === "closed") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_CLOSED, color: BOX_COLOR });
|
|
4263
|
+
if (phase === "open1") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_1, color: BOX_COLOR });
|
|
4264
|
+
if (phase === "open2") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_2, color: BOX_COLOR });
|
|
4265
|
+
if (phase === "empty") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
|
|
3920
4266
|
if (phase === "burst" && outcome.kind !== "no_hat") {
|
|
3921
|
-
return /* @__PURE__ */
|
|
4267
|
+
return /* @__PURE__ */ jsx10(ConfettiBurst, { tier: outcome.hat.rarity });
|
|
3922
4268
|
}
|
|
3923
|
-
if (outcome.kind === "no_hat") return /* @__PURE__ */
|
|
3924
|
-
return isAnimatedHat(outcome.hat) ? /* @__PURE__ */
|
|
4269
|
+
if (outcome.kind === "no_hat") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
|
|
4270
|
+
return isAnimatedHat(outcome.hat) ? /* @__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 } });
|
|
3925
4271
|
}
|
|
3926
4272
|
|
|
3927
4273
|
// src/ui/reveal.ts
|
|
3928
4274
|
async function runReveal(outcome) {
|
|
3929
4275
|
await new Promise((resolve) => {
|
|
3930
|
-
const app = render5(
|
|
4276
|
+
const app = render5(React6.createElement(RollReveal, {
|
|
3931
4277
|
outcome,
|
|
3932
4278
|
onDone: () => {
|
|
3933
4279
|
app.unmount();
|
|
@@ -3943,7 +4289,7 @@ function pendingFor(horse) {
|
|
|
3943
4289
|
const lastRolled = horse.last_rolled_level ?? Math.max(1, level - 1);
|
|
3944
4290
|
return level - lastRolled;
|
|
3945
4291
|
}
|
|
3946
|
-
async function rollCommand() {
|
|
4292
|
+
async function rollCommand(args = []) {
|
|
3947
4293
|
let stable;
|
|
3948
4294
|
try {
|
|
3949
4295
|
stable = await listStable();
|
|
@@ -3959,19 +4305,39 @@ async function rollCommand() {
|
|
|
3959
4305
|
console.log("No rolls available. Level up a horse to earn a roll!");
|
|
3960
4306
|
return 0;
|
|
3961
4307
|
}
|
|
3962
|
-
const
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
onPick: (h) => {
|
|
3966
|
-
app.unmount();
|
|
3967
|
-
resolve(h);
|
|
3968
|
-
},
|
|
3969
|
-
onCancel: () => {
|
|
3970
|
-
app.unmount();
|
|
3971
|
-
resolve(null);
|
|
3972
|
-
}
|
|
3973
|
-
}));
|
|
4308
|
+
const choice = await resolveHorse(eligible, {
|
|
4309
|
+
name: parseFlag(args, "--horse"),
|
|
4310
|
+
autoSelect: false
|
|
3974
4311
|
});
|
|
4312
|
+
if (choice.kind === "not_found") {
|
|
4313
|
+
console.error(`No horse named "${choice.name}" has a roll available.`);
|
|
4314
|
+
console.error(`With rolls: ${eligible.map((h) => h.name).join(", ")}`);
|
|
4315
|
+
return 1;
|
|
4316
|
+
}
|
|
4317
|
+
if (choice.kind === "no_tty") {
|
|
4318
|
+
console.error(noTtyMessage("token-derby roll"));
|
|
4319
|
+
return 1;
|
|
4320
|
+
}
|
|
4321
|
+
let picked;
|
|
4322
|
+
if (choice.kind === "resolved") {
|
|
4323
|
+
picked = choice.horse;
|
|
4324
|
+
const notice = noticeFor(choice);
|
|
4325
|
+
if (notice) console.log(notice);
|
|
4326
|
+
} else {
|
|
4327
|
+
picked = await new Promise((resolve) => {
|
|
4328
|
+
const app = render6(React7.createElement(RollHorsePicker, {
|
|
4329
|
+
horses: eligible,
|
|
4330
|
+
onPick: (h) => {
|
|
4331
|
+
app.unmount();
|
|
4332
|
+
resolve(h);
|
|
4333
|
+
},
|
|
4334
|
+
onCancel: () => {
|
|
4335
|
+
app.unmount();
|
|
4336
|
+
resolve(null);
|
|
4337
|
+
}
|
|
4338
|
+
}));
|
|
4339
|
+
});
|
|
4340
|
+
}
|
|
3975
4341
|
if (!picked) {
|
|
3976
4342
|
console.log("Cancelled.");
|
|
3977
4343
|
return 0;
|
|
@@ -4016,7 +4382,9 @@ async function rollCommand() {
|
|
|
4016
4382
|
console.log(`
|
|
4017
4383
|
\u2728 ${hat.name}${variantSuffix} [${hat.rarity.toUpperCase()}]
|
|
4018
4384
|
`);
|
|
4019
|
-
if (
|
|
4385
|
+
if (!interactive()) {
|
|
4386
|
+
console.log(`Not equipped \u2014 no terminal to confirm. Equip it with: token-derby stable edit "${chosen.name}"`);
|
|
4387
|
+
} else if (await promptYesNo("Equip now? [Y/n] ")) {
|
|
4020
4388
|
try {
|
|
4021
4389
|
await equipHat(chosen.stable_horse_id, { hat_index: result.hat_index });
|
|
4022
4390
|
console.log(`Equipped on ${chosen.name}.`);
|
|
@@ -4038,21 +4406,25 @@ No hat this time. +${result.xp_awarded} XP toward your next level.
|
|
|
4038
4406
|
`);
|
|
4039
4407
|
}
|
|
4040
4408
|
if (result.remaining_rolls <= 0) return 0;
|
|
4409
|
+
if (!interactive()) {
|
|
4410
|
+
console.log(`${result.remaining_rolls} more roll${result.remaining_rolls === 1 ? "" : "s"} available. Run again to spend another.`);
|
|
4411
|
+
return 0;
|
|
4412
|
+
}
|
|
4041
4413
|
if (!await promptYesNo(`${result.remaining_rolls} more roll${result.remaining_rolls === 1 ? "" : "s"} available. Roll again? [Y/n] `)) return 0;
|
|
4042
4414
|
}
|
|
4043
4415
|
}
|
|
4044
4416
|
|
|
4045
4417
|
// src/commands/claim.ts
|
|
4046
|
-
import
|
|
4418
|
+
import React8 from "react";
|
|
4047
4419
|
import { render as render7 } from "ink";
|
|
4048
|
-
async function claimCommand(token) {
|
|
4420
|
+
async function claimCommand(token, args = []) {
|
|
4049
4421
|
if (!token) {
|
|
4050
4422
|
console.error("Usage: token-derby claim <token>");
|
|
4051
4423
|
return 2;
|
|
4052
4424
|
}
|
|
4053
|
-
let
|
|
4425
|
+
let probe2;
|
|
4054
4426
|
try {
|
|
4055
|
-
|
|
4427
|
+
probe2 = await probeClaim(token);
|
|
4056
4428
|
} catch (e) {
|
|
4057
4429
|
if (e instanceof ApiError) {
|
|
4058
4430
|
console.error(`Error: ${e.code} ${e.message}`);
|
|
@@ -4074,23 +4446,44 @@ async function claimCommand(token) {
|
|
|
4074
4446
|
console.error("No horses in your stable. Run `token-derby stable create` first.");
|
|
4075
4447
|
return 1;
|
|
4076
4448
|
}
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
const picked = await new Promise((resolve) => {
|
|
4081
|
-
const app = render7(React15.createElement(HorsePicker, {
|
|
4082
|
-
horses: stable.horses,
|
|
4083
|
-
prompt: "Which horse should receive it?",
|
|
4084
|
-
onPick: (h) => {
|
|
4085
|
-
app.unmount();
|
|
4086
|
-
resolve(h);
|
|
4087
|
-
},
|
|
4088
|
-
onCancel: () => {
|
|
4089
|
-
app.unmount();
|
|
4090
|
-
resolve(null);
|
|
4091
|
-
}
|
|
4092
|
-
}));
|
|
4449
|
+
const choice = await resolveHorse(stable.horses, {
|
|
4450
|
+
name: parseFlag(args, "--horse"),
|
|
4451
|
+
pick: hasFlag(args, "--pick")
|
|
4093
4452
|
});
|
|
4453
|
+
if (choice.kind === "not_found") {
|
|
4454
|
+
console.error(`No horse named "${choice.name}" in your stable.`);
|
|
4455
|
+
console.error(`Your stable: ${stable.horses.map((h) => h.name).join(", ")}`);
|
|
4456
|
+
return 1;
|
|
4457
|
+
}
|
|
4458
|
+
if (choice.kind === "no_tty") {
|
|
4459
|
+
console.error(noTtyMessage("token-derby claim"));
|
|
4460
|
+
console.error("Your token is unspent.");
|
|
4461
|
+
return 1;
|
|
4462
|
+
}
|
|
4463
|
+
console.log(probe2.entry_count > 1 ? `
|
|
4464
|
+
\u{1F381} A pack of ${probe2.entry_count} cosmetics \u2014 one of them will be yours.
|
|
4465
|
+
` : "\n\u{1F381} A cosmetic has been awarded to you.\n");
|
|
4466
|
+
let picked;
|
|
4467
|
+
if (choice.kind === "resolved") {
|
|
4468
|
+
picked = choice.horse;
|
|
4469
|
+
const notice = noticeFor(choice);
|
|
4470
|
+
if (notice) console.log(notice + "\n");
|
|
4471
|
+
} else {
|
|
4472
|
+
picked = await new Promise((resolve) => {
|
|
4473
|
+
const app = render7(React8.createElement(HorsePicker, {
|
|
4474
|
+
horses: stable.horses,
|
|
4475
|
+
prompt: "Which horse should receive it?",
|
|
4476
|
+
onPick: (h) => {
|
|
4477
|
+
app.unmount();
|
|
4478
|
+
resolve(h);
|
|
4479
|
+
},
|
|
4480
|
+
onCancel: () => {
|
|
4481
|
+
app.unmount();
|
|
4482
|
+
resolve(null);
|
|
4483
|
+
}
|
|
4484
|
+
}));
|
|
4485
|
+
});
|
|
4486
|
+
}
|
|
4094
4487
|
if (!picked) {
|
|
4095
4488
|
console.log("Cancelled. Your token is unspent.");
|
|
4096
4489
|
return 0;
|
|
@@ -4124,7 +4517,9 @@ async function claimCommand(token) {
|
|
|
4124
4517
|
console.log(`
|
|
4125
4518
|
\u2728 ${hat2.name}${variantSuffix2} [${hat2.rarity.toUpperCase()}]
|
|
4126
4519
|
`);
|
|
4127
|
-
if (
|
|
4520
|
+
if (!interactive()) {
|
|
4521
|
+
console.log(`Not equipped \u2014 no terminal to confirm. Equip it with: token-derby stable edit "${picked.name}"`);
|
|
4522
|
+
} else if (await promptYesNo("Equip now? [Y/n] ")) {
|
|
4128
4523
|
try {
|
|
4129
4524
|
await equipHat(picked.stable_horse_id, { hat_index: result.hat_index });
|
|
4130
4525
|
console.log(`Equipped on ${picked.name}.`);
|
|
@@ -4144,6 +4539,121 @@ ${picked.name} already has ${hat?.name ?? result.hat_id}${variantSuffix}. +${res
|
|
|
4144
4539
|
return 0;
|
|
4145
4540
|
}
|
|
4146
4541
|
|
|
4542
|
+
// src/commands/stable-default.ts
|
|
4543
|
+
async function stableDefaultCommand(args) {
|
|
4544
|
+
const clear = args.includes("--clear");
|
|
4545
|
+
const name = args.find((a) => !a.startsWith("--"));
|
|
4546
|
+
if (clear && name !== void 0) {
|
|
4547
|
+
console.error("Pass a name or --clear, not both.");
|
|
4548
|
+
return 2;
|
|
4549
|
+
}
|
|
4550
|
+
if (clear) {
|
|
4551
|
+
await clearDefaultHorse();
|
|
4552
|
+
console.log("Default horse cleared.");
|
|
4553
|
+
return 0;
|
|
4554
|
+
}
|
|
4555
|
+
let horses;
|
|
4556
|
+
try {
|
|
4557
|
+
horses = (await listStable()).horses;
|
|
4558
|
+
} catch (e) {
|
|
4559
|
+
if (e instanceof ApiError) {
|
|
4560
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
4561
|
+
return 1;
|
|
4562
|
+
}
|
|
4563
|
+
throw e;
|
|
4564
|
+
}
|
|
4565
|
+
if (name === void 0) return showCurrent(horses);
|
|
4566
|
+
const found = horses.find((h) => h.name === name);
|
|
4567
|
+
if (!found) {
|
|
4568
|
+
console.error(`No horse named "${name}" in your stable.`);
|
|
4569
|
+
if (horses.length > 0) console.error(`Your stable: ${horses.map((h) => h.name).join(", ")}`);
|
|
4570
|
+
return 1;
|
|
4571
|
+
}
|
|
4572
|
+
await setDefaultHorse(found.stable_horse_id);
|
|
4573
|
+
console.log(`Default horse set: ${describeHorse(found)}`);
|
|
4574
|
+
return 0;
|
|
4575
|
+
}
|
|
4576
|
+
async function showCurrent(horses) {
|
|
4577
|
+
const { default_stable_horse_id } = await loadPrefs();
|
|
4578
|
+
if (default_stable_horse_id === void 0) {
|
|
4579
|
+
console.log("No default horse set.");
|
|
4580
|
+
console.log("Set one with: token-derby stable default <name>");
|
|
4581
|
+
return 0;
|
|
4582
|
+
}
|
|
4583
|
+
const found = horses.find((h) => h.stable_horse_id === default_stable_horse_id);
|
|
4584
|
+
if (!found) {
|
|
4585
|
+
console.log("Your default horse is no longer in your stable \u2014 it will be ignored.");
|
|
4586
|
+
console.log("Set a new one with: token-derby stable default <name>");
|
|
4587
|
+
return 0;
|
|
4588
|
+
}
|
|
4589
|
+
console.log(`Default horse: ${describeHorse(found)}`);
|
|
4590
|
+
return 0;
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
// src/commands/harness.ts
|
|
4594
|
+
function isHarnessKey(v) {
|
|
4595
|
+
return HARNESS_KEYS.includes(v);
|
|
4596
|
+
}
|
|
4597
|
+
function listValidIds() {
|
|
4598
|
+
return HARNESS_KEYS.join(", ");
|
|
4599
|
+
}
|
|
4600
|
+
async function harnessListCommand() {
|
|
4601
|
+
const prefs = await loadPrefs();
|
|
4602
|
+
const rows = await Promise.all(HARNESS_KEYS.map(async (key) => {
|
|
4603
|
+
const p = await probe(HARNESSES[key]);
|
|
4604
|
+
return {
|
|
4605
|
+
key,
|
|
4606
|
+
label: HARNESSES[key].label,
|
|
4607
|
+
on: isHarnessEnabled(prefs, key),
|
|
4608
|
+
chosen: prefs.harnesses?.[key] !== void 0,
|
|
4609
|
+
byDefault: HARNESSES[key].enabledByDefault,
|
|
4610
|
+
found: p.transcripts,
|
|
4611
|
+
dir: p.dir
|
|
4612
|
+
};
|
|
4613
|
+
}));
|
|
4614
|
+
const width = Math.max(...rows.map((r) => r.key.length));
|
|
4615
|
+
console.log("");
|
|
4616
|
+
for (const r of rows) {
|
|
4617
|
+
const state = r.on ? "on " : "off";
|
|
4618
|
+
const why = r.chosen ? "" : r.byDefault ? " (default)" : " (off by default \u2014 enable to count it)";
|
|
4619
|
+
const found = r.found === 1 ? "1 transcript" : `${r.found} transcripts`;
|
|
4620
|
+
console.log(` ${state} ${r.key.padEnd(width)} ${r.label}${why}`);
|
|
4621
|
+
console.log(` ${" ".repeat(width)} ${r.dir} (${found})`);
|
|
4622
|
+
}
|
|
4623
|
+
console.log("");
|
|
4624
|
+
console.log(" token-derby harness disable <id> stop counting one");
|
|
4625
|
+
console.log(" token-derby harness enable <id> start counting it again");
|
|
4626
|
+
console.log("");
|
|
4627
|
+
return 0;
|
|
4628
|
+
}
|
|
4629
|
+
async function harnessToggleCommand(id, enabled) {
|
|
4630
|
+
const verb = enabled ? "enable" : "disable";
|
|
4631
|
+
if (!id) {
|
|
4632
|
+
console.error(`Usage: token-derby harness ${verb} <id>`);
|
|
4633
|
+
console.error(`Available: ${listValidIds()}`);
|
|
4634
|
+
return 2;
|
|
4635
|
+
}
|
|
4636
|
+
if (!isHarnessKey(id)) {
|
|
4637
|
+
console.error(`Unknown coding agent '${id}'.`);
|
|
4638
|
+
console.error(`Available: ${listValidIds()}`);
|
|
4639
|
+
return 2;
|
|
4640
|
+
}
|
|
4641
|
+
const before = await loadPrefs();
|
|
4642
|
+
if (isHarnessEnabled(before, id) === enabled) {
|
|
4643
|
+
console.log(`${HARNESSES[id].label} is already ${enabled ? "counted" : "turned off"}.`);
|
|
4644
|
+
return 0;
|
|
4645
|
+
}
|
|
4646
|
+
await setHarnessEnabled(id, enabled);
|
|
4647
|
+
const remaining = enabledHarnesses(await loadPrefs());
|
|
4648
|
+
console.log(`${HARNESSES[id].label} is now ${enabled ? "counted" : "turned off"}.`);
|
|
4649
|
+
console.log("Takes effect on your next heartbeat \u2014 no need to rejoin.");
|
|
4650
|
+
if (remaining.length === 0) {
|
|
4651
|
+
console.log("");
|
|
4652
|
+
console.log("\u26A0 Every coding agent is now off, so your horse will not move.");
|
|
4653
|
+
}
|
|
4654
|
+
return 0;
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4147
4657
|
// src/commands/org-join.ts
|
|
4148
4658
|
async function orgJoinCommand(token) {
|
|
4149
4659
|
const join_token = token?.trim();
|
|
@@ -4191,7 +4701,7 @@ function envCommand(arg) {
|
|
|
4191
4701
|
}
|
|
4192
4702
|
|
|
4193
4703
|
// src/commands/logs.ts
|
|
4194
|
-
import * as
|
|
4704
|
+
import * as fs11 from "fs/promises";
|
|
4195
4705
|
import { existsSync as existsSync2 } from "fs";
|
|
4196
4706
|
var DEFAULT_TAIL_LINES = 50;
|
|
4197
4707
|
function tailCount(argv) {
|
|
@@ -4209,7 +4719,7 @@ async function logsCommand(argv) {
|
|
|
4209
4719
|
console.log(file);
|
|
4210
4720
|
const n = tailCount(argv);
|
|
4211
4721
|
if (n === null) return 0;
|
|
4212
|
-
const lines = (await
|
|
4722
|
+
const lines = (await fs11.readFile(file, "utf8")).split("\n").filter((l) => l.length > 0);
|
|
4213
4723
|
console.log("");
|
|
4214
4724
|
for (const line of lines.slice(-n)) console.log(line);
|
|
4215
4725
|
return 0;
|
|
@@ -4246,11 +4756,20 @@ Maintenance:
|
|
|
4246
4756
|
token-derby logs Show the path of the debug log
|
|
4247
4757
|
token-derby logs --tail [n] Print the last n log lines (default 50)
|
|
4248
4758
|
|
|
4759
|
+
Coding agents:
|
|
4760
|
+
token-derby harness list Show which agents this machine counts,
|
|
4761
|
+
and whether they have anything to count
|
|
4762
|
+
token-derby harness disable <id> Stop counting one (takes effect next heartbeat)
|
|
4763
|
+
token-derby harness enable <id> Start counting it again
|
|
4764
|
+
|
|
4249
4765
|
Stable management:
|
|
4250
4766
|
token-derby stable create Make a new horse (interactive)
|
|
4251
4767
|
token-derby stable list Show your saved horses
|
|
4252
4768
|
token-derby stable edit [name] Edit an existing horse's colors (interactive picker if no name)
|
|
4253
4769
|
token-derby stable delete <name> Remove a horse from your stable
|
|
4770
|
+
token-derby stable default [name] Show, set, or (--clear) unset the horse that
|
|
4771
|
+
claim/join/stable edit use when none is named.
|
|
4772
|
+
A stable of one is used automatically.
|
|
4254
4773
|
|
|
4255
4774
|
Organisations:
|
|
4256
4775
|
token-derby organisation join [token] Join an organisation with a join token,
|
|
@@ -4264,13 +4783,17 @@ Races:
|
|
|
4264
4783
|
Create a new race (interactive). When
|
|
4265
4784
|
--organisation is set, only members of
|
|
4266
4785
|
that org can join.
|
|
4267
|
-
token-derby join <join-code>
|
|
4786
|
+
token-derby join <join-code> [--horse <name>|--pick]
|
|
4787
|
+
Join (or resume) a race
|
|
4268
4788
|
token-derby end <admin-code> End a race early
|
|
4269
4789
|
|
|
4270
4790
|
Cosmetics:
|
|
4271
|
-
token-derby roll
|
|
4272
|
-
Earn rolls by leveling up horses.
|
|
4273
|
-
|
|
4791
|
+
token-derby roll [--horse <name>] Spend a pending roll to try for a hat.
|
|
4792
|
+
Earn rolls by leveling up horses. The picker
|
|
4793
|
+
is a confirmation step, so it is shown unless
|
|
4794
|
+
--horse names the horse outright.
|
|
4795
|
+
token-derby claim <token> [--horse <name>|--pick]
|
|
4796
|
+
Redeem a claim token for a cosmetic
|
|
4274
4797
|
awarded to you by an admin.
|
|
4275
4798
|
|
|
4276
4799
|
Environment:
|
|
@@ -4283,7 +4806,7 @@ Environment:
|
|
|
4283
4806
|
`;
|
|
4284
4807
|
function describeInvocation(argv) {
|
|
4285
4808
|
const cmd = argv[0] ?? "(none)";
|
|
4286
|
-
const container = cmd === "stable" || cmd === "organisation" || cmd === "org";
|
|
4809
|
+
const container = cmd === "stable" || cmd === "organisation" || cmd === "org" || cmd === "harness";
|
|
4287
4810
|
return {
|
|
4288
4811
|
cmd,
|
|
4289
4812
|
sub: container ? argv[1] : void 0,
|
|
@@ -4316,6 +4839,15 @@ async function main() {
|
|
|
4316
4839
|
if (cmd === "update") return updateCommand();
|
|
4317
4840
|
if (cmd === "env") return envCommand(argv[1]);
|
|
4318
4841
|
if (cmd === "logs") return logsCommand(argv.slice(1));
|
|
4842
|
+
if (cmd === "harness") {
|
|
4843
|
+
const sub = argv[1];
|
|
4844
|
+
if (sub === void 0 || sub === "list") return harnessListCommand();
|
|
4845
|
+
if (sub === "enable") return harnessToggleCommand(argv[2], true);
|
|
4846
|
+
if (sub === "disable") return harnessToggleCommand(argv[2], false);
|
|
4847
|
+
console.error(`Unknown harness subcommand: ${sub}`);
|
|
4848
|
+
console.error("Try: harness list | harness enable <id> | harness disable <id>");
|
|
4849
|
+
return 2;
|
|
4850
|
+
}
|
|
4319
4851
|
const identity = await loadIdentity();
|
|
4320
4852
|
if (!identity) {
|
|
4321
4853
|
console.error("Run `token-derby login` to set up your identity before using any other command.");
|
|
@@ -4327,8 +4859,9 @@ async function main() {
|
|
|
4327
4859
|
if (sub === "list") return stableListCommand();
|
|
4328
4860
|
if (sub === "edit") return stableEditCommand(argv[2]);
|
|
4329
4861
|
if (sub === "delete") return stableDeleteCommand(argv[2]);
|
|
4862
|
+
if (sub === "default") return stableDefaultCommand(argv.slice(2));
|
|
4330
4863
|
console.error(`Unknown stable subcommand: ${sub ?? "(none)"}`);
|
|
4331
|
-
console.error("Try: stable create | stable list | stable edit <name> | stable delete <name>");
|
|
4864
|
+
console.error("Try: stable create | stable list | stable edit <name> | stable delete <name> | stable default [<name>|--clear]");
|
|
4332
4865
|
return 2;
|
|
4333
4866
|
}
|
|
4334
4867
|
if (cmd === "organisation" || cmd === "org") {
|
|
@@ -4347,21 +4880,13 @@ async function main() {
|
|
|
4347
4880
|
if (cmd === "whoami") return whoamiCommand();
|
|
4348
4881
|
if (cmd === "join") return joinCommand(argv[1], argv.slice(2));
|
|
4349
4882
|
if (cmd === "end") return endCommand(argv[1]);
|
|
4350
|
-
if (cmd === "roll") return rollCommand();
|
|
4351
|
-
if (cmd === "claim") return claimCommand(argv[1]);
|
|
4883
|
+
if (cmd === "roll") return rollCommand(argv.slice(1));
|
|
4884
|
+
if (cmd === "claim") return claimCommand(argv[1], argv.slice(2));
|
|
4352
4885
|
if (cmd === "web") return webCommand();
|
|
4353
4886
|
console.error(`Unknown command: ${cmd}`);
|
|
4354
4887
|
console.error(HELP);
|
|
4355
4888
|
return 2;
|
|
4356
4889
|
}
|
|
4357
|
-
function parseFlag(args, flag) {
|
|
4358
|
-
for (let i = 0; i < args.length; i++) {
|
|
4359
|
-
if (args[i] === flag) return args[i + 1];
|
|
4360
|
-
const eq = `${flag}=`;
|
|
4361
|
-
if (args[i]?.startsWith(eq)) return args[i].slice(eq.length);
|
|
4362
|
-
}
|
|
4363
|
-
return void 0;
|
|
4364
|
-
}
|
|
4365
4890
|
var CRASH_HANDLERS_INSTALLED = /* @__PURE__ */ Symbol.for("token-derby.crash-handlers");
|
|
4366
4891
|
if (!(CRASH_HANDLERS_INSTALLED in process)) {
|
|
4367
4892
|
process[CRASH_HANDLERS_INSTALLED] = true;
|