@mauricode/token-derby 3.1.2 → 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 +69 -8
- package/dist/bin.js +1634 -928
- 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,15 @@ 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
|
+
}
|
|
982
|
+
function logDir() {
|
|
983
|
+
return path2.join(homeDir(), "logs");
|
|
984
|
+
}
|
|
985
|
+
function logFile() {
|
|
986
|
+
return path2.join(logDir(), "token-derby.log");
|
|
987
|
+
}
|
|
913
988
|
|
|
914
989
|
// src/identity/identity.ts
|
|
915
990
|
async function readIdentityFile() {
|
|
@@ -964,6 +1039,63 @@ function validateDisplayName(name) {
|
|
|
964
1039
|
return { ok: true, name: trimmed };
|
|
965
1040
|
}
|
|
966
1041
|
|
|
1042
|
+
// src/log/logger.ts
|
|
1043
|
+
import * as fs2 from "fs";
|
|
1044
|
+
var SECRET_KEY = /token|secret|authorization|credential|password/i;
|
|
1045
|
+
function redact(fields) {
|
|
1046
|
+
const out = {};
|
|
1047
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
1048
|
+
out[key] = SECRET_KEY.test(key) ? "[redacted]" : value;
|
|
1049
|
+
}
|
|
1050
|
+
return out;
|
|
1051
|
+
}
|
|
1052
|
+
function formatLine(at, level, event, fields) {
|
|
1053
|
+
const body = fields && Object.keys(fields).length > 0 ? ` ${JSON.stringify(redact(fields))}` : "";
|
|
1054
|
+
return `${at.toISOString()} ${level.padEnd(5)} ${event}${body}
|
|
1055
|
+
`;
|
|
1056
|
+
}
|
|
1057
|
+
var MAX_BYTES = 2e6;
|
|
1058
|
+
var MAX_FILES = 5;
|
|
1059
|
+
var currentBytes = null;
|
|
1060
|
+
var disabled = false;
|
|
1061
|
+
function rotate() {
|
|
1062
|
+
const base = logFile();
|
|
1063
|
+
fs2.rmSync(`${base}.${MAX_FILES - 1}`, { force: true });
|
|
1064
|
+
for (let i = MAX_FILES - 2; i >= 1; i--) {
|
|
1065
|
+
if (fs2.existsSync(`${base}.${i}`)) fs2.renameSync(`${base}.${i}`, `${base}.${i + 1}`);
|
|
1066
|
+
}
|
|
1067
|
+
if (fs2.existsSync(base)) fs2.renameSync(base, `${base}.1`);
|
|
1068
|
+
currentBytes = 0;
|
|
1069
|
+
}
|
|
1070
|
+
function write(level, event, fields) {
|
|
1071
|
+
if (disabled) return;
|
|
1072
|
+
try {
|
|
1073
|
+
append(level, event, fields);
|
|
1074
|
+
} catch {
|
|
1075
|
+
disabled = true;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function append(level, event, fields) {
|
|
1079
|
+
const line = formatLine(/* @__PURE__ */ new Date(), level, event, fields);
|
|
1080
|
+
const bytes = Buffer.byteLength(line);
|
|
1081
|
+
fs2.mkdirSync(logDir(), { recursive: true });
|
|
1082
|
+
if (currentBytes === null) {
|
|
1083
|
+
currentBytes = fs2.existsSync(logFile()) ? fs2.statSync(logFile()).size : 0;
|
|
1084
|
+
}
|
|
1085
|
+
if (currentBytes > 0 && currentBytes + bytes > MAX_BYTES) rotate();
|
|
1086
|
+
fs2.appendFileSync(logFile(), line, "utf8");
|
|
1087
|
+
currentBytes += bytes;
|
|
1088
|
+
}
|
|
1089
|
+
function logInfo(event, fields) {
|
|
1090
|
+
write("INFO", event, fields);
|
|
1091
|
+
}
|
|
1092
|
+
function logWarn(event, fields) {
|
|
1093
|
+
write("WARN", event, fields);
|
|
1094
|
+
}
|
|
1095
|
+
function logError(event, fields) {
|
|
1096
|
+
write("ERROR", event, fields);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
967
1099
|
// src/api/client.ts
|
|
968
1100
|
var ApiError = class extends Error {
|
|
969
1101
|
constructor(code, message, status) {
|
|
@@ -975,6 +1107,10 @@ var ApiError = class extends Error {
|
|
|
975
1107
|
code;
|
|
976
1108
|
status;
|
|
977
1109
|
};
|
|
1110
|
+
var SECRET_SEGMENT = /^\/(claims|races\/admin)\/[^/]+/;
|
|
1111
|
+
function loggablePath(path9) {
|
|
1112
|
+
return path9.replace(SECRET_SEGMENT, (match) => `${match.slice(0, match.lastIndexOf("/") + 1)}[redacted]`);
|
|
1113
|
+
}
|
|
978
1114
|
var identityCache = null;
|
|
979
1115
|
function getIdentity() {
|
|
980
1116
|
if (!identityCache) identityCache = loadIdentity();
|
|
@@ -995,6 +1131,9 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
|
|
|
995
1131
|
}
|
|
996
1132
|
if (horseAuthToken) headers["authorization"] = `Bearer ${horseAuthToken}`;
|
|
997
1133
|
if (body !== void 0) headers["content-type"] = "application/json";
|
|
1134
|
+
const safePath = loggablePath(path9);
|
|
1135
|
+
logInfo("http.req", { method, path: safePath });
|
|
1136
|
+
const startedAt2 = Date.now();
|
|
998
1137
|
let res;
|
|
999
1138
|
try {
|
|
1000
1139
|
res = await fetchImpl(url, {
|
|
@@ -1003,8 +1142,10 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
|
|
|
1003
1142
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1004
1143
|
});
|
|
1005
1144
|
} catch (e) {
|
|
1145
|
+
logError("http.err", { method, path: safePath, ms: Date.now() - startedAt2, message: e?.message ?? "fetch failed" });
|
|
1006
1146
|
throw new ApiError("NETWORK_ERROR", e?.message ?? "fetch failed", 0);
|
|
1007
1147
|
}
|
|
1148
|
+
logInfo("http.res", { method, path: safePath, status: res.status, ms: Date.now() - startedAt2 });
|
|
1008
1149
|
const text = await res.text();
|
|
1009
1150
|
const contentType = res.headers.get("content-type") ?? "";
|
|
1010
1151
|
let parsed = null;
|
|
@@ -1021,6 +1162,14 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
|
|
|
1021
1162
|
}
|
|
1022
1163
|
throw new ApiError("NETWORK_ERROR", `HTTP ${res.status}`, res.status);
|
|
1023
1164
|
}
|
|
1165
|
+
if (parsed === null) {
|
|
1166
|
+
const got = contentType || "no content-type";
|
|
1167
|
+
throw new ApiError(
|
|
1168
|
+
"NETWORK_ERROR",
|
|
1169
|
+
`Expected JSON from ${method} ${safePath} but the server returned ${got} (HTTP ${res.status}). The API may be unavailable or the request was rejected upstream.`,
|
|
1170
|
+
res.status
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1024
1173
|
return parsed;
|
|
1025
1174
|
}
|
|
1026
1175
|
|
|
@@ -1124,7 +1273,7 @@ function logoutDevice() {
|
|
|
1124
1273
|
async function stableCreateCommand() {
|
|
1125
1274
|
let exitCode = 0;
|
|
1126
1275
|
const app = render(
|
|
1127
|
-
|
|
1276
|
+
React2.createElement(HorseCreator, {
|
|
1128
1277
|
onSubmit: async (name, colors) => {
|
|
1129
1278
|
try {
|
|
1130
1279
|
await createStableHorse({ name, colors });
|
|
@@ -1156,7 +1305,7 @@ async function stableCreateCommand() {
|
|
|
1156
1305
|
}
|
|
1157
1306
|
|
|
1158
1307
|
// src/commands/stable-list.tsx
|
|
1159
|
-
import
|
|
1308
|
+
import React3 from "react";
|
|
1160
1309
|
import { render as render2, Box as Box4, Text as Text4 } from "ink";
|
|
1161
1310
|
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1162
1311
|
async function stableListCommand() {
|
|
@@ -1176,13 +1325,13 @@ async function stableListCommand() {
|
|
|
1176
1325
|
return 0;
|
|
1177
1326
|
}
|
|
1178
1327
|
const app = render2(
|
|
1179
|
-
|
|
1328
|
+
React3.createElement(StableList, { horses })
|
|
1180
1329
|
);
|
|
1181
1330
|
await app.waitUntilExit();
|
|
1182
1331
|
return 0;
|
|
1183
1332
|
}
|
|
1184
1333
|
function StableList({ horses }) {
|
|
1185
|
-
|
|
1334
|
+
React3.useEffect(() => {
|
|
1186
1335
|
setImmediate(() => process.exit(0));
|
|
1187
1336
|
}, []);
|
|
1188
1337
|
return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", children: [
|
|
@@ -1251,7 +1400,7 @@ async function stableDeleteCommand(name) {
|
|
|
1251
1400
|
}
|
|
1252
1401
|
|
|
1253
1402
|
// src/commands/stable-edit.ts
|
|
1254
|
-
import
|
|
1403
|
+
import React4 from "react";
|
|
1255
1404
|
import { render as render3 } from "ink";
|
|
1256
1405
|
|
|
1257
1406
|
// src/ui/HorsePicker.tsx
|
|
@@ -1308,121 +1457,699 @@ function HorsePicker({ horses, onPick, onCancel, prompt = "Pick a horse to race:
|
|
|
1308
1457
|
] });
|
|
1309
1458
|
}
|
|
1310
1459
|
|
|
1311
|
-
// src/
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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";
|
|
1323
1473
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1474
|
+
dir;
|
|
1475
|
+
};
|
|
1476
|
+
async function readRoot(dir, read) {
|
|
1477
|
+
try {
|
|
1478
|
+
return await read();
|
|
1479
|
+
} catch (e) {
|
|
1480
|
+
if (e?.code === "ENOENT") throw new SourceRootMissing(dir);
|
|
1481
|
+
throw e;
|
|
1327
1482
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
return;
|
|
1359
|
-
}
|
|
1360
|
-
throw e;
|
|
1361
|
-
}
|
|
1362
|
-
},
|
|
1363
|
-
onCancel: () => {
|
|
1364
|
-
app.unmount();
|
|
1365
|
-
console.log("Cancelled.");
|
|
1366
|
-
exitCode = 1;
|
|
1483
|
+
}
|
|
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 } });
|
|
1497
|
+
}
|
|
1498
|
+
|
|
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;
|
|
1367
1513
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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);
|
|
1521
|
+
}
|
|
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;
|
|
1372
1527
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
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);
|
|
1546
|
+
}
|
|
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);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1375
1569
|
}
|
|
1376
|
-
async function
|
|
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);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
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;
|
|
1377
1629
|
try {
|
|
1378
|
-
|
|
1379
|
-
return resp.horses;
|
|
1630
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1380
1631
|
} catch (e) {
|
|
1381
|
-
if (e
|
|
1382
|
-
console.error(`Error: ${e.code} ${e.message}`);
|
|
1383
|
-
return null;
|
|
1384
|
-
}
|
|
1632
|
+
if (e?.code === "ENOENT") return [];
|
|
1385
1633
|
throw e;
|
|
1386
1634
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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);
|
|
1392
1640
|
}
|
|
1393
|
-
|
|
1394
|
-
const picked = await new Promise((resolve) => {
|
|
1395
|
-
const app = render3(
|
|
1396
|
-
React6.createElement(HorsePicker, {
|
|
1397
|
-
horses,
|
|
1398
|
-
onPick: (h) => {
|
|
1399
|
-
app.unmount();
|
|
1400
|
-
resolve(h);
|
|
1401
|
-
},
|
|
1402
|
-
onCancel: () => {
|
|
1403
|
-
app.unmount();
|
|
1404
|
-
resolve(null);
|
|
1405
|
-
}
|
|
1406
|
-
})
|
|
1407
|
-
);
|
|
1408
|
-
});
|
|
1409
|
-
return picked ?? "cancelled";
|
|
1641
|
+
return out;
|
|
1410
1642
|
}
|
|
1411
1643
|
|
|
1412
|
-
// src/
|
|
1413
|
-
import * as
|
|
1414
|
-
import
|
|
1415
|
-
var
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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
|
+
}
|
|
1423
1676
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
+
}
|
|
1731
|
+
});
|
|
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();
|
|
1426
2153
|
if (!isIso(start)) {
|
|
1427
2154
|
console.error("Invalid start time.");
|
|
1428
2155
|
return 1;
|
|
@@ -1450,12 +2177,14 @@ async function createRaceCommand(organisationName) {
|
|
|
1450
2177
|
console.error("Organisation name must be 1\u201312 alphanumeric characters.");
|
|
1451
2178
|
return 1;
|
|
1452
2179
|
}
|
|
1453
|
-
const
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
+
}
|
|
1459
2188
|
const resp = await createRace({
|
|
1460
2189
|
name,
|
|
1461
2190
|
start_time: start,
|
|
@@ -1463,9 +2192,7 @@ async function createRaceCommand(organisationName) {
|
|
|
1463
2192
|
tz,
|
|
1464
2193
|
...max !== void 0 ? { max_participants: max } : {},
|
|
1465
2194
|
...org ? { organisation_name: org } : {},
|
|
1466
|
-
...
|
|
1467
|
-
...primary_top5 ? { primary_top5: true } : {},
|
|
1468
|
-
...stamina ? { stamina: true } : {}
|
|
2195
|
+
...modifiers.length > 0 ? { modifiers } : {}
|
|
1469
2196
|
});
|
|
1470
2197
|
console.log("");
|
|
1471
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");
|
|
@@ -1478,14 +2205,8 @@ async function createRaceCommand(organisationName) {
|
|
|
1478
2205
|
if (org) {
|
|
1479
2206
|
console.log(` Restricted to organisation: ${org}`);
|
|
1480
2207
|
}
|
|
1481
|
-
|
|
1482
|
-
console.log(
|
|
1483
|
-
}
|
|
1484
|
-
if (primary_top5) {
|
|
1485
|
-
console.log(" Primary score counts only each racer's top 5 conversations per beat.");
|
|
1486
|
-
}
|
|
1487
|
-
if (stamina) {
|
|
1488
|
-
console.log(" Stamina on \u2014 horses running above a sustainable pace will tire.");
|
|
2208
|
+
for (const id of modifiers) {
|
|
2209
|
+
console.log(` ${MODIFIERS[id].label} on \u2014 ${MODIFIERS[id].description}`);
|
|
1489
2210
|
}
|
|
1490
2211
|
console.log(` Share with participants: token-derby join ${resp.join_code}`);
|
|
1491
2212
|
return 0;
|
|
@@ -1506,37 +2227,28 @@ function isIso(s) {
|
|
|
1506
2227
|
}
|
|
1507
2228
|
|
|
1508
2229
|
// src/commands/join.ts
|
|
1509
|
-
import
|
|
2230
|
+
import React5 from "react";
|
|
1510
2231
|
import { render as render4 } from "ink";
|
|
1511
2232
|
|
|
1512
|
-
// src/
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
});
|
|
1524
|
-
return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
|
|
1525
|
-
/* @__PURE__ */ jsx6(Text6, { bold: true, children: "Pick your primary model for this race (counts 1:1; the others count at 50%)." }),
|
|
1526
|
-
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "This is locked for the whole race \u2014 you can't change it, even by rejoining." }),
|
|
1527
|
-
MODEL_KEYS.map((m, idx) => /* @__PURE__ */ jsxs4(Text6, { color: idx === i ? "cyan" : void 0, children: [
|
|
1528
|
-
idx === i ? "\u276F " : " ",
|
|
1529
|
-
LABELS[m]
|
|
1530
|
-
] }, m))
|
|
1531
|
-
] });
|
|
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}=`));
|
|
1532
2244
|
}
|
|
1533
2245
|
|
|
1534
2246
|
// src/stable/active-race.ts
|
|
1535
|
-
import * as
|
|
1536
|
-
import * as
|
|
2247
|
+
import * as fs8 from "fs/promises";
|
|
2248
|
+
import * as path7 from "path";
|
|
1537
2249
|
async function saveActiveRace(active) {
|
|
1538
|
-
await
|
|
1539
|
-
await
|
|
2250
|
+
await fs8.mkdir(activeRacesDir(), { recursive: true });
|
|
2251
|
+
await fs8.writeFile(
|
|
1540
2252
|
activeRaceFile(active.join_code),
|
|
1541
2253
|
JSON.stringify(active, null, 2) + "\n",
|
|
1542
2254
|
"utf8"
|
|
@@ -1544,29 +2256,30 @@ async function saveActiveRace(active) {
|
|
|
1544
2256
|
}
|
|
1545
2257
|
|
|
1546
2258
|
// src/runtime/run-race.tsx
|
|
1547
|
-
import { useEffect as useEffect2, useRef, useState as
|
|
1548
|
-
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";
|
|
1549
2261
|
|
|
1550
2262
|
// src/ui/StatusScreen.tsx
|
|
1551
|
-
import { Box as
|
|
1552
|
-
import { Fragment as Fragment2, jsx as
|
|
1553
|
-
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";
|
|
1554
2265
|
function ModelList(props) {
|
|
1555
|
-
const
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
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: [
|
|
1560
2270
|
i > 0 ? " \xB7 " : "",
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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)" })
|
|
1564
2277
|
] }) });
|
|
1565
2278
|
}
|
|
1566
2279
|
function StatusScreen(props) {
|
|
1567
|
-
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;
|
|
1568
2281
|
if (!race) {
|
|
1569
|
-
return /* @__PURE__ */
|
|
2282
|
+
return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: /* @__PURE__ */ jsx6(Text6, { children: "Joining race\u2026" }) });
|
|
1570
2283
|
}
|
|
1571
2284
|
const own = race.horses.find((h) => h.horse_id === ownHorseId);
|
|
1572
2285
|
const leader = race.horses[0];
|
|
@@ -1592,36 +2305,36 @@ function StatusScreen(props) {
|
|
|
1592
2305
|
},
|
|
1593
2306
|
{
|
|
1594
2307
|
label: "Last heartbeat:",
|
|
1595
|
-
value: /* @__PURE__ */
|
|
2308
|
+
value: /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
1596
2309
|
lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
|
|
1597
2310
|
" ",
|
|
1598
|
-
/* @__PURE__ */
|
|
2311
|
+
/* @__PURE__ */ jsx6(Text6, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
|
|
1599
2312
|
] })
|
|
1600
2313
|
}
|
|
1601
2314
|
];
|
|
1602
|
-
return /* @__PURE__ */
|
|
1603
|
-
/* @__PURE__ */
|
|
2315
|
+
return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
2316
|
+
/* @__PURE__ */ jsxs4(Text6, { children: [
|
|
1604
2317
|
"\u{1F3C7} TOKEN DERBY \u2500\u2500\u2500 ",
|
|
1605
|
-
/* @__PURE__ */
|
|
2318
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, children: race.name }),
|
|
1606
2319
|
" \u2500\u2500\u2500 status: ",
|
|
1607
|
-
/* @__PURE__ */
|
|
2320
|
+
/* @__PURE__ */ jsx6(Text6, { color: statusColor(race.status), children: race.status })
|
|
1608
2321
|
] }),
|
|
1609
|
-
/* @__PURE__ */
|
|
1610
|
-
/* @__PURE__ */
|
|
1611
|
-
/* @__PURE__ */
|
|
1612
|
-
/* @__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: [
|
|
1613
2326
|
" ",
|
|
1614
2327
|
ownHorseName,
|
|
1615
2328
|
" ",
|
|
1616
|
-
/* @__PURE__ */
|
|
2329
|
+
/* @__PURE__ */ jsxs4(Text6, { color: "cyan", children: [
|
|
1617
2330
|
"[Lvl. ",
|
|
1618
2331
|
lvl.level,
|
|
1619
2332
|
"]"
|
|
1620
2333
|
] })
|
|
1621
2334
|
] }),
|
|
1622
|
-
/* @__PURE__ */
|
|
2335
|
+
/* @__PURE__ */ jsxs4(Text6, { children: [
|
|
1623
2336
|
" ",
|
|
1624
|
-
/* @__PURE__ */
|
|
2337
|
+
/* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
|
|
1625
2338
|
"(",
|
|
1626
2339
|
ownUserName,
|
|
1627
2340
|
")"
|
|
@@ -1629,30 +2342,39 @@ function StatusScreen(props) {
|
|
|
1629
2342
|
] })
|
|
1630
2343
|
] })
|
|
1631
2344
|
] }),
|
|
1632
|
-
/* @__PURE__ */
|
|
1633
|
-
/* @__PURE__ */
|
|
1634
|
-
stalled && /* @__PURE__ */
|
|
2345
|
+
/* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", marginTop: 1, children: [
|
|
2346
|
+
/* @__PURE__ */ jsx6(StatLines, { rows }),
|
|
2347
|
+
stalled && /* @__PURE__ */ jsxs4(Text6, { color: "yellow", children: [
|
|
1635
2348
|
"\u26A0 ",
|
|
1636
2349
|
stallReason ?? "Can't read token usage",
|
|
1637
2350
|
". Your race continues."
|
|
1638
2351
|
] }),
|
|
1639
|
-
!stalled &&
|
|
1640
|
-
"\u26A0
|
|
1641
|
-
|
|
1642
|
-
"
|
|
1643
|
-
|
|
1644
|
-
"
|
|
1645
|
-
|
|
1646
|
-
"
|
|
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."
|
|
1647
2369
|
] })
|
|
1648
2370
|
] }),
|
|
1649
|
-
|
|
1650
|
-
/* @__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." }) })
|
|
1651
2373
|
] });
|
|
1652
2374
|
}
|
|
1653
2375
|
function StatLines(props) {
|
|
1654
2376
|
const width = Math.max(...props.rows.map((r) => r.label.length)) + 1;
|
|
1655
|
-
return /* @__PURE__ */
|
|
2377
|
+
return /* @__PURE__ */ jsx6(Fragment2, { children: props.rows.map((r) => /* @__PURE__ */ jsxs4(Text6, { children: [
|
|
1656
2378
|
r.label.padEnd(width),
|
|
1657
2379
|
r.value
|
|
1658
2380
|
] }, r.label)) });
|
|
@@ -1674,17 +2396,17 @@ function bar(pct, width) {
|
|
|
1674
2396
|
return "\u2593".repeat(filled) + "\u2591".repeat(width - filled);
|
|
1675
2397
|
}
|
|
1676
2398
|
function staminaLine(own, race) {
|
|
1677
|
-
const
|
|
2399
|
+
const stamina2 = staminaOf(own ?? {});
|
|
1678
2400
|
const cfg = resolveStaminaConfig(race);
|
|
1679
2401
|
const floor = cfg.taper_floor;
|
|
1680
|
-
const band =
|
|
2402
|
+
const band = stamina2 > 50 ? "green" : stamina2 >= floor ? "amber" : "red";
|
|
1681
2403
|
const color = band === "green" ? "green" : band === "amber" ? "yellow" : "red";
|
|
1682
|
-
const pct = Math.max(0, Math.min(1,
|
|
1683
|
-
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;
|
|
1684
2406
|
return {
|
|
1685
2407
|
label: "Stamina:",
|
|
1686
|
-
value: /* @__PURE__ */
|
|
1687
|
-
`${Math.round(
|
|
2408
|
+
value: /* @__PURE__ */ jsxs4(Text6, { color, children: [
|
|
2409
|
+
`${Math.round(stamina2)}% ${bar(pct, 20)}`,
|
|
1688
2410
|
multiplier !== null ? ` \xD7${multiplier.toFixed(2)}` : ""
|
|
1689
2411
|
] })
|
|
1690
2412
|
};
|
|
@@ -1709,6 +2431,7 @@ function runHeartbeatLoop(opts) {
|
|
|
1709
2431
|
let stopped = false;
|
|
1710
2432
|
let pending = null;
|
|
1711
2433
|
const stop = () => {
|
|
2434
|
+
if (!stopped) logInfo("beat.stop", { retry: retryIndex });
|
|
1712
2435
|
stopped = true;
|
|
1713
2436
|
if (timer) clearTimeout(timer);
|
|
1714
2437
|
timer = null;
|
|
@@ -1721,9 +2444,16 @@ function runHeartbeatLoop(opts) {
|
|
|
1721
2444
|
const tick = async () => {
|
|
1722
2445
|
if (stopped) return;
|
|
1723
2446
|
try {
|
|
1724
|
-
if (!pending)
|
|
2447
|
+
if (!pending) {
|
|
2448
|
+
logInfo("beat.prepare.start");
|
|
2449
|
+
const startedAt2 = Date.now();
|
|
2450
|
+
pending = await opts.prepareBeat();
|
|
2451
|
+
logInfo("beat.prepare.done", { seq: pending.seq, ms: Date.now() - startedAt2 });
|
|
2452
|
+
}
|
|
1725
2453
|
const snapshot = pending;
|
|
2454
|
+
const sentAt = Date.now();
|
|
1726
2455
|
const resp = await opts.sendBeat(snapshot);
|
|
2456
|
+
logInfo("beat.send.ok", { seq: snapshot.seq, ms: Date.now() - sentAt, race_status: resp.race_status });
|
|
1727
2457
|
pending = null;
|
|
1728
2458
|
retryIndex = 0;
|
|
1729
2459
|
opts.onSuccess(resp, snapshot);
|
|
@@ -1736,6 +2466,13 @@ function runHeartbeatLoop(opts) {
|
|
|
1736
2466
|
} catch (err) {
|
|
1737
2467
|
opts.onError(err);
|
|
1738
2468
|
const delay = opts.retryDelaysMs[Math.min(retryIndex, opts.retryDelaysMs.length - 1)] ?? 1e3;
|
|
2469
|
+
logError("beat.send.err", {
|
|
2470
|
+
seq: pending?.seq,
|
|
2471
|
+
code: err?.code,
|
|
2472
|
+
message: err?.message ?? String(err),
|
|
2473
|
+
retry: retryIndex,
|
|
2474
|
+
next_ms: delay
|
|
2475
|
+
});
|
|
1739
2476
|
retryIndex += 1;
|
|
1740
2477
|
schedule(delay);
|
|
1741
2478
|
}
|
|
@@ -1743,30 +2480,13 @@ function runHeartbeatLoop(opts) {
|
|
|
1743
2480
|
schedule(0);
|
|
1744
2481
|
}
|
|
1745
2482
|
|
|
1746
|
-
// src/tokens/
|
|
1747
|
-
import * as
|
|
1748
|
-
import * as path6 from "path";
|
|
1749
|
-
|
|
1750
|
-
// src/tokens/pool.ts
|
|
1751
|
-
var SCAN_CONCURRENCY = 12;
|
|
1752
|
-
async function mapWithConcurrency(items, limit, fn) {
|
|
1753
|
-
const out = new Array(items.length);
|
|
1754
|
-
let next = 0;
|
|
1755
|
-
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1756
|
-
while (true) {
|
|
1757
|
-
const i = next++;
|
|
1758
|
-
if (i >= items.length) return;
|
|
1759
|
-
out[i] = await fn(items[i], i);
|
|
1760
|
-
}
|
|
1761
|
-
});
|
|
1762
|
-
await Promise.all(workers);
|
|
1763
|
-
return out;
|
|
1764
|
-
}
|
|
2483
|
+
// src/tokens/harnesses/engine.ts
|
|
2484
|
+
import * as fs10 from "fs/promises";
|
|
1765
2485
|
|
|
1766
2486
|
// src/tokens/scan-cache.ts
|
|
1767
|
-
import * as
|
|
1768
|
-
import * as
|
|
1769
|
-
var CACHE_VERSION =
|
|
2487
|
+
import * as fs9 from "fs/promises";
|
|
2488
|
+
import * as path8 from "path";
|
|
2489
|
+
var CACHE_VERSION = 2;
|
|
1770
2490
|
function isEntry(v) {
|
|
1771
2491
|
const e = v;
|
|
1772
2492
|
return !!e && typeof e.mtimeMs === "number" && typeof e.size === "number" && typeof e.offset === "number";
|
|
@@ -1802,7 +2522,7 @@ var ScanCache = class _ScanCache {
|
|
|
1802
2522
|
* re-reading that line once the writer completes it.
|
|
1803
2523
|
*/
|
|
1804
2524
|
async readIncremental(file, fold) {
|
|
1805
|
-
const st = await
|
|
2525
|
+
const st = await fs9.stat(file);
|
|
1806
2526
|
const prev = this.entries.get(file);
|
|
1807
2527
|
this.touched.add(file);
|
|
1808
2528
|
if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
|
|
@@ -1819,11 +2539,11 @@ var ScanCache = class _ScanCache {
|
|
|
1819
2539
|
* chats). Gated on mtime+size, recomputed in full whenever either moves.
|
|
1820
2540
|
*/
|
|
1821
2541
|
async readWhenChanged(file, compute) {
|
|
1822
|
-
const st = await
|
|
2542
|
+
const st = await fs9.stat(file);
|
|
1823
2543
|
const prev = this.entries.get(file);
|
|
1824
2544
|
this.touched.add(file);
|
|
1825
2545
|
if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
|
|
1826
|
-
const value = await compute(await
|
|
2546
|
+
const value = await compute(await fs9.readFile(file, "utf8"));
|
|
1827
2547
|
this.entries.set(file, { mtimeMs: st.mtimeMs, size: st.size, offset: st.size, value });
|
|
1828
2548
|
return value;
|
|
1829
2549
|
}
|
|
@@ -1835,20 +2555,20 @@ var ScanCache = class _ScanCache {
|
|
|
1835
2555
|
const target = cacheFile(this.source);
|
|
1836
2556
|
const tmp = `${target}.tmp`;
|
|
1837
2557
|
try {
|
|
1838
|
-
await
|
|
1839
|
-
await
|
|
1840
|
-
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);
|
|
1841
2561
|
} catch {
|
|
1842
2562
|
}
|
|
1843
2563
|
}
|
|
1844
2564
|
};
|
|
1845
2565
|
function cacheFile(source) {
|
|
1846
|
-
return
|
|
2566
|
+
return path8.join(homeDir(), "scan-cache", `${source}.json`);
|
|
1847
2567
|
}
|
|
1848
2568
|
async function loadEntries(source) {
|
|
1849
2569
|
let parsed;
|
|
1850
2570
|
try {
|
|
1851
|
-
parsed = JSON.parse(await
|
|
2571
|
+
parsed = JSON.parse(await fs9.readFile(cacheFile(source), "utf8"));
|
|
1852
2572
|
} catch {
|
|
1853
2573
|
return /* @__PURE__ */ new Map();
|
|
1854
2574
|
}
|
|
@@ -1863,7 +2583,7 @@ async function loadEntries(source) {
|
|
|
1863
2583
|
}
|
|
1864
2584
|
async function readCompleteLines(file, start, end) {
|
|
1865
2585
|
if (end <= start) return { lines: [], tail: null, consumedTo: start };
|
|
1866
|
-
const fh = await
|
|
2586
|
+
const fh = await fs9.open(file, "r");
|
|
1867
2587
|
try {
|
|
1868
2588
|
const buf = Buffer.allocUnsafe(end - start);
|
|
1869
2589
|
const { bytesRead } = await fh.read(buf, 0, end - start, start);
|
|
@@ -1878,269 +2598,67 @@ async function readCompleteLines(file, start, end) {
|
|
|
1878
2598
|
tail: tail === "" ? null : tail,
|
|
1879
2599
|
consumedTo: start + lastNl + 1
|
|
1880
2600
|
};
|
|
1881
|
-
} finally {
|
|
1882
|
-
await fh.close();
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
// src/tokens/source-root.ts
|
|
1887
|
-
var SourceRootMissing = class extends Error {
|
|
1888
|
-
constructor(dir) {
|
|
1889
|
-
super(`No history directory at ${dir}`);
|
|
1890
|
-
this.dir = dir;
|
|
1891
|
-
this.name = "SourceRootMissing";
|
|
1892
|
-
}
|
|
1893
|
-
dir;
|
|
1894
|
-
};
|
|
1895
|
-
async function readRoot(dir, read) {
|
|
1896
|
-
try {
|
|
1897
|
-
return await read();
|
|
1898
|
-
} catch (e) {
|
|
1899
|
-
if (e?.code === "ENOENT") throw new SourceRootMissing(dir);
|
|
1900
|
-
throw e;
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
// src/tokens/transcripts.ts
|
|
1905
|
-
var MAX_PROJECT_DEPTH = 8;
|
|
1906
|
-
function conversationId(file, root) {
|
|
1907
|
-
const rel = path6.relative(root, file);
|
|
1908
|
-
const [project, session] = rel.split(path6.sep);
|
|
1909
|
-
if (project === void 0 || session === void 0) return rel.replace(/\.jsonl$/, "");
|
|
1910
|
-
return `${project}/${session.replace(/\.jsonl$/, "")}`;
|
|
1911
|
-
}
|
|
1912
|
-
async function sumTokensByConversation() {
|
|
1913
|
-
const root = claudeProjectsDir();
|
|
1914
|
-
const files = await listJsonlFiles(root);
|
|
1915
|
-
const cache = await ScanCache.open("claude");
|
|
1916
|
-
const totals = await mapWithConcurrency(files, SCAN_CONCURRENCY, (f) => cache.readIncremental(f, CLAUDE_FOLD));
|
|
1917
|
-
await cache.save();
|
|
1918
|
-
const byConv = /* @__PURE__ */ new Map();
|
|
1919
|
-
files.forEach((file, i) => {
|
|
1920
|
-
const t = totals[i];
|
|
1921
|
-
const id = conversationId(file, root);
|
|
1922
|
-
const acc = byConv.get(id) ?? { input: 0, output: 0 };
|
|
1923
|
-
acc.input += t.input;
|
|
1924
|
-
acc.output += t.output;
|
|
1925
|
-
byConv.set(id, acc);
|
|
1926
|
-
});
|
|
1927
|
-
return byConv;
|
|
1928
|
-
}
|
|
1929
|
-
async function sumTokens() {
|
|
1930
|
-
const byConv = await sumTokensByConversation();
|
|
1931
|
-
let input = 0;
|
|
1932
|
-
let output = 0;
|
|
1933
|
-
for (const t of byConv.values()) {
|
|
1934
|
-
input += t.input;
|
|
1935
|
-
output += t.output;
|
|
1936
|
-
}
|
|
1937
|
-
return { input, output };
|
|
1938
|
-
}
|
|
1939
|
-
async function listJsonlFiles(root) {
|
|
1940
|
-
const entries = await readEntries(root, true);
|
|
1941
|
-
const out = [];
|
|
1942
|
-
for (const entry of entries) {
|
|
1943
|
-
if (!await isDirectory(entry, root)) continue;
|
|
1944
|
-
await collectJsonl(path6.join(root, entry.name), MAX_PROJECT_DEPTH, out);
|
|
1945
|
-
}
|
|
1946
|
-
return out;
|
|
1947
|
-
}
|
|
1948
|
-
async function collectJsonl(dir, depth, out) {
|
|
1949
|
-
if (depth <= 0) return;
|
|
1950
|
-
for (const entry of await readEntries(dir, false)) {
|
|
1951
|
-
if (entry.name.endsWith(".jsonl")) {
|
|
1952
|
-
out.push(path6.join(dir, entry.name));
|
|
1953
|
-
} else if (depth > 1 && await isDirectory(entry, dir)) {
|
|
1954
|
-
await collectJsonl(path6.join(dir, entry.name), depth - 1, out);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
async function readEntries(dir, failLoud) {
|
|
1959
|
-
if (failLoud) return readRoot(dir, () => fs4.readdir(dir, { withFileTypes: true }));
|
|
1960
|
-
return fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1961
|
-
}
|
|
1962
|
-
async function isDirectory(entry, parent) {
|
|
1963
|
-
if (entry.isDirectory()) return true;
|
|
1964
|
-
if (!entry.isSymbolicLink()) return false;
|
|
1965
|
-
return fs4.stat(path6.join(parent, entry.name)).then((st) => st.isDirectory()).catch(() => false);
|
|
1966
|
-
}
|
|
1967
|
-
function addNum(value) {
|
|
1968
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1969
|
-
}
|
|
1970
|
-
var CLAUDE_FOLD = {
|
|
1971
|
-
empty: () => ({ input: 0, output: 0 }),
|
|
1972
|
-
append: (acc, lines) => {
|
|
1973
|
-
let { input, output } = acc;
|
|
1974
|
-
for (const line of lines) {
|
|
1975
|
-
if (!line.trim()) continue;
|
|
1976
|
-
let parsed;
|
|
1977
|
-
try {
|
|
1978
|
-
parsed = JSON.parse(line);
|
|
1979
|
-
} catch {
|
|
1980
|
-
continue;
|
|
1981
|
-
}
|
|
1982
|
-
const usage = parsed?.message?.usage;
|
|
1983
|
-
if (!usage) continue;
|
|
1984
|
-
input += addNum(usage.input_tokens) + addNum(usage.cache_creation_input_tokens);
|
|
1985
|
-
output += addNum(usage.output_tokens);
|
|
1986
|
-
}
|
|
1987
|
-
return { input, output };
|
|
1988
|
-
}
|
|
1989
|
-
};
|
|
1990
|
-
|
|
1991
|
-
// src/tokens/codex.ts
|
|
1992
|
-
import * as fs5 from "fs/promises";
|
|
1993
|
-
import * as path7 from "path";
|
|
1994
|
-
function num(v) {
|
|
1995
|
-
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1996
|
-
}
|
|
1997
|
-
async function sumCodexByConversation() {
|
|
1998
|
-
const root = codexSessionsDir();
|
|
1999
|
-
await readRoot(root, () => fs5.stat(root));
|
|
2000
|
-
const files = await listCodexRollouts(root);
|
|
2001
|
-
const cache = await ScanCache.open("codex");
|
|
2002
|
-
const totals = await mapWithConcurrency(
|
|
2003
|
-
files,
|
|
2004
|
-
SCAN_CONCURRENCY,
|
|
2005
|
-
(f) => cache.readIncremental(f, CODEX_FOLD).catch(() => ({ input: 0, output: 0 }))
|
|
2006
|
-
);
|
|
2007
|
-
await cache.save();
|
|
2008
|
-
const byConv = /* @__PURE__ */ new Map();
|
|
2009
|
-
files.forEach((file, i) => byConv.set(file, totals[i]));
|
|
2010
|
-
return byConv;
|
|
2011
|
-
}
|
|
2012
|
-
async function sumCodexTokens() {
|
|
2013
|
-
const byConv = await sumCodexByConversation();
|
|
2014
|
-
let input = 0;
|
|
2015
|
-
let output = 0;
|
|
2016
|
-
for (const t of byConv.values()) {
|
|
2017
|
-
input += t.input;
|
|
2018
|
-
output += t.output;
|
|
2019
|
-
}
|
|
2020
|
-
return { input, output };
|
|
2021
|
-
}
|
|
2022
|
-
async function listCodexRollouts(root) {
|
|
2023
|
-
return [
|
|
2024
|
-
...await collectRollouts(path7.join(root, "sessions")),
|
|
2025
|
-
...await collectRollouts(path7.join(root, "archived_sessions"))
|
|
2026
|
-
];
|
|
2027
|
-
}
|
|
2028
|
-
async function collectRollouts(dir) {
|
|
2029
|
-
let entries;
|
|
2030
|
-
try {
|
|
2031
|
-
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
2032
|
-
} catch (e) {
|
|
2033
|
-
if (e?.code === "ENOENT") return [];
|
|
2034
|
-
throw e;
|
|
2035
|
-
}
|
|
2036
|
-
const out = [];
|
|
2037
|
-
for (const entry of entries) {
|
|
2038
|
-
const full = path7.join(dir, entry.name);
|
|
2039
|
-
if (entry.isDirectory()) out.push(...await collectRollouts(full));
|
|
2040
|
-
else if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) out.push(full);
|
|
2041
|
-
}
|
|
2042
|
-
return out;
|
|
2043
|
-
}
|
|
2044
|
-
var CODEX_FOLD = {
|
|
2045
|
-
empty: () => ({ input: 0, output: 0 }),
|
|
2046
|
-
append: (acc, lines) => {
|
|
2047
|
-
let usage = null;
|
|
2048
|
-
for (const line of lines) {
|
|
2049
|
-
if (!line.trim()) continue;
|
|
2050
|
-
let parsed;
|
|
2051
|
-
try {
|
|
2052
|
-
parsed = JSON.parse(line);
|
|
2053
|
-
} catch {
|
|
2054
|
-
continue;
|
|
2055
|
-
}
|
|
2056
|
-
if (parsed?.payload?.type === "token_count" && parsed.payload.info?.total_token_usage) {
|
|
2057
|
-
usage = parsed.payload.info.total_token_usage;
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
if (!usage) return acc;
|
|
2061
|
-
return {
|
|
2062
|
-
input: Math.max(0, num(usage.input_tokens) - num(usage.cached_input_tokens)),
|
|
2063
|
-
output: num(usage.output_tokens)
|
|
2064
|
-
};
|
|
2065
|
-
}
|
|
2066
|
-
};
|
|
2067
|
-
|
|
2068
|
-
// src/tokens/gemini.ts
|
|
2069
|
-
import * as fs6 from "fs/promises";
|
|
2070
|
-
import * as path8 from "path";
|
|
2071
|
-
function num2(v) {
|
|
2072
|
-
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
2073
|
-
}
|
|
2074
|
-
async function sumGeminiByConversation() {
|
|
2075
|
-
const files = await listChatFiles(geminiTmpDir());
|
|
2076
|
-
const cache = await ScanCache.open("gemini");
|
|
2077
|
-
const totals = await mapWithConcurrency(
|
|
2078
|
-
files,
|
|
2079
|
-
SCAN_CONCURRENCY,
|
|
2080
|
-
(f) => cache.readWhenChanged(f, async (raw) => sumGeminiRaw(f, raw)).catch(() => ({ input: 0, output: 0 }))
|
|
2081
|
-
);
|
|
2082
|
-
await cache.save();
|
|
2083
|
-
const byConv = /* @__PURE__ */ new Map();
|
|
2084
|
-
files.forEach((file, i) => byConv.set(file, totals[i]));
|
|
2085
|
-
return byConv;
|
|
2086
|
-
}
|
|
2087
|
-
async function sumGeminiTokens() {
|
|
2088
|
-
const byConv = await sumGeminiByConversation();
|
|
2089
|
-
let input = 0;
|
|
2090
|
-
let output = 0;
|
|
2091
|
-
for (const t of byConv.values()) {
|
|
2092
|
-
input += t.input;
|
|
2093
|
-
output += t.output;
|
|
2094
|
-
}
|
|
2095
|
-
return { input, output };
|
|
2096
|
-
}
|
|
2097
|
-
async function listChatFiles(root) {
|
|
2098
|
-
const entries = await readRoot(root, () => fs6.readdir(root));
|
|
2099
|
-
const out = [];
|
|
2100
|
-
for (const entry of entries) {
|
|
2101
|
-
const chatsDir = path8.join(root, entry, "chats");
|
|
2102
|
-
let files;
|
|
2103
|
-
try {
|
|
2104
|
-
files = await fs6.readdir(chatsDir);
|
|
2105
|
-
} catch {
|
|
2106
|
-
continue;
|
|
2107
|
-
}
|
|
2108
|
-
for (const f of files) {
|
|
2109
|
-
if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(path8.join(chatsDir, f));
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
return out;
|
|
2113
|
-
}
|
|
2114
|
-
function sumGeminiRaw(file, raw) {
|
|
2115
|
-
const messages = file.endsWith(".jsonl") ? parseJsonl(raw) : parseJson(raw);
|
|
2116
|
-
let input = 0;
|
|
2117
|
-
let output = 0;
|
|
2118
|
-
for (const m of messages) {
|
|
2119
|
-
const tk = m?.tokens;
|
|
2120
|
-
if (!tk || typeof tk !== "object") continue;
|
|
2121
|
-
input += Math.max(0, num2(tk.input) - num2(tk.cached));
|
|
2122
|
-
output += num2(tk.output);
|
|
2123
|
-
}
|
|
2124
|
-
return { input, output };
|
|
2125
|
-
}
|
|
2126
|
-
function parseJson(raw) {
|
|
2127
|
-
try {
|
|
2128
|
-
const data = JSON.parse(raw);
|
|
2129
|
-
return Array.isArray(data?.messages) ? data.messages : [];
|
|
2130
|
-
} catch {
|
|
2131
|
-
return [];
|
|
2601
|
+
} finally {
|
|
2602
|
+
await fh.close();
|
|
2132
2603
|
}
|
|
2133
2604
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
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);
|
|
2141
2633
|
}
|
|
2142
|
-
}
|
|
2143
|
-
|
|
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 };
|
|
2144
2662
|
}
|
|
2145
2663
|
|
|
2146
2664
|
// src/tokens/race-tokens.ts
|
|
@@ -2153,60 +2671,70 @@ async function scanWithTimeout(scan, timeoutMs, describeTimeout) {
|
|
|
2153
2671
|
const budget = new Promise((resolve) => {
|
|
2154
2672
|
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
2155
2673
|
});
|
|
2674
|
+
const startedAt2 = Date.now();
|
|
2156
2675
|
try {
|
|
2157
2676
|
const result = await Promise.race([scan(), budget]);
|
|
2158
|
-
if (result !== TIMED_OUT)
|
|
2677
|
+
if (result !== TIMED_OUT) {
|
|
2678
|
+
if (isStall(result)) logWarn("scan.stall", { reason: result.stall, ms: Date.now() - startedAt2 });
|
|
2679
|
+
return result;
|
|
2680
|
+
}
|
|
2159
2681
|
const detail = describeTimeout ? await describeTimeout() : null;
|
|
2160
|
-
|
|
2682
|
+
const stall = detail ?? `Token scan timed out after ${Math.round(timeoutMs / 1e3)}s`;
|
|
2683
|
+
logWarn("scan.timeout", { budget_ms: timeoutMs, reason: stall });
|
|
2684
|
+
return { stall };
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
logError("scan.error", {
|
|
2687
|
+
message: err?.message ?? String(err),
|
|
2688
|
+
stack: err?.stack,
|
|
2689
|
+
ms: Date.now() - startedAt2
|
|
2690
|
+
});
|
|
2691
|
+
throw err;
|
|
2161
2692
|
} finally {
|
|
2162
2693
|
clearTimeout(timer);
|
|
2163
2694
|
}
|
|
2164
2695
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
return race.counts_input ? t.input + t.output : t.output;
|
|
2177
|
-
}
|
|
2178
|
-
async function readAllSources(race, primary, progress) {
|
|
2179
|
-
progress?.begin(primary);
|
|
2180
|
-
const primaryScan = BY_CONVERSATION_READERS[primary]().then(
|
|
2181
|
-
(map) => ({ ok: true, map }),
|
|
2182
|
-
(err) => ({ ok: false, err })
|
|
2183
|
-
).finally(() => progress?.end(primary));
|
|
2184
|
-
const secondaryKeys = MODEL_KEYS.filter((k) => k !== primary);
|
|
2185
|
-
const secondaryScans = secondaryKeys.map((k) => {
|
|
2186
|
-
progress?.begin(k);
|
|
2187
|
-
return SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch(() => 0).finally(() => progress?.end(k));
|
|
2188
|
-
});
|
|
2189
|
-
const [primaryResult, secondaryValues] = await Promise.all([
|
|
2190
|
-
primaryScan,
|
|
2191
|
-
Promise.all(secondaryScans)
|
|
2192
|
-
]);
|
|
2193
|
-
const primaryByConv = /* @__PURE__ */ new Map();
|
|
2194
|
-
if (primaryResult.ok) {
|
|
2195
|
-
for (const [id, totals] of primaryResult.map) primaryByConv.set(id, scoreFor(race, totals));
|
|
2196
|
-
} else if (!(primaryResult.err instanceof SourceRootMissing)) {
|
|
2197
|
-
const err = primaryResult.err;
|
|
2198
|
-
return { stall: `Can't read ${primary} token usage: ${err?.message ?? String(err)}` };
|
|
2199
|
-
}
|
|
2200
|
-
const secondary = { claude: 0, codex: 0, gemini: 0 };
|
|
2201
|
-
secondaryKeys.forEach((k, i) => {
|
|
2202
|
-
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));
|
|
2203
2707
|
});
|
|
2204
|
-
|
|
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
|
+
}
|
|
2205
2733
|
}
|
|
2206
2734
|
|
|
2207
2735
|
// src/tokens/scan-progress.ts
|
|
2208
2736
|
function skipVar(key) {
|
|
2209
|
-
return
|
|
2737
|
+
return HARNESSES[key].overrideVar;
|
|
2210
2738
|
}
|
|
2211
2739
|
function formatBytes(bytes) {
|
|
2212
2740
|
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
|
|
@@ -2240,45 +2768,30 @@ async function diagnoseScanTimeout(timeoutMs, progress) {
|
|
|
2240
2768
|
return describeScanTimeout(timeoutMs, outstanding);
|
|
2241
2769
|
}
|
|
2242
2770
|
|
|
2243
|
-
// src/tokens/primary-cap.ts
|
|
2244
|
-
var PRIMARY_TOP_CONVERSATIONS = 5;
|
|
2245
|
-
function primaryConversationCap(enabled) {
|
|
2246
|
-
return enabled ? PRIMARY_TOP_CONVERSATIONS : Infinity;
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
2771
|
// src/tokens/race-score.ts
|
|
2250
2772
|
var STALL_THRESHOLD = 5;
|
|
2251
|
-
function
|
|
2252
|
-
return {
|
|
2773
|
+
function cloneAnchors(a) {
|
|
2774
|
+
return { anthropic: { ...a.anthropic }, openai: { ...a.openai }, google: { ...a.google } };
|
|
2253
2775
|
}
|
|
2254
2776
|
var RaceScoreTracker = class {
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
primaryConvAcked;
|
|
2258
|
-
primaryConvLast;
|
|
2777
|
+
convAcked;
|
|
2778
|
+
convLast;
|
|
2259
2779
|
counted;
|
|
2260
2780
|
seq;
|
|
2261
2781
|
stalls = 0;
|
|
2262
2782
|
lastStall = null;
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
this.
|
|
2268
|
-
this.lastGood = { ...init.lastGood };
|
|
2269
|
-
this.primaryConvAcked = { ...init.primaryConvAcked };
|
|
2270
|
-
this.primaryConvLast = { ...init.primaryConvAcked };
|
|
2271
|
-
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 };
|
|
2272
2788
|
this.seq = init.seq;
|
|
2273
|
-
this.primary = primary;
|
|
2274
|
-
this.primaryTop5 = primaryTop5;
|
|
2275
2789
|
}
|
|
2276
2790
|
/**
|
|
2277
2791
|
* Record a scan result.
|
|
2278
2792
|
* - `null` or a `{ stall }` reading → stall (warning), anchors untouched. A
|
|
2279
2793
|
* stall reading also captures its cause for the UI.
|
|
2280
|
-
* - otherwise →
|
|
2281
|
-
* primary's per-conversation latest readings are updated (monotonic).
|
|
2794
|
+
* - otherwise → every model's per-conversation readings advance (monotonic).
|
|
2282
2795
|
*/
|
|
2283
2796
|
recordReading(reading) {
|
|
2284
2797
|
if (reading === null || isStall(reading)) {
|
|
@@ -2288,181 +2801,91 @@ var RaceScoreTracker = class {
|
|
|
2288
2801
|
}
|
|
2289
2802
|
this.stalls = 0;
|
|
2290
2803
|
this.lastStall = null;
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
const prev = this.primaryConvLast[id] ?? 0;
|
|
2299
|
-
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
|
+
}
|
|
2300
2811
|
}
|
|
2301
2812
|
}
|
|
2302
2813
|
/** Frozen payload for the next heartbeat. Pure — call repeatedly for retries. */
|
|
2303
2814
|
nextBeat() {
|
|
2304
|
-
const components =
|
|
2305
|
-
for (const
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
const d = Math.max(0, last - (this.primaryConvAcked[id] ?? 0));
|
|
2312
|
-
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;
|
|
2313
2822
|
}
|
|
2314
|
-
pending.sort((a, b) => b - a);
|
|
2315
|
-
const cap = primaryConversationCap(this.primaryTop5);
|
|
2316
|
-
const take = cap === Infinity ? pending.length : Math.min(cap, pending.length);
|
|
2317
|
-
let primarySum = 0;
|
|
2318
|
-
for (const d of pending.slice(0, take)) primarySum += d;
|
|
2319
|
-
components[this.primary] = primarySum;
|
|
2320
2823
|
return {
|
|
2321
2824
|
seq: this.seq + 1,
|
|
2322
2825
|
components,
|
|
2323
|
-
|
|
2324
|
-
primaryConvReadings: { ...this.primaryConvLast }
|
|
2826
|
+
convReadings: cloneAnchors(this.convLast)
|
|
2325
2827
|
};
|
|
2326
2828
|
}
|
|
2327
2829
|
/** Commit a heartbeat the server accepted. `serverLastSeq` self-heals drift. */
|
|
2328
2830
|
ack(snapshot, serverLastSeq) {
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
this.acked[key] = snapshot.readings[key];
|
|
2332
|
-
}
|
|
2333
|
-
this.primaryConvAcked = { ...snapshot.primaryConvReadings };
|
|
2334
|
-
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];
|
|
2335
2833
|
this.seq = Math.max(snapshot.seq, serverLastSeq);
|
|
2336
2834
|
}
|
|
2337
2835
|
/** Pin anchors to the latest readings so the next deltas are 0 (pending race). */
|
|
2338
2836
|
reprime() {
|
|
2339
|
-
|
|
2340
|
-
if (key === this.primary) continue;
|
|
2341
|
-
this.acked[key] = this.lastGood[key];
|
|
2342
|
-
}
|
|
2343
|
-
this.primaryConvAcked = { ...this.primaryConvLast };
|
|
2837
|
+
this.convAcked = cloneAnchors(this.convLast);
|
|
2344
2838
|
}
|
|
2345
2839
|
get stalled() {
|
|
2346
2840
|
return this.stalls >= STALL_THRESHOLD;
|
|
2347
2841
|
}
|
|
2348
|
-
/**
|
|
2349
|
-
get
|
|
2350
|
-
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;
|
|
2351
2845
|
}
|
|
2352
2846
|
/** Human-readable cause of the most recent stall (null once a good read recovers). */
|
|
2353
2847
|
get stallReason() {
|
|
2354
2848
|
return this.lastStall;
|
|
2355
2849
|
}
|
|
2356
|
-
/** Cumulative
|
|
2357
|
-
|
|
2358
|
-
return this.counted;
|
|
2850
|
+
/** Cumulative tokens credited per family since joining (for the UI's model rows). */
|
|
2851
|
+
countedPerFamily() {
|
|
2852
|
+
return { ...this.counted };
|
|
2359
2853
|
}
|
|
2360
|
-
/**
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
for (const
|
|
2364
|
-
|
|
2365
|
-
out[key] = Math.max(0, this.lastGood[key] - baseline[key]);
|
|
2366
|
-
}
|
|
2367
|
-
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;
|
|
2368
2859
|
}
|
|
2369
2860
|
toState() {
|
|
2370
2861
|
return {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
primaryConvAcked: { ...this.primaryConvAcked },
|
|
2374
|
-
primaryCounted: this.counted,
|
|
2862
|
+
convAcked: cloneAnchors(this.convAcked),
|
|
2863
|
+
counted: { ...this.counted },
|
|
2375
2864
|
seq: this.seq
|
|
2376
2865
|
};
|
|
2377
2866
|
}
|
|
2378
2867
|
};
|
|
2379
2868
|
|
|
2380
|
-
// src/tokens/source-probe.ts
|
|
2381
|
-
import * as fs7 from "fs/promises";
|
|
2382
|
-
var ROOTS = {
|
|
2383
|
-
claude: claudeProjectsDir,
|
|
2384
|
-
codex: codexSessionsDir,
|
|
2385
|
-
gemini: geminiTmpDir
|
|
2386
|
-
};
|
|
2387
|
-
var LISTERS = {
|
|
2388
|
-
claude: listJsonlFiles,
|
|
2389
|
-
codex: listCodexRollouts,
|
|
2390
|
-
gemini: listChatFiles
|
|
2391
|
-
};
|
|
2392
|
-
var LABELS2 = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
|
|
2393
|
-
function sourceDir(key) {
|
|
2394
|
-
return ROOTS[key]();
|
|
2395
|
-
}
|
|
2396
|
-
async function probeSource(key) {
|
|
2397
|
-
const dir = ROOTS[key]();
|
|
2398
|
-
const exists = await fs7.stat(dir).then((st) => st.isDirectory()).catch(() => false);
|
|
2399
|
-
if (!exists) return { key, dir, exists: false, projects: 0, transcripts: 0 };
|
|
2400
|
-
const entries = await fs7.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2401
|
-
const projects = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).length;
|
|
2402
|
-
const files = await LISTERS[key](dir).catch(() => []);
|
|
2403
|
-
return { key, dir, exists: true, projects, transcripts: files.length };
|
|
2404
|
-
}
|
|
2405
|
-
function overrideVar(key) {
|
|
2406
|
-
return `TOKEN_DERBY_${key.toUpperCase()}_DIR`;
|
|
2407
|
-
}
|
|
2408
|
-
async function confirmEmptySource(opts) {
|
|
2409
|
-
if (opts.probe.transcripts > 0) return true;
|
|
2410
|
-
opts.warn(describeEmptySource(opts.probe));
|
|
2411
|
-
if (!opts.interactive) return true;
|
|
2412
|
-
return opts.ask();
|
|
2413
|
-
}
|
|
2414
|
-
function describeEmptySource(probe) {
|
|
2415
|
-
const label = LABELS2[probe.key];
|
|
2416
|
-
const populated = probe.exists && probe.projects > 0;
|
|
2417
|
-
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";
|
|
2418
|
-
const lines = [
|
|
2419
|
-
`\u26A0 No ${label} transcripts found \u2014 your horse will not move.`,
|
|
2420
|
-
``,
|
|
2421
|
-
` Looked in: ${probe.dir}`,
|
|
2422
|
-
` (${reason})`,
|
|
2423
|
-
``
|
|
2424
|
-
];
|
|
2425
|
-
if (populated) {
|
|
2426
|
-
lines.push(
|
|
2427
|
-
` The directory is there and has history in it, so this is usually a`,
|
|
2428
|
-
` dangling symlink or a permissions problem on one of those projects.`,
|
|
2429
|
-
` To find dangling links:`,
|
|
2430
|
-
` find ${probe.dir} -type l ! -exec test -e {} \\; -print`,
|
|
2431
|
-
``
|
|
2432
|
-
);
|
|
2433
|
-
}
|
|
2434
|
-
lines.push(
|
|
2435
|
-
` Token Derby counts ${label} usage from this machine's own filesystem.`,
|
|
2436
|
-
` If ${label} runs in a container, over SSH, or on another machine, join`,
|
|
2437
|
-
` the race from there instead.`
|
|
2438
|
-
);
|
|
2439
|
-
if (probe.key === "claude") {
|
|
2440
|
-
lines.push(
|
|
2441
|
-
` If CLAUDE_CONFIG_DIR relocated your config, Token Derby follows it \u2014`,
|
|
2442
|
-
` check it points at the config root, not the projects directory.`
|
|
2443
|
-
);
|
|
2444
|
-
}
|
|
2445
|
-
lines.push(``, ` To read them from somewhere else: export ${overrideVar(probe.key)}=<dir>`);
|
|
2446
|
-
return lines.join("\n");
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
2869
|
// src/runtime/run-race.tsx
|
|
2450
|
-
import { jsx as
|
|
2870
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2451
2871
|
function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
2452
2872
|
const { exit } = useApp();
|
|
2453
|
-
const [race, setRace] =
|
|
2454
|
-
const [lastHbAt, setLastHbAt] =
|
|
2455
|
-
const [lastHbOk, setLastHbOk] =
|
|
2456
|
-
const [tickNow, setTickNow] =
|
|
2457
|
-
const [fatalError, setFatalError] =
|
|
2458
|
-
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([]);
|
|
2459
2879
|
const shownAchievementAtRef = useRef(0);
|
|
2460
|
-
const trackerRef = useRef(new RaceScoreTracker(initialState
|
|
2880
|
+
const trackerRef = useRef(new RaceScoreTracker(initialState));
|
|
2461
2881
|
const pendingRef = useRef(pendingMode);
|
|
2462
2882
|
const ctrl = useRef(new AbortController());
|
|
2463
|
-
const [stalled, setStalled] =
|
|
2464
|
-
const [stallReason, setStallReason] =
|
|
2465
|
-
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([]);
|
|
2466
2889
|
useEffect2(() => {
|
|
2467
2890
|
const t = setInterval(() => setTickNow(/* @__PURE__ */ new Date()), 1e3);
|
|
2468
2891
|
return () => clearInterval(t);
|
|
@@ -2479,7 +2902,7 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2479
2902
|
const progress = new ScanProgress();
|
|
2480
2903
|
try {
|
|
2481
2904
|
return await scanWithTimeout(
|
|
2482
|
-
() => readAllSources(
|
|
2905
|
+
() => readAllSources(progress),
|
|
2483
2906
|
SCAN_TIMEOUT_MS,
|
|
2484
2907
|
() => diagnoseScanTimeout(SCAN_TIMEOUT_MS, progress)
|
|
2485
2908
|
);
|
|
@@ -2494,7 +2917,11 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2494
2917
|
if (pendingRef.current && !isStall(reading)) tracker.reprime();
|
|
2495
2918
|
setStalled(tracker.stalled);
|
|
2496
2919
|
setStallReason(tracker.stalled ? tracker.stallReason : null);
|
|
2497
|
-
|
|
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)));
|
|
2498
2925
|
return tracker.nextBeat();
|
|
2499
2926
|
},
|
|
2500
2927
|
sendBeat: async (snapshot) => {
|
|
@@ -2544,13 +2971,13 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2544
2971
|
}, []);
|
|
2545
2972
|
const lastHeartbeatAgoSec = lastHbAt ? Math.max(0, Math.floor((tickNow.getTime() - lastHbAt.getTime()) / 1e3)) : null;
|
|
2546
2973
|
if (fatalError) {
|
|
2547
|
-
return /* @__PURE__ */
|
|
2548
|
-
/* @__PURE__ */
|
|
2549
|
-
/* @__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 })
|
|
2550
2977
|
] });
|
|
2551
2978
|
}
|
|
2552
|
-
return /* @__PURE__ */
|
|
2553
|
-
/* @__PURE__ */
|
|
2979
|
+
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
|
|
2980
|
+
/* @__PURE__ */ jsx7(
|
|
2554
2981
|
StatusScreen,
|
|
2555
2982
|
{
|
|
2556
2983
|
race,
|
|
@@ -2562,28 +2989,29 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2562
2989
|
lastHeartbeatOk: lastHbOk,
|
|
2563
2990
|
stalled,
|
|
2564
2991
|
stallReason,
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2992
|
+
sourcesSilent,
|
|
2993
|
+
degraded,
|
|
2994
|
+
notices,
|
|
2995
|
+
disabledHarnesses
|
|
2568
2996
|
}
|
|
2569
2997
|
),
|
|
2570
|
-
achievements.length > 0 && /* @__PURE__ */
|
|
2571
|
-
/* @__PURE__ */
|
|
2998
|
+
achievements.length > 0 && /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
2999
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, children: "Achievements" }),
|
|
2572
3000
|
achievements.map(({ key, event }) => {
|
|
2573
|
-
const description = describeAchievement(event
|
|
2574
|
-
return /* @__PURE__ */
|
|
2575
|
-
/* @__PURE__ */
|
|
3001
|
+
const description = describeAchievement(event);
|
|
3002
|
+
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "row", children: [
|
|
3003
|
+
/* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
|
|
2576
3004
|
" ",
|
|
2577
3005
|
formatClockTime(event.at),
|
|
2578
3006
|
" "
|
|
2579
3007
|
] }),
|
|
2580
|
-
/* @__PURE__ */
|
|
3008
|
+
/* @__PURE__ */ jsxs5(Text7, { color: "yellow", bold: true, children: [
|
|
2581
3009
|
"+",
|
|
2582
3010
|
event.xp,
|
|
2583
3011
|
" XP "
|
|
2584
3012
|
] }),
|
|
2585
|
-
/* @__PURE__ */
|
|
2586
|
-
/* @__PURE__ */
|
|
3013
|
+
/* @__PURE__ */ jsx7(Text7, { children: event.name }),
|
|
3014
|
+
/* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
|
|
2587
3015
|
" \u2014 ",
|
|
2588
3016
|
description
|
|
2589
3017
|
] })
|
|
@@ -2609,28 +3037,81 @@ function raceViewFrom(resp) {
|
|
|
2609
3037
|
};
|
|
2610
3038
|
}
|
|
2611
3039
|
async function buildInitialState(args) {
|
|
2612
|
-
|
|
2613
|
-
const primaryConvAcked = {};
|
|
3040
|
+
const convAcked = { anthropic: {}, openai: {}, google: {} };
|
|
2614
3041
|
try {
|
|
2615
|
-
const now = await readAllSources(
|
|
3042
|
+
const now = await readAllSources();
|
|
2616
3043
|
if (!isStall(now)) {
|
|
2617
|
-
|
|
2618
|
-
|
|
3044
|
+
for (const family of MODEL_FAMILIES) {
|
|
3045
|
+
for (const [id, value] of now.byFamily[family]) convAcked[family][id] = value;
|
|
3046
|
+
}
|
|
2619
3047
|
}
|
|
2620
3048
|
} catch {
|
|
2621
3049
|
}
|
|
2622
3050
|
return {
|
|
2623
3051
|
initialState: {
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
primaryConvAcked,
|
|
2627
|
-
primaryCounted: 0,
|
|
3052
|
+
convAcked,
|
|
3053
|
+
counted: zeroPerFamily(),
|
|
2628
3054
|
seq: args.serverLastSeq
|
|
2629
3055
|
},
|
|
2630
3056
|
pendingMode: args.raceStatus === "pending"
|
|
2631
3057
|
};
|
|
2632
3058
|
}
|
|
2633
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
|
+
|
|
2634
3115
|
// src/ui/prompt.ts
|
|
2635
3116
|
async function promptYesNo(question, opts = {}) {
|
|
2636
3117
|
const input = opts.input ?? process.stdin;
|
|
@@ -2654,31 +3135,12 @@ function resetStdinAfterInk() {
|
|
|
2654
3135
|
}
|
|
2655
3136
|
|
|
2656
3137
|
// src/commands/join.ts
|
|
2657
|
-
function parsePrimaryFlag(argv) {
|
|
2658
|
-
for (let i = 0; i < argv.length; i++) {
|
|
2659
|
-
const a = argv[i];
|
|
2660
|
-
let value;
|
|
2661
|
-
if (a === "--primary") value = argv[i + 1];
|
|
2662
|
-
else if (a.startsWith("--primary=")) value = a.slice("--primary=".length);
|
|
2663
|
-
else continue;
|
|
2664
|
-
if (!isModelKey(value)) throw new Error(`--primary must be one of claude, codex, gemini (got ${value ?? ""})`);
|
|
2665
|
-
return value;
|
|
2666
|
-
}
|
|
2667
|
-
return null;
|
|
2668
|
-
}
|
|
2669
3138
|
async function joinCommand(joinCode, argv = []) {
|
|
2670
3139
|
if (!joinCode) {
|
|
2671
3140
|
console.error("Usage: token-derby join <join-code>");
|
|
2672
3141
|
return 2;
|
|
2673
3142
|
}
|
|
2674
3143
|
const code = joinCode.toUpperCase();
|
|
2675
|
-
let primaryFlag;
|
|
2676
|
-
try {
|
|
2677
|
-
primaryFlag = parsePrimaryFlag(argv);
|
|
2678
|
-
} catch (e) {
|
|
2679
|
-
console.error(e.message);
|
|
2680
|
-
return 2;
|
|
2681
|
-
}
|
|
2682
3144
|
const identity = await loadIdentity();
|
|
2683
3145
|
if (!identity) {
|
|
2684
3146
|
console.error("Run `token-derby login` to set up your identity.");
|
|
@@ -2733,7 +3195,27 @@ async function joinCommand(joinCode, argv = []) {
|
|
|
2733
3195
|
console.error("Your stable is empty. Run `token-derby stable create` first.");
|
|
2734
3196
|
return 1;
|
|
2735
3197
|
}
|
|
2736
|
-
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
|
+
}
|
|
2737
3219
|
if (!picked) {
|
|
2738
3220
|
console.log("Cancelled.");
|
|
2739
3221
|
return 1;
|
|
@@ -2742,14 +3224,8 @@ async function joinCommand(joinCode, argv = []) {
|
|
|
2742
3224
|
chosenName = picked.name;
|
|
2743
3225
|
chosenColors = picked.colors;
|
|
2744
3226
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
if (primaryFlag) chosenPrimary = primaryFlag;
|
|
2748
|
-
else if (process.stdout.isTTY) chosenPrimary = await pickPrimary();
|
|
2749
|
-
}
|
|
2750
|
-
const effectivePrimary = ownHorse?.primary_model ?? chosenPrimary;
|
|
2751
|
-
const proceed = await confirmEmptySource({
|
|
2752
|
-
probe: await probeSource(effectivePrimary),
|
|
3227
|
+
const proceed = await confirmNoSources({
|
|
3228
|
+
probes: await probeAll(),
|
|
2753
3229
|
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
2754
3230
|
warn: (text) => console.error(`
|
|
2755
3231
|
${text}
|
|
@@ -2762,7 +3238,7 @@ ${text}
|
|
|
2762
3238
|
}
|
|
2763
3239
|
let joinResp;
|
|
2764
3240
|
try {
|
|
2765
|
-
joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId
|
|
3241
|
+
joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId });
|
|
2766
3242
|
} catch (e) {
|
|
2767
3243
|
if (e instanceof ApiError) {
|
|
2768
3244
|
if (e.code === "RACE_FULL") console.error("This race is full.");
|
|
@@ -2788,27 +3264,22 @@ ${text}
|
|
|
2788
3264
|
horse_colors: chosenColors,
|
|
2789
3265
|
joined_at: ownHorse?.joined_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2790
3266
|
last_heartbeat_at: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2791
|
-
primary_model: joinResp.primary_model,
|
|
2792
3267
|
score: {
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
primaryConvAcked: {},
|
|
2796
|
-
primaryCounted: 0,
|
|
3268
|
+
convAcked: { anthropic: {}, openai: {}, google: {} },
|
|
3269
|
+
counted: { anthropic: 0, openai: 0, google: 0 },
|
|
2797
3270
|
seq: ownHorse?.last_seq ?? 0
|
|
2798
|
-
}
|
|
2799
|
-
...race.counts_input ? { counts_input: true } : {},
|
|
2800
|
-
...race.primary_top5 ? { primary_top5: true } : {}
|
|
3271
|
+
}
|
|
2801
3272
|
};
|
|
2802
3273
|
await saveActiveRace(active);
|
|
2803
3274
|
const initial = await buildInitialState({ active, raceStatus: status, serverLastSeq: ownHorse?.last_seq ?? 0 });
|
|
2804
|
-
const app = render4(
|
|
3275
|
+
const app = render4(React5.createElement(RunRace, { active, initialState: initial.initialState, pendingMode: initial.pendingMode, ownUserName: identity.display_name }));
|
|
2805
3276
|
await app.waitUntilExit();
|
|
2806
3277
|
return 0;
|
|
2807
3278
|
}
|
|
2808
3279
|
async function pickHorse(horses) {
|
|
2809
3280
|
return new Promise((resolve) => {
|
|
2810
3281
|
const app = render4(
|
|
2811
|
-
|
|
3282
|
+
React5.createElement(HorsePicker, {
|
|
2812
3283
|
horses,
|
|
2813
3284
|
onPick: (h) => {
|
|
2814
3285
|
app.unmount();
|
|
@@ -2822,18 +3293,6 @@ async function pickHorse(horses) {
|
|
|
2822
3293
|
);
|
|
2823
3294
|
});
|
|
2824
3295
|
}
|
|
2825
|
-
async function pickPrimary() {
|
|
2826
|
-
return new Promise((resolve) => {
|
|
2827
|
-
const app = render4(
|
|
2828
|
-
React9.createElement(PrimaryPicker, {
|
|
2829
|
-
onPick: (m) => {
|
|
2830
|
-
app.unmount();
|
|
2831
|
-
resolve(m);
|
|
2832
|
-
}
|
|
2833
|
-
})
|
|
2834
|
-
);
|
|
2835
|
-
});
|
|
2836
|
-
}
|
|
2837
3296
|
|
|
2838
3297
|
// src/commands/end.ts
|
|
2839
3298
|
import * as readline3 from "readline/promises";
|
|
@@ -3558,16 +4017,16 @@ function runNpmUpgrade(spawnImpl) {
|
|
|
3558
4017
|
}
|
|
3559
4018
|
|
|
3560
4019
|
// src/commands/roll.ts
|
|
3561
|
-
import
|
|
4020
|
+
import React7 from "react";
|
|
3562
4021
|
import { render as render6 } from "ink";
|
|
3563
4022
|
|
|
3564
4023
|
// src/ui/RollHorsePicker.tsx
|
|
3565
|
-
import { useState as
|
|
3566
|
-
import { Box as
|
|
3567
|
-
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";
|
|
3568
4027
|
function RollHorsePicker({ horses, onPick, onCancel }) {
|
|
3569
|
-
const [idx, setIdx] =
|
|
3570
|
-
|
|
4028
|
+
const [idx, setIdx] = useState5(0);
|
|
4029
|
+
useInput3((input, key) => {
|
|
3571
4030
|
if (key.escape) {
|
|
3572
4031
|
onCancel();
|
|
3573
4032
|
return;
|
|
@@ -3586,61 +4045,61 @@ function RollHorsePicker({ horses, onPick, onCancel }) {
|
|
|
3586
4045
|
return;
|
|
3587
4046
|
}
|
|
3588
4047
|
});
|
|
3589
|
-
return /* @__PURE__ */
|
|
3590
|
-
/* @__PURE__ */
|
|
3591
|
-
horses.map((h, i) => /* @__PURE__ */
|
|
3592
|
-
/* @__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: [
|
|
3593
4052
|
i === idx ? "\u25BA" : " ",
|
|
3594
4053
|
" ",
|
|
3595
4054
|
h.name,
|
|
3596
4055
|
" ",
|
|
3597
|
-
/* @__PURE__ */
|
|
4056
|
+
/* @__PURE__ */ jsxs6(Text8, { color: "cyan", children: [
|
|
3598
4057
|
"[Lvl. ",
|
|
3599
4058
|
levelFromXp(h.xp),
|
|
3600
4059
|
"]"
|
|
3601
4060
|
] }),
|
|
3602
4061
|
" ",
|
|
3603
|
-
/* @__PURE__ */
|
|
4062
|
+
/* @__PURE__ */ jsxs6(Text8, { color: "yellow", children: [
|
|
3604
4063
|
"\u2014 ",
|
|
3605
4064
|
h.pending,
|
|
3606
4065
|
" roll",
|
|
3607
4066
|
h.pending === 1 ? "" : "s"
|
|
3608
4067
|
] })
|
|
3609
4068
|
] }) }),
|
|
3610
|
-
/* @__PURE__ */
|
|
3611
|
-
/* @__PURE__ */
|
|
3612
|
-
/* @__PURE__ */
|
|
4069
|
+
/* @__PURE__ */ jsxs6(Box8, { flexDirection: "row", children: [
|
|
4070
|
+
/* @__PURE__ */ jsx8(Text8, { children: " " }),
|
|
4071
|
+
/* @__PURE__ */ jsx8(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
|
|
3613
4072
|
] })
|
|
3614
4073
|
] }, h.stable_horse_id)),
|
|
3615
|
-
/* @__PURE__ */
|
|
4074
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
|
|
3616
4075
|
] });
|
|
3617
4076
|
}
|
|
3618
4077
|
|
|
3619
4078
|
// src/ui/reveal.ts
|
|
3620
|
-
import
|
|
4079
|
+
import React6 from "react";
|
|
3621
4080
|
import { render as render5 } from "ink";
|
|
3622
4081
|
|
|
3623
4082
|
// src/ui/RollReveal.tsx
|
|
3624
|
-
import { useState as
|
|
3625
|
-
import { Box as
|
|
4083
|
+
import { useState as useState7, useEffect as useEffect4, useMemo } from "react";
|
|
4084
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
3626
4085
|
|
|
3627
4086
|
// src/ui/HatSprite.tsx
|
|
3628
|
-
import { useEffect as useEffect3, useState as
|
|
3629
|
-
import { Box as
|
|
3630
|
-
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";
|
|
3631
4090
|
function HatSprite({ hat, variant, centerIn }) {
|
|
3632
4091
|
const colors = hatColors(hat, variant ?? 0);
|
|
3633
4092
|
const grid = makeHatGrid(hat, colors, centerIn);
|
|
3634
4093
|
const lines = hexGridToHalfBlocks(grid);
|
|
3635
|
-
return /* @__PURE__ */
|
|
4094
|
+
return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx9(Text9, { children: line }, i)) });
|
|
3636
4095
|
}
|
|
3637
4096
|
function AnimatedHatSprite({ hat, variant, centerIn }) {
|
|
3638
4097
|
if (!isAnimatedHat(hat)) {
|
|
3639
|
-
return /* @__PURE__ */
|
|
4098
|
+
return /* @__PURE__ */ jsx9(HatSprite, { hat, variant, centerIn });
|
|
3640
4099
|
}
|
|
3641
4100
|
const frames = hat.animation.frames;
|
|
3642
4101
|
const fps = hat.animation.fps;
|
|
3643
|
-
const [idx, setIdx] =
|
|
4102
|
+
const [idx, setIdx] = useState6(0);
|
|
3644
4103
|
useEffect3(() => {
|
|
3645
4104
|
if (frames.length <= 1) return;
|
|
3646
4105
|
const interval = setInterval(
|
|
@@ -3650,7 +4109,7 @@ function AnimatedHatSprite({ hat, variant, centerIn }) {
|
|
|
3650
4109
|
return () => clearInterval(interval);
|
|
3651
4110
|
}, [frames.length, fps]);
|
|
3652
4111
|
const framed = { ...hat, colors: { ...hat.colors, A: frames[idx] } };
|
|
3653
|
-
return /* @__PURE__ */
|
|
4112
|
+
return /* @__PURE__ */ jsx9(HatSprite, { hat: framed, variant, centerIn });
|
|
3654
4113
|
}
|
|
3655
4114
|
function makeHatGrid(hat, colors, centerIn) {
|
|
3656
4115
|
const w = centerIn?.w ?? hat.width;
|
|
@@ -3673,7 +4132,7 @@ function makeHatGrid(hat, colors, centerIn) {
|
|
|
3673
4132
|
}
|
|
3674
4133
|
|
|
3675
4134
|
// src/ui/RollReveal.tsx
|
|
3676
|
-
import { jsx as
|
|
4135
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
3677
4136
|
var RESET3 = "\x1B[0m";
|
|
3678
4137
|
var BOX_COLOR = "#E5C76B";
|
|
3679
4138
|
var TIER_PALETTE = {
|
|
@@ -3743,13 +4202,13 @@ var BOX_EMPTY = [
|
|
|
3743
4202
|
""
|
|
3744
4203
|
].map(pad);
|
|
3745
4204
|
function GiftBox({ frame, color }) {
|
|
3746
|
-
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)) });
|
|
3747
4206
|
}
|
|
3748
|
-
function spawnParticles(tier,
|
|
4207
|
+
function spawnParticles(tier, count2, cx, cy) {
|
|
3749
4208
|
const palette = TIER_PALETTE[tier];
|
|
3750
4209
|
const out = [];
|
|
3751
|
-
for (let i = 0; i <
|
|
3752
|
-
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;
|
|
3753
4212
|
const speed = 0.8 + Math.random() * 1.6;
|
|
3754
4213
|
out.push({
|
|
3755
4214
|
x: cx,
|
|
@@ -3766,7 +4225,7 @@ function ConfettiBurst({ tier }) {
|
|
|
3766
4225
|
const cx = Math.floor(SCENE_W / 2);
|
|
3767
4226
|
const cy = Math.floor(SCENE_H / 2);
|
|
3768
4227
|
const particles = useMemo(() => spawnParticles(tier, 36, cx, cy), [tier, cx, cy]);
|
|
3769
|
-
const [tick, setTick] =
|
|
4228
|
+
const [tick, setTick] = useState7(0);
|
|
3770
4229
|
useEffect4(() => {
|
|
3771
4230
|
const i = setInterval(() => setTick((t) => t + 1), 70);
|
|
3772
4231
|
return () => clearInterval(i);
|
|
@@ -3779,13 +4238,13 @@ function ConfettiBurst({ tier }) {
|
|
|
3779
4238
|
grid[y][x] = ansiFg(p.color) + p.char + RESET3;
|
|
3780
4239
|
}
|
|
3781
4240
|
}
|
|
3782
|
-
return /* @__PURE__ */
|
|
4241
|
+
return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: grid.map((row, y) => /* @__PURE__ */ jsx10(Text10, { children: row.join("") }, y)) });
|
|
3783
4242
|
}
|
|
3784
4243
|
var CLOSED_HOLD_MS = 3e3;
|
|
3785
4244
|
function RollReveal({ outcome, onDone }) {
|
|
3786
4245
|
const isNoHat = outcome.kind === "no_hat";
|
|
3787
4246
|
const isShowpiece = outcome.kind !== "no_hat" && (outcome.hat.rarity === "legendary" || outcome.hat.rarity === "limited");
|
|
3788
|
-
const [phase, setPhase] =
|
|
4247
|
+
const [phase, setPhase] = useState7("closed");
|
|
3789
4248
|
useEffect4(() => {
|
|
3790
4249
|
const timers = [];
|
|
3791
4250
|
timers.push(setTimeout(() => setPhase("open1"), CLOSED_HOLD_MS));
|
|
@@ -3800,21 +4259,21 @@ function RollReveal({ outcome, onDone }) {
|
|
|
3800
4259
|
}
|
|
3801
4260
|
return () => timers.forEach(clearTimeout);
|
|
3802
4261
|
}, [isNoHat, isShowpiece, onDone]);
|
|
3803
|
-
if (phase === "closed") return /* @__PURE__ */
|
|
3804
|
-
if (phase === "open1") return /* @__PURE__ */
|
|
3805
|
-
if (phase === "open2") return /* @__PURE__ */
|
|
3806
|
-
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 });
|
|
3807
4266
|
if (phase === "burst" && outcome.kind !== "no_hat") {
|
|
3808
|
-
return /* @__PURE__ */
|
|
4267
|
+
return /* @__PURE__ */ jsx10(ConfettiBurst, { tier: outcome.hat.rarity });
|
|
3809
4268
|
}
|
|
3810
|
-
if (outcome.kind === "no_hat") return /* @__PURE__ */
|
|
3811
|
-
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 } });
|
|
3812
4271
|
}
|
|
3813
4272
|
|
|
3814
4273
|
// src/ui/reveal.ts
|
|
3815
4274
|
async function runReveal(outcome) {
|
|
3816
4275
|
await new Promise((resolve) => {
|
|
3817
|
-
const app = render5(
|
|
4276
|
+
const app = render5(React6.createElement(RollReveal, {
|
|
3818
4277
|
outcome,
|
|
3819
4278
|
onDone: () => {
|
|
3820
4279
|
app.unmount();
|
|
@@ -3830,7 +4289,7 @@ function pendingFor(horse) {
|
|
|
3830
4289
|
const lastRolled = horse.last_rolled_level ?? Math.max(1, level - 1);
|
|
3831
4290
|
return level - lastRolled;
|
|
3832
4291
|
}
|
|
3833
|
-
async function rollCommand() {
|
|
4292
|
+
async function rollCommand(args = []) {
|
|
3834
4293
|
let stable;
|
|
3835
4294
|
try {
|
|
3836
4295
|
stable = await listStable();
|
|
@@ -3846,19 +4305,39 @@ async function rollCommand() {
|
|
|
3846
4305
|
console.log("No rolls available. Level up a horse to earn a roll!");
|
|
3847
4306
|
return 0;
|
|
3848
4307
|
}
|
|
3849
|
-
const
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
onPick: (h) => {
|
|
3853
|
-
app.unmount();
|
|
3854
|
-
resolve(h);
|
|
3855
|
-
},
|
|
3856
|
-
onCancel: () => {
|
|
3857
|
-
app.unmount();
|
|
3858
|
-
resolve(null);
|
|
3859
|
-
}
|
|
3860
|
-
}));
|
|
4308
|
+
const choice = await resolveHorse(eligible, {
|
|
4309
|
+
name: parseFlag(args, "--horse"),
|
|
4310
|
+
autoSelect: false
|
|
3861
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
|
+
}
|
|
3862
4341
|
if (!picked) {
|
|
3863
4342
|
console.log("Cancelled.");
|
|
3864
4343
|
return 0;
|
|
@@ -3903,7 +4382,9 @@ async function rollCommand() {
|
|
|
3903
4382
|
console.log(`
|
|
3904
4383
|
\u2728 ${hat.name}${variantSuffix} [${hat.rarity.toUpperCase()}]
|
|
3905
4384
|
`);
|
|
3906
|
-
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] ")) {
|
|
3907
4388
|
try {
|
|
3908
4389
|
await equipHat(chosen.stable_horse_id, { hat_index: result.hat_index });
|
|
3909
4390
|
console.log(`Equipped on ${chosen.name}.`);
|
|
@@ -3925,21 +4406,25 @@ No hat this time. +${result.xp_awarded} XP toward your next level.
|
|
|
3925
4406
|
`);
|
|
3926
4407
|
}
|
|
3927
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
|
+
}
|
|
3928
4413
|
if (!await promptYesNo(`${result.remaining_rolls} more roll${result.remaining_rolls === 1 ? "" : "s"} available. Roll again? [Y/n] `)) return 0;
|
|
3929
4414
|
}
|
|
3930
4415
|
}
|
|
3931
4416
|
|
|
3932
4417
|
// src/commands/claim.ts
|
|
3933
|
-
import
|
|
4418
|
+
import React8 from "react";
|
|
3934
4419
|
import { render as render7 } from "ink";
|
|
3935
|
-
async function claimCommand(token) {
|
|
4420
|
+
async function claimCommand(token, args = []) {
|
|
3936
4421
|
if (!token) {
|
|
3937
4422
|
console.error("Usage: token-derby claim <token>");
|
|
3938
4423
|
return 2;
|
|
3939
4424
|
}
|
|
3940
|
-
let
|
|
4425
|
+
let probe2;
|
|
3941
4426
|
try {
|
|
3942
|
-
|
|
4427
|
+
probe2 = await probeClaim(token);
|
|
3943
4428
|
} catch (e) {
|
|
3944
4429
|
if (e instanceof ApiError) {
|
|
3945
4430
|
console.error(`Error: ${e.code} ${e.message}`);
|
|
@@ -3961,23 +4446,44 @@ async function claimCommand(token) {
|
|
|
3961
4446
|
console.error("No horses in your stable. Run `token-derby stable create` first.");
|
|
3962
4447
|
return 1;
|
|
3963
4448
|
}
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
const picked = await new Promise((resolve) => {
|
|
3968
|
-
const app = render7(React15.createElement(HorsePicker, {
|
|
3969
|
-
horses: stable.horses,
|
|
3970
|
-
prompt: "Which horse should receive it?",
|
|
3971
|
-
onPick: (h) => {
|
|
3972
|
-
app.unmount();
|
|
3973
|
-
resolve(h);
|
|
3974
|
-
},
|
|
3975
|
-
onCancel: () => {
|
|
3976
|
-
app.unmount();
|
|
3977
|
-
resolve(null);
|
|
3978
|
-
}
|
|
3979
|
-
}));
|
|
4449
|
+
const choice = await resolveHorse(stable.horses, {
|
|
4450
|
+
name: parseFlag(args, "--horse"),
|
|
4451
|
+
pick: hasFlag(args, "--pick")
|
|
3980
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
|
+
}
|
|
3981
4487
|
if (!picked) {
|
|
3982
4488
|
console.log("Cancelled. Your token is unspent.");
|
|
3983
4489
|
return 0;
|
|
@@ -4011,7 +4517,9 @@ async function claimCommand(token) {
|
|
|
4011
4517
|
console.log(`
|
|
4012
4518
|
\u2728 ${hat2.name}${variantSuffix2} [${hat2.rarity.toUpperCase()}]
|
|
4013
4519
|
`);
|
|
4014
|
-
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] ")) {
|
|
4015
4523
|
try {
|
|
4016
4524
|
await equipHat(picked.stable_horse_id, { hat_index: result.hat_index });
|
|
4017
4525
|
console.log(`Equipped on ${picked.name}.`);
|
|
@@ -4031,6 +4539,121 @@ ${picked.name} already has ${hat?.name ?? result.hat_id}${variantSuffix}. +${res
|
|
|
4031
4539
|
return 0;
|
|
4032
4540
|
}
|
|
4033
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
|
+
|
|
4034
4657
|
// src/commands/org-join.ts
|
|
4035
4658
|
async function orgJoinCommand(token) {
|
|
4036
4659
|
const join_token = token?.trim();
|
|
@@ -4077,6 +4700,31 @@ function envCommand(arg) {
|
|
|
4077
4700
|
return 0;
|
|
4078
4701
|
}
|
|
4079
4702
|
|
|
4703
|
+
// src/commands/logs.ts
|
|
4704
|
+
import * as fs11 from "fs/promises";
|
|
4705
|
+
import { existsSync as existsSync2 } from "fs";
|
|
4706
|
+
var DEFAULT_TAIL_LINES = 50;
|
|
4707
|
+
function tailCount(argv) {
|
|
4708
|
+
const i = argv.indexOf("--tail");
|
|
4709
|
+
if (i === -1) return null;
|
|
4710
|
+
const n = Number(argv[i + 1]);
|
|
4711
|
+
return Number.isInteger(n) && n > 0 ? n : DEFAULT_TAIL_LINES;
|
|
4712
|
+
}
|
|
4713
|
+
async function logsCommand(argv) {
|
|
4714
|
+
const file = logFile();
|
|
4715
|
+
if (!existsSync2(file)) {
|
|
4716
|
+
console.log(`No log file yet \u2014 it appears at ${file} the first time a command runs.`);
|
|
4717
|
+
return 0;
|
|
4718
|
+
}
|
|
4719
|
+
console.log(file);
|
|
4720
|
+
const n = tailCount(argv);
|
|
4721
|
+
if (n === null) return 0;
|
|
4722
|
+
const lines = (await fs11.readFile(file, "utf8")).split("\n").filter((l) => l.length > 0);
|
|
4723
|
+
console.log("");
|
|
4724
|
+
for (const line of lines.slice(-n)) console.log(line);
|
|
4725
|
+
return 0;
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4080
4728
|
// src/bin.ts
|
|
4081
4729
|
var HELP = `token-derby v${CLI_VERSION}
|
|
4082
4730
|
|
|
@@ -4105,12 +4753,23 @@ Identity:
|
|
|
4105
4753
|
|
|
4106
4754
|
Maintenance:
|
|
4107
4755
|
token-derby update Check for and install the latest CLI version
|
|
4756
|
+
token-derby logs Show the path of the debug log
|
|
4757
|
+
token-derby logs --tail [n] Print the last n log lines (default 50)
|
|
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
|
|
4108
4764
|
|
|
4109
4765
|
Stable management:
|
|
4110
4766
|
token-derby stable create Make a new horse (interactive)
|
|
4111
4767
|
token-derby stable list Show your saved horses
|
|
4112
4768
|
token-derby stable edit [name] Edit an existing horse's colors (interactive picker if no name)
|
|
4113
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.
|
|
4114
4773
|
|
|
4115
4774
|
Organisations:
|
|
4116
4775
|
token-derby organisation join [token] Join an organisation with a join token,
|
|
@@ -4124,13 +4783,17 @@ Races:
|
|
|
4124
4783
|
Create a new race (interactive). When
|
|
4125
4784
|
--organisation is set, only members of
|
|
4126
4785
|
that org can join.
|
|
4127
|
-
token-derby join <join-code>
|
|
4786
|
+
token-derby join <join-code> [--horse <name>|--pick]
|
|
4787
|
+
Join (or resume) a race
|
|
4128
4788
|
token-derby end <admin-code> End a race early
|
|
4129
4789
|
|
|
4130
4790
|
Cosmetics:
|
|
4131
|
-
token-derby roll
|
|
4132
|
-
Earn rolls by leveling up horses.
|
|
4133
|
-
|
|
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
|
|
4134
4797
|
awarded to you by an admin.
|
|
4135
4798
|
|
|
4136
4799
|
Environment:
|
|
@@ -4141,9 +4804,25 @@ Environment:
|
|
|
4141
4804
|
TOKEN_DERBY_API_BASE Hard-override API base URL (wins over env)
|
|
4142
4805
|
TOKEN_DERBY_HOME Hard-override identity/stable directory
|
|
4143
4806
|
`;
|
|
4807
|
+
function describeInvocation(argv) {
|
|
4808
|
+
const cmd = argv[0] ?? "(none)";
|
|
4809
|
+
const container = cmd === "stable" || cmd === "organisation" || cmd === "org" || cmd === "harness";
|
|
4810
|
+
return {
|
|
4811
|
+
cmd,
|
|
4812
|
+
sub: container ? argv[1] : void 0,
|
|
4813
|
+
flags: argv.filter((a) => a.startsWith("--")).map((a) => a.split("=")[0])
|
|
4814
|
+
};
|
|
4815
|
+
}
|
|
4144
4816
|
async function main() {
|
|
4145
4817
|
const argv = process.argv.slice(2);
|
|
4146
4818
|
const cmd = argv[0];
|
|
4819
|
+
logInfo("cmd.start", {
|
|
4820
|
+
...describeInvocation(argv),
|
|
4821
|
+
version: CLI_VERSION,
|
|
4822
|
+
node: process.version,
|
|
4823
|
+
pid: process.pid,
|
|
4824
|
+
env: selectedEnv()
|
|
4825
|
+
});
|
|
4147
4826
|
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
4148
4827
|
console.log(HELP);
|
|
4149
4828
|
return 0;
|
|
@@ -4159,6 +4838,16 @@ async function main() {
|
|
|
4159
4838
|
if (cmd === "login") return loginCommand(argv.slice(1));
|
|
4160
4839
|
if (cmd === "update") return updateCommand();
|
|
4161
4840
|
if (cmd === "env") return envCommand(argv[1]);
|
|
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
|
+
}
|
|
4162
4851
|
const identity = await loadIdentity();
|
|
4163
4852
|
if (!identity) {
|
|
4164
4853
|
console.error("Run `token-derby login` to set up your identity before using any other command.");
|
|
@@ -4170,8 +4859,9 @@ async function main() {
|
|
|
4170
4859
|
if (sub === "list") return stableListCommand();
|
|
4171
4860
|
if (sub === "edit") return stableEditCommand(argv[2]);
|
|
4172
4861
|
if (sub === "delete") return stableDeleteCommand(argv[2]);
|
|
4862
|
+
if (sub === "default") return stableDefaultCommand(argv.slice(2));
|
|
4173
4863
|
console.error(`Unknown stable subcommand: ${sub ?? "(none)"}`);
|
|
4174
|
-
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]");
|
|
4175
4865
|
return 2;
|
|
4176
4866
|
}
|
|
4177
4867
|
if (cmd === "organisation" || cmd === "org") {
|
|
@@ -4190,24 +4880,40 @@ async function main() {
|
|
|
4190
4880
|
if (cmd === "whoami") return whoamiCommand();
|
|
4191
4881
|
if (cmd === "join") return joinCommand(argv[1], argv.slice(2));
|
|
4192
4882
|
if (cmd === "end") return endCommand(argv[1]);
|
|
4193
|
-
if (cmd === "roll") return rollCommand();
|
|
4194
|
-
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));
|
|
4195
4885
|
if (cmd === "web") return webCommand();
|
|
4196
4886
|
console.error(`Unknown command: ${cmd}`);
|
|
4197
4887
|
console.error(HELP);
|
|
4198
4888
|
return 2;
|
|
4199
4889
|
}
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4890
|
+
var CRASH_HANDLERS_INSTALLED = /* @__PURE__ */ Symbol.for("token-derby.crash-handlers");
|
|
4891
|
+
if (!(CRASH_HANDLERS_INSTALLED in process)) {
|
|
4892
|
+
process[CRASH_HANDLERS_INSTALLED] = true;
|
|
4893
|
+
process.on("uncaughtException", (err) => {
|
|
4894
|
+
logError("cmd.uncaught", { message: err?.message ?? String(err), stack: err?.stack });
|
|
4895
|
+
console.error(err?.stack ?? err);
|
|
4896
|
+
process.exit(1);
|
|
4897
|
+
});
|
|
4898
|
+
process.on("unhandledRejection", (reason) => {
|
|
4899
|
+
logError("cmd.unhandled", {
|
|
4900
|
+
message: reason?.message ?? String(reason),
|
|
4901
|
+
stack: reason?.stack
|
|
4902
|
+
});
|
|
4903
|
+
});
|
|
4207
4904
|
}
|
|
4905
|
+
var startedAt = Date.now();
|
|
4208
4906
|
main().then(
|
|
4209
|
-
(code) =>
|
|
4907
|
+
(code) => {
|
|
4908
|
+
logInfo("cmd.exit", { code, ms: Date.now() - startedAt });
|
|
4909
|
+
process.exit(code);
|
|
4910
|
+
},
|
|
4210
4911
|
(err) => {
|
|
4912
|
+
logError("cmd.crash", {
|
|
4913
|
+
message: err?.message ?? String(err),
|
|
4914
|
+
stack: err?.stack,
|
|
4915
|
+
ms: Date.now() - startedAt
|
|
4916
|
+
});
|
|
4211
4917
|
console.error(err?.stack ?? err);
|
|
4212
4918
|
process.exit(1);
|
|
4213
4919
|
}
|