@mauricode/token-derby 3.1.2 → 3.1.3
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 +29 -0
- package/dist/bin.js +213 -32
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,35 @@ at usage you didn't produce.
|
|
|
97
97
|
|
|
98
98
|
- `~/.token-derby/stable.json` — saved horses
|
|
99
99
|
- `~/.token-derby/active-races/<join-code>.json` — per-race state for rejoin
|
|
100
|
+
- `~/.token-derby/logs/token-derby.log` — debug log (see below)
|
|
101
|
+
|
|
102
|
+
## Debug log
|
|
103
|
+
|
|
104
|
+
Every command appends to a rolling log, so a race that stalls overnight can be
|
|
105
|
+
diagnosed afterwards. The race UI takes over the terminal, which is exactly when
|
|
106
|
+
nothing can be printed to the screen.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
token-derby logs # print the path of the log file
|
|
110
|
+
token-derby logs --tail 100 # print the last 100 lines (default 50)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The log rolls at 2MB and keeps five files (`token-derby.log` plus `.1`–`.4`), so
|
|
114
|
+
it never exceeds ~10MB. Each environment has its own, next to that environment's
|
|
115
|
+
identity.
|
|
116
|
+
|
|
117
|
+
What the lines mean when a race misbehaves:
|
|
118
|
+
|
|
119
|
+
- `beat.prepare.start` with no `beat.prepare.done` after it — the token scan
|
|
120
|
+
hung, and the poller is still waiting on it.
|
|
121
|
+
- repeated `beat.send.err` with a climbing `next_ms` — the heartbeat is
|
|
122
|
+
reaching the network and failing; `retry` counts the attempts.
|
|
123
|
+
- `scan.timeout` — the scan blew its budget; `reason` names the source that was
|
|
124
|
+
still running.
|
|
125
|
+
|
|
126
|
+
Credentials are never written: identity and horse tokens, request headers and
|
|
127
|
+
bodies are all omitted, and claim tokens and admin codes are masked out of the
|
|
128
|
+
URLs they travel in.
|
|
100
129
|
|
|
101
130
|
## Environment
|
|
102
131
|
|
package/dist/bin.js
CHANGED
|
@@ -862,8 +862,8 @@ var PRIMARY_SILENT_THRESHOLD = 10;
|
|
|
862
862
|
// src/version.ts
|
|
863
863
|
import { createRequire } from "module";
|
|
864
864
|
function readVersion() {
|
|
865
|
-
if ("3.1.
|
|
866
|
-
return "3.1.
|
|
865
|
+
if ("3.1.3".length > 0) {
|
|
866
|
+
return "3.1.3";
|
|
867
867
|
}
|
|
868
868
|
try {
|
|
869
869
|
const req = createRequire(import.meta.url);
|
|
@@ -910,6 +910,12 @@ function codexSessionsDir() {
|
|
|
910
910
|
function geminiTmpDir() {
|
|
911
911
|
return process.env.TOKEN_DERBY_GEMINI_DIR ?? path2.join(os2.homedir(), ".gemini", "tmp");
|
|
912
912
|
}
|
|
913
|
+
function logDir() {
|
|
914
|
+
return path2.join(homeDir(), "logs");
|
|
915
|
+
}
|
|
916
|
+
function logFile() {
|
|
917
|
+
return path2.join(logDir(), "token-derby.log");
|
|
918
|
+
}
|
|
913
919
|
|
|
914
920
|
// src/identity/identity.ts
|
|
915
921
|
async function readIdentityFile() {
|
|
@@ -964,6 +970,63 @@ function validateDisplayName(name) {
|
|
|
964
970
|
return { ok: true, name: trimmed };
|
|
965
971
|
}
|
|
966
972
|
|
|
973
|
+
// src/log/logger.ts
|
|
974
|
+
import * as fs2 from "fs";
|
|
975
|
+
var SECRET_KEY = /token|secret|authorization|credential|password/i;
|
|
976
|
+
function redact(fields) {
|
|
977
|
+
const out = {};
|
|
978
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
979
|
+
out[key] = SECRET_KEY.test(key) ? "[redacted]" : value;
|
|
980
|
+
}
|
|
981
|
+
return out;
|
|
982
|
+
}
|
|
983
|
+
function formatLine(at, level, event, fields) {
|
|
984
|
+
const body = fields && Object.keys(fields).length > 0 ? ` ${JSON.stringify(redact(fields))}` : "";
|
|
985
|
+
return `${at.toISOString()} ${level.padEnd(5)} ${event}${body}
|
|
986
|
+
`;
|
|
987
|
+
}
|
|
988
|
+
var MAX_BYTES = 2e6;
|
|
989
|
+
var MAX_FILES = 5;
|
|
990
|
+
var currentBytes = null;
|
|
991
|
+
var disabled = false;
|
|
992
|
+
function rotate() {
|
|
993
|
+
const base = logFile();
|
|
994
|
+
fs2.rmSync(`${base}.${MAX_FILES - 1}`, { force: true });
|
|
995
|
+
for (let i = MAX_FILES - 2; i >= 1; i--) {
|
|
996
|
+
if (fs2.existsSync(`${base}.${i}`)) fs2.renameSync(`${base}.${i}`, `${base}.${i + 1}`);
|
|
997
|
+
}
|
|
998
|
+
if (fs2.existsSync(base)) fs2.renameSync(base, `${base}.1`);
|
|
999
|
+
currentBytes = 0;
|
|
1000
|
+
}
|
|
1001
|
+
function write(level, event, fields) {
|
|
1002
|
+
if (disabled) return;
|
|
1003
|
+
try {
|
|
1004
|
+
append(level, event, fields);
|
|
1005
|
+
} catch {
|
|
1006
|
+
disabled = true;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function append(level, event, fields) {
|
|
1010
|
+
const line = formatLine(/* @__PURE__ */ new Date(), level, event, fields);
|
|
1011
|
+
const bytes = Buffer.byteLength(line);
|
|
1012
|
+
fs2.mkdirSync(logDir(), { recursive: true });
|
|
1013
|
+
if (currentBytes === null) {
|
|
1014
|
+
currentBytes = fs2.existsSync(logFile()) ? fs2.statSync(logFile()).size : 0;
|
|
1015
|
+
}
|
|
1016
|
+
if (currentBytes > 0 && currentBytes + bytes > MAX_BYTES) rotate();
|
|
1017
|
+
fs2.appendFileSync(logFile(), line, "utf8");
|
|
1018
|
+
currentBytes += bytes;
|
|
1019
|
+
}
|
|
1020
|
+
function logInfo(event, fields) {
|
|
1021
|
+
write("INFO", event, fields);
|
|
1022
|
+
}
|
|
1023
|
+
function logWarn(event, fields) {
|
|
1024
|
+
write("WARN", event, fields);
|
|
1025
|
+
}
|
|
1026
|
+
function logError(event, fields) {
|
|
1027
|
+
write("ERROR", event, fields);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
967
1030
|
// src/api/client.ts
|
|
968
1031
|
var ApiError = class extends Error {
|
|
969
1032
|
constructor(code, message, status) {
|
|
@@ -975,6 +1038,10 @@ var ApiError = class extends Error {
|
|
|
975
1038
|
code;
|
|
976
1039
|
status;
|
|
977
1040
|
};
|
|
1041
|
+
var SECRET_SEGMENT = /^\/(claims|races\/admin)\/[^/]+/;
|
|
1042
|
+
function loggablePath(path9) {
|
|
1043
|
+
return path9.replace(SECRET_SEGMENT, (match) => `${match.slice(0, match.lastIndexOf("/") + 1)}[redacted]`);
|
|
1044
|
+
}
|
|
978
1045
|
var identityCache = null;
|
|
979
1046
|
function getIdentity() {
|
|
980
1047
|
if (!identityCache) identityCache = loadIdentity();
|
|
@@ -995,6 +1062,9 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
|
|
|
995
1062
|
}
|
|
996
1063
|
if (horseAuthToken) headers["authorization"] = `Bearer ${horseAuthToken}`;
|
|
997
1064
|
if (body !== void 0) headers["content-type"] = "application/json";
|
|
1065
|
+
const safePath = loggablePath(path9);
|
|
1066
|
+
logInfo("http.req", { method, path: safePath });
|
|
1067
|
+
const startedAt2 = Date.now();
|
|
998
1068
|
let res;
|
|
999
1069
|
try {
|
|
1000
1070
|
res = await fetchImpl(url, {
|
|
@@ -1003,8 +1073,10 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
|
|
|
1003
1073
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1004
1074
|
});
|
|
1005
1075
|
} catch (e) {
|
|
1076
|
+
logError("http.err", { method, path: safePath, ms: Date.now() - startedAt2, message: e?.message ?? "fetch failed" });
|
|
1006
1077
|
throw new ApiError("NETWORK_ERROR", e?.message ?? "fetch failed", 0);
|
|
1007
1078
|
}
|
|
1079
|
+
logInfo("http.res", { method, path: safePath, status: res.status, ms: Date.now() - startedAt2 });
|
|
1008
1080
|
const text = await res.text();
|
|
1009
1081
|
const contentType = res.headers.get("content-type") ?? "";
|
|
1010
1082
|
let parsed = null;
|
|
@@ -1021,6 +1093,14 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
|
|
|
1021
1093
|
}
|
|
1022
1094
|
throw new ApiError("NETWORK_ERROR", `HTTP ${res.status}`, res.status);
|
|
1023
1095
|
}
|
|
1096
|
+
if (parsed === null) {
|
|
1097
|
+
const got = contentType || "no content-type";
|
|
1098
|
+
throw new ApiError(
|
|
1099
|
+
"NETWORK_ERROR",
|
|
1100
|
+
`Expected JSON from ${method} ${safePath} but the server returned ${got} (HTTP ${res.status}). The API may be unavailable or the request was rejected upstream.`,
|
|
1101
|
+
res.status
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1024
1104
|
return parsed;
|
|
1025
1105
|
}
|
|
1026
1106
|
|
|
@@ -1532,11 +1612,11 @@ function PrimaryPicker({ onPick }) {
|
|
|
1532
1612
|
}
|
|
1533
1613
|
|
|
1534
1614
|
// src/stable/active-race.ts
|
|
1535
|
-
import * as
|
|
1615
|
+
import * as fs3 from "fs/promises";
|
|
1536
1616
|
import * as path4 from "path";
|
|
1537
1617
|
async function saveActiveRace(active) {
|
|
1538
|
-
await
|
|
1539
|
-
await
|
|
1618
|
+
await fs3.mkdir(activeRacesDir(), { recursive: true });
|
|
1619
|
+
await fs3.writeFile(
|
|
1540
1620
|
activeRaceFile(active.join_code),
|
|
1541
1621
|
JSON.stringify(active, null, 2) + "\n",
|
|
1542
1622
|
"utf8"
|
|
@@ -1709,6 +1789,7 @@ function runHeartbeatLoop(opts) {
|
|
|
1709
1789
|
let stopped = false;
|
|
1710
1790
|
let pending = null;
|
|
1711
1791
|
const stop = () => {
|
|
1792
|
+
if (!stopped) logInfo("beat.stop", { retry: retryIndex });
|
|
1712
1793
|
stopped = true;
|
|
1713
1794
|
if (timer) clearTimeout(timer);
|
|
1714
1795
|
timer = null;
|
|
@@ -1721,9 +1802,16 @@ function runHeartbeatLoop(opts) {
|
|
|
1721
1802
|
const tick = async () => {
|
|
1722
1803
|
if (stopped) return;
|
|
1723
1804
|
try {
|
|
1724
|
-
if (!pending)
|
|
1805
|
+
if (!pending) {
|
|
1806
|
+
logInfo("beat.prepare.start");
|
|
1807
|
+
const startedAt2 = Date.now();
|
|
1808
|
+
pending = await opts.prepareBeat();
|
|
1809
|
+
logInfo("beat.prepare.done", { seq: pending.seq, ms: Date.now() - startedAt2 });
|
|
1810
|
+
}
|
|
1725
1811
|
const snapshot = pending;
|
|
1812
|
+
const sentAt = Date.now();
|
|
1726
1813
|
const resp = await opts.sendBeat(snapshot);
|
|
1814
|
+
logInfo("beat.send.ok", { seq: snapshot.seq, ms: Date.now() - sentAt, race_status: resp.race_status });
|
|
1727
1815
|
pending = null;
|
|
1728
1816
|
retryIndex = 0;
|
|
1729
1817
|
opts.onSuccess(resp, snapshot);
|
|
@@ -1736,6 +1824,13 @@ function runHeartbeatLoop(opts) {
|
|
|
1736
1824
|
} catch (err) {
|
|
1737
1825
|
opts.onError(err);
|
|
1738
1826
|
const delay = opts.retryDelaysMs[Math.min(retryIndex, opts.retryDelaysMs.length - 1)] ?? 1e3;
|
|
1827
|
+
logError("beat.send.err", {
|
|
1828
|
+
seq: pending?.seq,
|
|
1829
|
+
code: err?.code,
|
|
1830
|
+
message: err?.message ?? String(err),
|
|
1831
|
+
retry: retryIndex,
|
|
1832
|
+
next_ms: delay
|
|
1833
|
+
});
|
|
1739
1834
|
retryIndex += 1;
|
|
1740
1835
|
schedule(delay);
|
|
1741
1836
|
}
|
|
@@ -1744,7 +1839,7 @@ function runHeartbeatLoop(opts) {
|
|
|
1744
1839
|
}
|
|
1745
1840
|
|
|
1746
1841
|
// src/tokens/transcripts.ts
|
|
1747
|
-
import * as
|
|
1842
|
+
import * as fs5 from "fs/promises";
|
|
1748
1843
|
import * as path6 from "path";
|
|
1749
1844
|
|
|
1750
1845
|
// src/tokens/pool.ts
|
|
@@ -1764,7 +1859,7 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
1764
1859
|
}
|
|
1765
1860
|
|
|
1766
1861
|
// src/tokens/scan-cache.ts
|
|
1767
|
-
import * as
|
|
1862
|
+
import * as fs4 from "fs/promises";
|
|
1768
1863
|
import * as path5 from "path";
|
|
1769
1864
|
var CACHE_VERSION = 1;
|
|
1770
1865
|
function isEntry(v) {
|
|
@@ -1802,7 +1897,7 @@ var ScanCache = class _ScanCache {
|
|
|
1802
1897
|
* re-reading that line once the writer completes it.
|
|
1803
1898
|
*/
|
|
1804
1899
|
async readIncremental(file, fold) {
|
|
1805
|
-
const st = await
|
|
1900
|
+
const st = await fs4.stat(file);
|
|
1806
1901
|
const prev = this.entries.get(file);
|
|
1807
1902
|
this.touched.add(file);
|
|
1808
1903
|
if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
|
|
@@ -1819,11 +1914,11 @@ var ScanCache = class _ScanCache {
|
|
|
1819
1914
|
* chats). Gated on mtime+size, recomputed in full whenever either moves.
|
|
1820
1915
|
*/
|
|
1821
1916
|
async readWhenChanged(file, compute) {
|
|
1822
|
-
const st = await
|
|
1917
|
+
const st = await fs4.stat(file);
|
|
1823
1918
|
const prev = this.entries.get(file);
|
|
1824
1919
|
this.touched.add(file);
|
|
1825
1920
|
if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
|
|
1826
|
-
const value = await compute(await
|
|
1921
|
+
const value = await compute(await fs4.readFile(file, "utf8"));
|
|
1827
1922
|
this.entries.set(file, { mtimeMs: st.mtimeMs, size: st.size, offset: st.size, value });
|
|
1828
1923
|
return value;
|
|
1829
1924
|
}
|
|
@@ -1835,9 +1930,9 @@ var ScanCache = class _ScanCache {
|
|
|
1835
1930
|
const target = cacheFile(this.source);
|
|
1836
1931
|
const tmp = `${target}.tmp`;
|
|
1837
1932
|
try {
|
|
1838
|
-
await
|
|
1839
|
-
await
|
|
1840
|
-
await
|
|
1933
|
+
await fs4.mkdir(path5.dirname(target), { recursive: true });
|
|
1934
|
+
await fs4.writeFile(tmp, JSON.stringify({ version: CACHE_VERSION, files: Object.fromEntries(this.entries) }));
|
|
1935
|
+
await fs4.rename(tmp, target);
|
|
1841
1936
|
} catch {
|
|
1842
1937
|
}
|
|
1843
1938
|
}
|
|
@@ -1848,7 +1943,7 @@ function cacheFile(source) {
|
|
|
1848
1943
|
async function loadEntries(source) {
|
|
1849
1944
|
let parsed;
|
|
1850
1945
|
try {
|
|
1851
|
-
parsed = JSON.parse(await
|
|
1946
|
+
parsed = JSON.parse(await fs4.readFile(cacheFile(source), "utf8"));
|
|
1852
1947
|
} catch {
|
|
1853
1948
|
return /* @__PURE__ */ new Map();
|
|
1854
1949
|
}
|
|
@@ -1863,7 +1958,7 @@ async function loadEntries(source) {
|
|
|
1863
1958
|
}
|
|
1864
1959
|
async function readCompleteLines(file, start, end) {
|
|
1865
1960
|
if (end <= start) return { lines: [], tail: null, consumedTo: start };
|
|
1866
|
-
const fh = await
|
|
1961
|
+
const fh = await fs4.open(file, "r");
|
|
1867
1962
|
try {
|
|
1868
1963
|
const buf = Buffer.allocUnsafe(end - start);
|
|
1869
1964
|
const { bytesRead } = await fh.read(buf, 0, end - start, start);
|
|
@@ -1956,13 +2051,13 @@ async function collectJsonl(dir, depth, out) {
|
|
|
1956
2051
|
}
|
|
1957
2052
|
}
|
|
1958
2053
|
async function readEntries(dir, failLoud) {
|
|
1959
|
-
if (failLoud) return readRoot(dir, () =>
|
|
1960
|
-
return
|
|
2054
|
+
if (failLoud) return readRoot(dir, () => fs5.readdir(dir, { withFileTypes: true }));
|
|
2055
|
+
return fs5.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1961
2056
|
}
|
|
1962
2057
|
async function isDirectory(entry, parent) {
|
|
1963
2058
|
if (entry.isDirectory()) return true;
|
|
1964
2059
|
if (!entry.isSymbolicLink()) return false;
|
|
1965
|
-
return
|
|
2060
|
+
return fs5.stat(path6.join(parent, entry.name)).then((st) => st.isDirectory()).catch(() => false);
|
|
1966
2061
|
}
|
|
1967
2062
|
function addNum(value) {
|
|
1968
2063
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
@@ -1989,14 +2084,14 @@ var CLAUDE_FOLD = {
|
|
|
1989
2084
|
};
|
|
1990
2085
|
|
|
1991
2086
|
// src/tokens/codex.ts
|
|
1992
|
-
import * as
|
|
2087
|
+
import * as fs6 from "fs/promises";
|
|
1993
2088
|
import * as path7 from "path";
|
|
1994
2089
|
function num(v) {
|
|
1995
2090
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1996
2091
|
}
|
|
1997
2092
|
async function sumCodexByConversation() {
|
|
1998
2093
|
const root = codexSessionsDir();
|
|
1999
|
-
await readRoot(root, () =>
|
|
2094
|
+
await readRoot(root, () => fs6.stat(root));
|
|
2000
2095
|
const files = await listCodexRollouts(root);
|
|
2001
2096
|
const cache = await ScanCache.open("codex");
|
|
2002
2097
|
const totals = await mapWithConcurrency(
|
|
@@ -2028,7 +2123,7 @@ async function listCodexRollouts(root) {
|
|
|
2028
2123
|
async function collectRollouts(dir) {
|
|
2029
2124
|
let entries;
|
|
2030
2125
|
try {
|
|
2031
|
-
entries = await
|
|
2126
|
+
entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
2032
2127
|
} catch (e) {
|
|
2033
2128
|
if (e?.code === "ENOENT") return [];
|
|
2034
2129
|
throw e;
|
|
@@ -2066,7 +2161,7 @@ var CODEX_FOLD = {
|
|
|
2066
2161
|
};
|
|
2067
2162
|
|
|
2068
2163
|
// src/tokens/gemini.ts
|
|
2069
|
-
import * as
|
|
2164
|
+
import * as fs7 from "fs/promises";
|
|
2070
2165
|
import * as path8 from "path";
|
|
2071
2166
|
function num2(v) {
|
|
2072
2167
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
@@ -2095,13 +2190,13 @@ async function sumGeminiTokens() {
|
|
|
2095
2190
|
return { input, output };
|
|
2096
2191
|
}
|
|
2097
2192
|
async function listChatFiles(root) {
|
|
2098
|
-
const entries = await readRoot(root, () =>
|
|
2193
|
+
const entries = await readRoot(root, () => fs7.readdir(root));
|
|
2099
2194
|
const out = [];
|
|
2100
2195
|
for (const entry of entries) {
|
|
2101
2196
|
const chatsDir = path8.join(root, entry, "chats");
|
|
2102
2197
|
let files;
|
|
2103
2198
|
try {
|
|
2104
|
-
files = await
|
|
2199
|
+
files = await fs7.readdir(chatsDir);
|
|
2105
2200
|
} catch {
|
|
2106
2201
|
continue;
|
|
2107
2202
|
}
|
|
@@ -2153,11 +2248,24 @@ async function scanWithTimeout(scan, timeoutMs, describeTimeout) {
|
|
|
2153
2248
|
const budget = new Promise((resolve) => {
|
|
2154
2249
|
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
2155
2250
|
});
|
|
2251
|
+
const startedAt2 = Date.now();
|
|
2156
2252
|
try {
|
|
2157
2253
|
const result = await Promise.race([scan(), budget]);
|
|
2158
|
-
if (result !== TIMED_OUT)
|
|
2254
|
+
if (result !== TIMED_OUT) {
|
|
2255
|
+
if (isStall(result)) logWarn("scan.stall", { reason: result.stall, ms: Date.now() - startedAt2 });
|
|
2256
|
+
return result;
|
|
2257
|
+
}
|
|
2159
2258
|
const detail = describeTimeout ? await describeTimeout() : null;
|
|
2160
|
-
|
|
2259
|
+
const stall = detail ?? `Token scan timed out after ${Math.round(timeoutMs / 1e3)}s`;
|
|
2260
|
+
logWarn("scan.timeout", { budget_ms: timeoutMs, reason: stall });
|
|
2261
|
+
return { stall };
|
|
2262
|
+
} catch (err) {
|
|
2263
|
+
logError("scan.error", {
|
|
2264
|
+
message: err?.message ?? String(err),
|
|
2265
|
+
stack: err?.stack,
|
|
2266
|
+
ms: Date.now() - startedAt2
|
|
2267
|
+
});
|
|
2268
|
+
throw err;
|
|
2161
2269
|
} finally {
|
|
2162
2270
|
clearTimeout(timer);
|
|
2163
2271
|
}
|
|
@@ -2184,7 +2292,12 @@ async function readAllSources(race, primary, progress) {
|
|
|
2184
2292
|
const secondaryKeys = MODEL_KEYS.filter((k) => k !== primary);
|
|
2185
2293
|
const secondaryScans = secondaryKeys.map((k) => {
|
|
2186
2294
|
progress?.begin(k);
|
|
2187
|
-
return SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch(() =>
|
|
2295
|
+
return SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch((err) => {
|
|
2296
|
+
if (!(err instanceof SourceRootMissing)) {
|
|
2297
|
+
logWarn("scan.source.err", { source: k, message: err?.message ?? String(err) });
|
|
2298
|
+
}
|
|
2299
|
+
return 0;
|
|
2300
|
+
}).finally(() => progress?.end(k));
|
|
2188
2301
|
});
|
|
2189
2302
|
const [primaryResult, secondaryValues] = await Promise.all([
|
|
2190
2303
|
primaryScan,
|
|
@@ -2378,7 +2491,7 @@ var RaceScoreTracker = class {
|
|
|
2378
2491
|
};
|
|
2379
2492
|
|
|
2380
2493
|
// src/tokens/source-probe.ts
|
|
2381
|
-
import * as
|
|
2494
|
+
import * as fs8 from "fs/promises";
|
|
2382
2495
|
var ROOTS = {
|
|
2383
2496
|
claude: claudeProjectsDir,
|
|
2384
2497
|
codex: codexSessionsDir,
|
|
@@ -2395,9 +2508,9 @@ function sourceDir(key) {
|
|
|
2395
2508
|
}
|
|
2396
2509
|
async function probeSource(key) {
|
|
2397
2510
|
const dir = ROOTS[key]();
|
|
2398
|
-
const exists = await
|
|
2511
|
+
const exists = await fs8.stat(dir).then((st) => st.isDirectory()).catch(() => false);
|
|
2399
2512
|
if (!exists) return { key, dir, exists: false, projects: 0, transcripts: 0 };
|
|
2400
|
-
const entries = await
|
|
2513
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2401
2514
|
const projects = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).length;
|
|
2402
2515
|
const files = await LISTERS[key](dir).catch(() => []);
|
|
2403
2516
|
return { key, dir, exists: true, projects, transcripts: files.length };
|
|
@@ -4077,6 +4190,31 @@ function envCommand(arg) {
|
|
|
4077
4190
|
return 0;
|
|
4078
4191
|
}
|
|
4079
4192
|
|
|
4193
|
+
// src/commands/logs.ts
|
|
4194
|
+
import * as fs9 from "fs/promises";
|
|
4195
|
+
import { existsSync as existsSync2 } from "fs";
|
|
4196
|
+
var DEFAULT_TAIL_LINES = 50;
|
|
4197
|
+
function tailCount(argv) {
|
|
4198
|
+
const i = argv.indexOf("--tail");
|
|
4199
|
+
if (i === -1) return null;
|
|
4200
|
+
const n = Number(argv[i + 1]);
|
|
4201
|
+
return Number.isInteger(n) && n > 0 ? n : DEFAULT_TAIL_LINES;
|
|
4202
|
+
}
|
|
4203
|
+
async function logsCommand(argv) {
|
|
4204
|
+
const file = logFile();
|
|
4205
|
+
if (!existsSync2(file)) {
|
|
4206
|
+
console.log(`No log file yet \u2014 it appears at ${file} the first time a command runs.`);
|
|
4207
|
+
return 0;
|
|
4208
|
+
}
|
|
4209
|
+
console.log(file);
|
|
4210
|
+
const n = tailCount(argv);
|
|
4211
|
+
if (n === null) return 0;
|
|
4212
|
+
const lines = (await fs9.readFile(file, "utf8")).split("\n").filter((l) => l.length > 0);
|
|
4213
|
+
console.log("");
|
|
4214
|
+
for (const line of lines.slice(-n)) console.log(line);
|
|
4215
|
+
return 0;
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4080
4218
|
// src/bin.ts
|
|
4081
4219
|
var HELP = `token-derby v${CLI_VERSION}
|
|
4082
4220
|
|
|
@@ -4105,6 +4243,8 @@ Identity:
|
|
|
4105
4243
|
|
|
4106
4244
|
Maintenance:
|
|
4107
4245
|
token-derby update Check for and install the latest CLI version
|
|
4246
|
+
token-derby logs Show the path of the debug log
|
|
4247
|
+
token-derby logs --tail [n] Print the last n log lines (default 50)
|
|
4108
4248
|
|
|
4109
4249
|
Stable management:
|
|
4110
4250
|
token-derby stable create Make a new horse (interactive)
|
|
@@ -4141,9 +4281,25 @@ Environment:
|
|
|
4141
4281
|
TOKEN_DERBY_API_BASE Hard-override API base URL (wins over env)
|
|
4142
4282
|
TOKEN_DERBY_HOME Hard-override identity/stable directory
|
|
4143
4283
|
`;
|
|
4284
|
+
function describeInvocation(argv) {
|
|
4285
|
+
const cmd = argv[0] ?? "(none)";
|
|
4286
|
+
const container = cmd === "stable" || cmd === "organisation" || cmd === "org";
|
|
4287
|
+
return {
|
|
4288
|
+
cmd,
|
|
4289
|
+
sub: container ? argv[1] : void 0,
|
|
4290
|
+
flags: argv.filter((a) => a.startsWith("--")).map((a) => a.split("=")[0])
|
|
4291
|
+
};
|
|
4292
|
+
}
|
|
4144
4293
|
async function main() {
|
|
4145
4294
|
const argv = process.argv.slice(2);
|
|
4146
4295
|
const cmd = argv[0];
|
|
4296
|
+
logInfo("cmd.start", {
|
|
4297
|
+
...describeInvocation(argv),
|
|
4298
|
+
version: CLI_VERSION,
|
|
4299
|
+
node: process.version,
|
|
4300
|
+
pid: process.pid,
|
|
4301
|
+
env: selectedEnv()
|
|
4302
|
+
});
|
|
4147
4303
|
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
4148
4304
|
console.log(HELP);
|
|
4149
4305
|
return 0;
|
|
@@ -4159,6 +4315,7 @@ async function main() {
|
|
|
4159
4315
|
if (cmd === "login") return loginCommand(argv.slice(1));
|
|
4160
4316
|
if (cmd === "update") return updateCommand();
|
|
4161
4317
|
if (cmd === "env") return envCommand(argv[1]);
|
|
4318
|
+
if (cmd === "logs") return logsCommand(argv.slice(1));
|
|
4162
4319
|
const identity = await loadIdentity();
|
|
4163
4320
|
if (!identity) {
|
|
4164
4321
|
console.error("Run `token-derby login` to set up your identity before using any other command.");
|
|
@@ -4205,9 +4362,33 @@ function parseFlag(args, flag) {
|
|
|
4205
4362
|
}
|
|
4206
4363
|
return void 0;
|
|
4207
4364
|
}
|
|
4365
|
+
var CRASH_HANDLERS_INSTALLED = /* @__PURE__ */ Symbol.for("token-derby.crash-handlers");
|
|
4366
|
+
if (!(CRASH_HANDLERS_INSTALLED in process)) {
|
|
4367
|
+
process[CRASH_HANDLERS_INSTALLED] = true;
|
|
4368
|
+
process.on("uncaughtException", (err) => {
|
|
4369
|
+
logError("cmd.uncaught", { message: err?.message ?? String(err), stack: err?.stack });
|
|
4370
|
+
console.error(err?.stack ?? err);
|
|
4371
|
+
process.exit(1);
|
|
4372
|
+
});
|
|
4373
|
+
process.on("unhandledRejection", (reason) => {
|
|
4374
|
+
logError("cmd.unhandled", {
|
|
4375
|
+
message: reason?.message ?? String(reason),
|
|
4376
|
+
stack: reason?.stack
|
|
4377
|
+
});
|
|
4378
|
+
});
|
|
4379
|
+
}
|
|
4380
|
+
var startedAt = Date.now();
|
|
4208
4381
|
main().then(
|
|
4209
|
-
(code) =>
|
|
4382
|
+
(code) => {
|
|
4383
|
+
logInfo("cmd.exit", { code, ms: Date.now() - startedAt });
|
|
4384
|
+
process.exit(code);
|
|
4385
|
+
},
|
|
4210
4386
|
(err) => {
|
|
4387
|
+
logError("cmd.crash", {
|
|
4388
|
+
message: err?.message ?? String(err),
|
|
4389
|
+
stack: err?.stack,
|
|
4390
|
+
ms: Date.now() - startedAt
|
|
4391
|
+
});
|
|
4211
4392
|
console.error(err?.stack ?? err);
|
|
4212
4393
|
process.exit(1);
|
|
4213
4394
|
}
|