@mauricode/token-derby 3.1.0 → 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 +34 -0
- package/dist/bin.js +381 -66
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -857,12 +857,13 @@ function apiBase() {
|
|
|
857
857
|
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
858
858
|
var SCAN_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 0.75;
|
|
859
859
|
var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
|
|
860
|
+
var PRIMARY_SILENT_THRESHOLD = 10;
|
|
860
861
|
|
|
861
862
|
// src/version.ts
|
|
862
863
|
import { createRequire } from "module";
|
|
863
864
|
function readVersion() {
|
|
864
|
-
if ("3.1.
|
|
865
|
-
return "3.1.
|
|
865
|
+
if ("3.1.3".length > 0) {
|
|
866
|
+
return "3.1.3";
|
|
866
867
|
}
|
|
867
868
|
try {
|
|
868
869
|
const req = createRequire(import.meta.url);
|
|
@@ -897,7 +898,11 @@ function activeRacesDir() {
|
|
|
897
898
|
return path2.join(homeDir(), "active-races");
|
|
898
899
|
}
|
|
899
900
|
function claudeProjectsDir() {
|
|
900
|
-
|
|
901
|
+
const override = process.env.TOKEN_DERBY_CLAUDE_DIR;
|
|
902
|
+
if (override) return override;
|
|
903
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
904
|
+
if (configDir) return path2.join(configDir, "projects");
|
|
905
|
+
return path2.join(os2.homedir(), ".claude", "projects");
|
|
901
906
|
}
|
|
902
907
|
function codexSessionsDir() {
|
|
903
908
|
return process.env.TOKEN_DERBY_CODEX_DIR ?? path2.join(os2.homedir(), ".codex");
|
|
@@ -905,6 +910,12 @@ function codexSessionsDir() {
|
|
|
905
910
|
function geminiTmpDir() {
|
|
906
911
|
return process.env.TOKEN_DERBY_GEMINI_DIR ?? path2.join(os2.homedir(), ".gemini", "tmp");
|
|
907
912
|
}
|
|
913
|
+
function logDir() {
|
|
914
|
+
return path2.join(homeDir(), "logs");
|
|
915
|
+
}
|
|
916
|
+
function logFile() {
|
|
917
|
+
return path2.join(logDir(), "token-derby.log");
|
|
918
|
+
}
|
|
908
919
|
|
|
909
920
|
// src/identity/identity.ts
|
|
910
921
|
async function readIdentityFile() {
|
|
@@ -959,6 +970,63 @@ function validateDisplayName(name) {
|
|
|
959
970
|
return { ok: true, name: trimmed };
|
|
960
971
|
}
|
|
961
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
|
+
|
|
962
1030
|
// src/api/client.ts
|
|
963
1031
|
var ApiError = class extends Error {
|
|
964
1032
|
constructor(code, message, status) {
|
|
@@ -970,6 +1038,10 @@ var ApiError = class extends Error {
|
|
|
970
1038
|
code;
|
|
971
1039
|
status;
|
|
972
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
|
+
}
|
|
973
1045
|
var identityCache = null;
|
|
974
1046
|
function getIdentity() {
|
|
975
1047
|
if (!identityCache) identityCache = loadIdentity();
|
|
@@ -990,6 +1062,9 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
|
|
|
990
1062
|
}
|
|
991
1063
|
if (horseAuthToken) headers["authorization"] = `Bearer ${horseAuthToken}`;
|
|
992
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();
|
|
993
1068
|
let res;
|
|
994
1069
|
try {
|
|
995
1070
|
res = await fetchImpl(url, {
|
|
@@ -998,8 +1073,10 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
|
|
|
998
1073
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
999
1074
|
});
|
|
1000
1075
|
} catch (e) {
|
|
1076
|
+
logError("http.err", { method, path: safePath, ms: Date.now() - startedAt2, message: e?.message ?? "fetch failed" });
|
|
1001
1077
|
throw new ApiError("NETWORK_ERROR", e?.message ?? "fetch failed", 0);
|
|
1002
1078
|
}
|
|
1079
|
+
logInfo("http.res", { method, path: safePath, status: res.status, ms: Date.now() - startedAt2 });
|
|
1003
1080
|
const text = await res.text();
|
|
1004
1081
|
const contentType = res.headers.get("content-type") ?? "";
|
|
1005
1082
|
let parsed = null;
|
|
@@ -1016,6 +1093,14 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
|
|
|
1016
1093
|
}
|
|
1017
1094
|
throw new ApiError("NETWORK_ERROR", `HTTP ${res.status}`, res.status);
|
|
1018
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
|
+
}
|
|
1019
1104
|
return parsed;
|
|
1020
1105
|
}
|
|
1021
1106
|
|
|
@@ -1527,11 +1612,11 @@ function PrimaryPicker({ onPick }) {
|
|
|
1527
1612
|
}
|
|
1528
1613
|
|
|
1529
1614
|
// src/stable/active-race.ts
|
|
1530
|
-
import * as
|
|
1615
|
+
import * as fs3 from "fs/promises";
|
|
1531
1616
|
import * as path4 from "path";
|
|
1532
1617
|
async function saveActiveRace(active) {
|
|
1533
|
-
await
|
|
1534
|
-
await
|
|
1618
|
+
await fs3.mkdir(activeRacesDir(), { recursive: true });
|
|
1619
|
+
await fs3.writeFile(
|
|
1535
1620
|
activeRaceFile(active.join_code),
|
|
1536
1621
|
JSON.stringify(active, null, 2) + "\n",
|
|
1537
1622
|
"utf8"
|
|
@@ -1559,7 +1644,7 @@ function ModelList(props) {
|
|
|
1559
1644
|
] }) });
|
|
1560
1645
|
}
|
|
1561
1646
|
function StatusScreen(props) {
|
|
1562
|
-
const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, stallReason, primaryModel } = props;
|
|
1647
|
+
const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, stallReason, primarySilent, primarySourceDir, primaryModel } = props;
|
|
1563
1648
|
if (!race) {
|
|
1564
1649
|
return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx7(Text7, { children: "Joining race\u2026" }) });
|
|
1565
1650
|
}
|
|
@@ -1630,6 +1715,15 @@ function StatusScreen(props) {
|
|
|
1630
1715
|
"\u26A0 ",
|
|
1631
1716
|
stallReason ?? "Can't read token usage",
|
|
1632
1717
|
". Your race continues."
|
|
1718
|
+
] }),
|
|
1719
|
+
!stalled && primarySilent && /* @__PURE__ */ jsxs5(Text7, { color: "yellow", children: [
|
|
1720
|
+
"\u26A0 No ",
|
|
1721
|
+
MODEL_LABELS[primaryModel ?? "claude"],
|
|
1722
|
+
" transcripts in ",
|
|
1723
|
+
PRIMARY_SILENT_THRESHOLD,
|
|
1724
|
+
" beats",
|
|
1725
|
+
primarySourceDir ? ` \u2014 nothing under ${primarySourceDir}` : "",
|
|
1726
|
+
". Your race continues, but your horse cannot move until they can be read."
|
|
1633
1727
|
] })
|
|
1634
1728
|
] }),
|
|
1635
1729
|
primaryModel && /* @__PURE__ */ jsx7(ModelList, { primaryModel }),
|
|
@@ -1695,6 +1789,7 @@ function runHeartbeatLoop(opts) {
|
|
|
1695
1789
|
let stopped = false;
|
|
1696
1790
|
let pending = null;
|
|
1697
1791
|
const stop = () => {
|
|
1792
|
+
if (!stopped) logInfo("beat.stop", { retry: retryIndex });
|
|
1698
1793
|
stopped = true;
|
|
1699
1794
|
if (timer) clearTimeout(timer);
|
|
1700
1795
|
timer = null;
|
|
@@ -1707,9 +1802,16 @@ function runHeartbeatLoop(opts) {
|
|
|
1707
1802
|
const tick = async () => {
|
|
1708
1803
|
if (stopped) return;
|
|
1709
1804
|
try {
|
|
1710
|
-
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
|
+
}
|
|
1711
1811
|
const snapshot = pending;
|
|
1812
|
+
const sentAt = Date.now();
|
|
1712
1813
|
const resp = await opts.sendBeat(snapshot);
|
|
1814
|
+
logInfo("beat.send.ok", { seq: snapshot.seq, ms: Date.now() - sentAt, race_status: resp.race_status });
|
|
1713
1815
|
pending = null;
|
|
1714
1816
|
retryIndex = 0;
|
|
1715
1817
|
opts.onSuccess(resp, snapshot);
|
|
@@ -1722,6 +1824,13 @@ function runHeartbeatLoop(opts) {
|
|
|
1722
1824
|
} catch (err) {
|
|
1723
1825
|
opts.onError(err);
|
|
1724
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
|
+
});
|
|
1725
1834
|
retryIndex += 1;
|
|
1726
1835
|
schedule(delay);
|
|
1727
1836
|
}
|
|
@@ -1730,7 +1839,7 @@ function runHeartbeatLoop(opts) {
|
|
|
1730
1839
|
}
|
|
1731
1840
|
|
|
1732
1841
|
// src/tokens/transcripts.ts
|
|
1733
|
-
import * as
|
|
1842
|
+
import * as fs5 from "fs/promises";
|
|
1734
1843
|
import * as path6 from "path";
|
|
1735
1844
|
|
|
1736
1845
|
// src/tokens/pool.ts
|
|
@@ -1750,7 +1859,7 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
1750
1859
|
}
|
|
1751
1860
|
|
|
1752
1861
|
// src/tokens/scan-cache.ts
|
|
1753
|
-
import * as
|
|
1862
|
+
import * as fs4 from "fs/promises";
|
|
1754
1863
|
import * as path5 from "path";
|
|
1755
1864
|
var CACHE_VERSION = 1;
|
|
1756
1865
|
function isEntry(v) {
|
|
@@ -1788,7 +1897,7 @@ var ScanCache = class _ScanCache {
|
|
|
1788
1897
|
* re-reading that line once the writer completes it.
|
|
1789
1898
|
*/
|
|
1790
1899
|
async readIncremental(file, fold) {
|
|
1791
|
-
const st = await
|
|
1900
|
+
const st = await fs4.stat(file);
|
|
1792
1901
|
const prev = this.entries.get(file);
|
|
1793
1902
|
this.touched.add(file);
|
|
1794
1903
|
if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
|
|
@@ -1805,11 +1914,11 @@ var ScanCache = class _ScanCache {
|
|
|
1805
1914
|
* chats). Gated on mtime+size, recomputed in full whenever either moves.
|
|
1806
1915
|
*/
|
|
1807
1916
|
async readWhenChanged(file, compute) {
|
|
1808
|
-
const st = await
|
|
1917
|
+
const st = await fs4.stat(file);
|
|
1809
1918
|
const prev = this.entries.get(file);
|
|
1810
1919
|
this.touched.add(file);
|
|
1811
1920
|
if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
|
|
1812
|
-
const value = await compute(await
|
|
1921
|
+
const value = await compute(await fs4.readFile(file, "utf8"));
|
|
1813
1922
|
this.entries.set(file, { mtimeMs: st.mtimeMs, size: st.size, offset: st.size, value });
|
|
1814
1923
|
return value;
|
|
1815
1924
|
}
|
|
@@ -1821,9 +1930,9 @@ var ScanCache = class _ScanCache {
|
|
|
1821
1930
|
const target = cacheFile(this.source);
|
|
1822
1931
|
const tmp = `${target}.tmp`;
|
|
1823
1932
|
try {
|
|
1824
|
-
await
|
|
1825
|
-
await
|
|
1826
|
-
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);
|
|
1827
1936
|
} catch {
|
|
1828
1937
|
}
|
|
1829
1938
|
}
|
|
@@ -1834,7 +1943,7 @@ function cacheFile(source) {
|
|
|
1834
1943
|
async function loadEntries(source) {
|
|
1835
1944
|
let parsed;
|
|
1836
1945
|
try {
|
|
1837
|
-
parsed = JSON.parse(await
|
|
1946
|
+
parsed = JSON.parse(await fs4.readFile(cacheFile(source), "utf8"));
|
|
1838
1947
|
} catch {
|
|
1839
1948
|
return /* @__PURE__ */ new Map();
|
|
1840
1949
|
}
|
|
@@ -1849,7 +1958,7 @@ async function loadEntries(source) {
|
|
|
1849
1958
|
}
|
|
1850
1959
|
async function readCompleteLines(file, start, end) {
|
|
1851
1960
|
if (end <= start) return { lines: [], tail: null, consumedTo: start };
|
|
1852
|
-
const fh = await
|
|
1961
|
+
const fh = await fs4.open(file, "r");
|
|
1853
1962
|
try {
|
|
1854
1963
|
const buf = Buffer.allocUnsafe(end - start);
|
|
1855
1964
|
const { bytesRead } = await fh.read(buf, 0, end - start, start);
|
|
@@ -1869,6 +1978,24 @@ async function readCompleteLines(file, start, end) {
|
|
|
1869
1978
|
}
|
|
1870
1979
|
}
|
|
1871
1980
|
|
|
1981
|
+
// src/tokens/source-root.ts
|
|
1982
|
+
var SourceRootMissing = class extends Error {
|
|
1983
|
+
constructor(dir) {
|
|
1984
|
+
super(`No history directory at ${dir}`);
|
|
1985
|
+
this.dir = dir;
|
|
1986
|
+
this.name = "SourceRootMissing";
|
|
1987
|
+
}
|
|
1988
|
+
dir;
|
|
1989
|
+
};
|
|
1990
|
+
async function readRoot(dir, read) {
|
|
1991
|
+
try {
|
|
1992
|
+
return await read();
|
|
1993
|
+
} catch (e) {
|
|
1994
|
+
if (e?.code === "ENOENT") throw new SourceRootMissing(dir);
|
|
1995
|
+
throw e;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1872
1999
|
// src/tokens/transcripts.ts
|
|
1873
2000
|
var MAX_PROJECT_DEPTH = 8;
|
|
1874
2001
|
function conversationId(file, root) {
|
|
@@ -1905,29 +2032,33 @@ async function sumTokens() {
|
|
|
1905
2032
|
return { input, output };
|
|
1906
2033
|
}
|
|
1907
2034
|
async function listJsonlFiles(root) {
|
|
1908
|
-
const
|
|
2035
|
+
const entries = await readEntries(root, true);
|
|
1909
2036
|
const out = [];
|
|
1910
|
-
for (const
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
if (!stat4.isDirectory()) continue;
|
|
1914
|
-
await collectJsonl(projectDir, MAX_PROJECT_DEPTH, out);
|
|
2037
|
+
for (const entry of entries) {
|
|
2038
|
+
if (!await isDirectory(entry, root)) continue;
|
|
2039
|
+
await collectJsonl(path6.join(root, entry.name), MAX_PROJECT_DEPTH, out);
|
|
1915
2040
|
}
|
|
1916
2041
|
return out;
|
|
1917
2042
|
}
|
|
1918
2043
|
async function collectJsonl(dir, depth, out) {
|
|
1919
2044
|
if (depth <= 0) return;
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
const child = path6.join(dir, entry);
|
|
1926
|
-
const st = await fs4.stat(child);
|
|
1927
|
-
if (st.isDirectory()) await collectJsonl(child, depth - 1, out);
|
|
2045
|
+
for (const entry of await readEntries(dir, false)) {
|
|
2046
|
+
if (entry.name.endsWith(".jsonl")) {
|
|
2047
|
+
out.push(path6.join(dir, entry.name));
|
|
2048
|
+
} else if (depth > 1 && await isDirectory(entry, dir)) {
|
|
2049
|
+
await collectJsonl(path6.join(dir, entry.name), depth - 1, out);
|
|
1928
2050
|
}
|
|
1929
2051
|
}
|
|
1930
2052
|
}
|
|
2053
|
+
async function readEntries(dir, failLoud) {
|
|
2054
|
+
if (failLoud) return readRoot(dir, () => fs5.readdir(dir, { withFileTypes: true }));
|
|
2055
|
+
return fs5.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2056
|
+
}
|
|
2057
|
+
async function isDirectory(entry, parent) {
|
|
2058
|
+
if (entry.isDirectory()) return true;
|
|
2059
|
+
if (!entry.isSymbolicLink()) return false;
|
|
2060
|
+
return fs5.stat(path6.join(parent, entry.name)).then((st) => st.isDirectory()).catch(() => false);
|
|
2061
|
+
}
|
|
1931
2062
|
function addNum(value) {
|
|
1932
2063
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1933
2064
|
}
|
|
@@ -1953,18 +2084,15 @@ var CLAUDE_FOLD = {
|
|
|
1953
2084
|
};
|
|
1954
2085
|
|
|
1955
2086
|
// src/tokens/codex.ts
|
|
1956
|
-
import * as
|
|
2087
|
+
import * as fs6 from "fs/promises";
|
|
1957
2088
|
import * as path7 from "path";
|
|
1958
2089
|
function num(v) {
|
|
1959
2090
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1960
2091
|
}
|
|
1961
2092
|
async function sumCodexByConversation() {
|
|
1962
2093
|
const root = codexSessionsDir();
|
|
1963
|
-
await
|
|
1964
|
-
const files =
|
|
1965
|
-
...await collectRollouts(path7.join(root, "sessions")),
|
|
1966
|
-
...await collectRollouts(path7.join(root, "archived_sessions"))
|
|
1967
|
-
];
|
|
2094
|
+
await readRoot(root, () => fs6.stat(root));
|
|
2095
|
+
const files = await listCodexRollouts(root);
|
|
1968
2096
|
const cache = await ScanCache.open("codex");
|
|
1969
2097
|
const totals = await mapWithConcurrency(
|
|
1970
2098
|
files,
|
|
@@ -1986,10 +2114,16 @@ async function sumCodexTokens() {
|
|
|
1986
2114
|
}
|
|
1987
2115
|
return { input, output };
|
|
1988
2116
|
}
|
|
2117
|
+
async function listCodexRollouts(root) {
|
|
2118
|
+
return [
|
|
2119
|
+
...await collectRollouts(path7.join(root, "sessions")),
|
|
2120
|
+
...await collectRollouts(path7.join(root, "archived_sessions"))
|
|
2121
|
+
];
|
|
2122
|
+
}
|
|
1989
2123
|
async function collectRollouts(dir) {
|
|
1990
2124
|
let entries;
|
|
1991
2125
|
try {
|
|
1992
|
-
entries = await
|
|
2126
|
+
entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1993
2127
|
} catch (e) {
|
|
1994
2128
|
if (e?.code === "ENOENT") return [];
|
|
1995
2129
|
throw e;
|
|
@@ -2027,7 +2161,7 @@ var CODEX_FOLD = {
|
|
|
2027
2161
|
};
|
|
2028
2162
|
|
|
2029
2163
|
// src/tokens/gemini.ts
|
|
2030
|
-
import * as
|
|
2164
|
+
import * as fs7 from "fs/promises";
|
|
2031
2165
|
import * as path8 from "path";
|
|
2032
2166
|
function num2(v) {
|
|
2033
2167
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
@@ -2056,13 +2190,13 @@ async function sumGeminiTokens() {
|
|
|
2056
2190
|
return { input, output };
|
|
2057
2191
|
}
|
|
2058
2192
|
async function listChatFiles(root) {
|
|
2059
|
-
const entries = await
|
|
2193
|
+
const entries = await readRoot(root, () => fs7.readdir(root));
|
|
2060
2194
|
const out = [];
|
|
2061
2195
|
for (const entry of entries) {
|
|
2062
2196
|
const chatsDir = path8.join(root, entry, "chats");
|
|
2063
2197
|
let files;
|
|
2064
2198
|
try {
|
|
2065
|
-
files = await
|
|
2199
|
+
files = await fs7.readdir(chatsDir);
|
|
2066
2200
|
} catch {
|
|
2067
2201
|
continue;
|
|
2068
2202
|
}
|
|
@@ -2114,11 +2248,24 @@ async function scanWithTimeout(scan, timeoutMs, describeTimeout) {
|
|
|
2114
2248
|
const budget = new Promise((resolve) => {
|
|
2115
2249
|
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
2116
2250
|
});
|
|
2251
|
+
const startedAt2 = Date.now();
|
|
2117
2252
|
try {
|
|
2118
2253
|
const result = await Promise.race([scan(), budget]);
|
|
2119
|
-
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
|
+
}
|
|
2120
2258
|
const detail = describeTimeout ? await describeTimeout() : null;
|
|
2121
|
-
|
|
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;
|
|
2122
2269
|
} finally {
|
|
2123
2270
|
clearTimeout(timer);
|
|
2124
2271
|
}
|
|
@@ -2145,7 +2292,12 @@ async function readAllSources(race, primary, progress) {
|
|
|
2145
2292
|
const secondaryKeys = MODEL_KEYS.filter((k) => k !== primary);
|
|
2146
2293
|
const secondaryScans = secondaryKeys.map((k) => {
|
|
2147
2294
|
progress?.begin(k);
|
|
2148
|
-
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));
|
|
2149
2301
|
});
|
|
2150
2302
|
const [primaryResult, secondaryValues] = await Promise.all([
|
|
2151
2303
|
primaryScan,
|
|
@@ -2154,7 +2306,7 @@ async function readAllSources(race, primary, progress) {
|
|
|
2154
2306
|
const primaryByConv = /* @__PURE__ */ new Map();
|
|
2155
2307
|
if (primaryResult.ok) {
|
|
2156
2308
|
for (const [id, totals] of primaryResult.map) primaryByConv.set(id, scoreFor(race, totals));
|
|
2157
|
-
} else if (primaryResult.err
|
|
2309
|
+
} else if (!(primaryResult.err instanceof SourceRootMissing)) {
|
|
2158
2310
|
const err = primaryResult.err;
|
|
2159
2311
|
return { stall: `Can't read ${primary} token usage: ${err?.message ?? String(err)}` };
|
|
2160
2312
|
}
|
|
@@ -2221,6 +2373,7 @@ var RaceScoreTracker = class {
|
|
|
2221
2373
|
seq;
|
|
2222
2374
|
stalls = 0;
|
|
2223
2375
|
lastStall = null;
|
|
2376
|
+
primaryEmptyBeats = 0;
|
|
2224
2377
|
primary;
|
|
2225
2378
|
primaryTop5;
|
|
2226
2379
|
constructor(init, primary, primaryTop5) {
|
|
@@ -2253,6 +2406,7 @@ var RaceScoreTracker = class {
|
|
|
2253
2406
|
const v = reading.secondary[key];
|
|
2254
2407
|
if (v > 0) this.lastGood[key] = v;
|
|
2255
2408
|
}
|
|
2409
|
+
this.primaryEmptyBeats = reading.primaryByConv.size === 0 ? this.primaryEmptyBeats + 1 : 0;
|
|
2256
2410
|
for (const [id, v] of reading.primaryByConv) {
|
|
2257
2411
|
const prev = this.primaryConvLast[id] ?? 0;
|
|
2258
2412
|
if (v > prev) this.primaryConvLast[id] = v;
|
|
@@ -2304,6 +2458,10 @@ var RaceScoreTracker = class {
|
|
|
2304
2458
|
get stalled() {
|
|
2305
2459
|
return this.stalls >= STALL_THRESHOLD;
|
|
2306
2460
|
}
|
|
2461
|
+
/** The primary source has produced no conversations for long enough to be worth saying. */
|
|
2462
|
+
get primarySilent() {
|
|
2463
|
+
return this.primaryEmptyBeats >= PRIMARY_SILENT_THRESHOLD;
|
|
2464
|
+
}
|
|
2307
2465
|
/** Human-readable cause of the most recent stall (null once a good read recovers). */
|
|
2308
2466
|
get stallReason() {
|
|
2309
2467
|
return this.lastStall;
|
|
@@ -2332,6 +2490,75 @@ var RaceScoreTracker = class {
|
|
|
2332
2490
|
}
|
|
2333
2491
|
};
|
|
2334
2492
|
|
|
2493
|
+
// src/tokens/source-probe.ts
|
|
2494
|
+
import * as fs8 from "fs/promises";
|
|
2495
|
+
var ROOTS = {
|
|
2496
|
+
claude: claudeProjectsDir,
|
|
2497
|
+
codex: codexSessionsDir,
|
|
2498
|
+
gemini: geminiTmpDir
|
|
2499
|
+
};
|
|
2500
|
+
var LISTERS = {
|
|
2501
|
+
claude: listJsonlFiles,
|
|
2502
|
+
codex: listCodexRollouts,
|
|
2503
|
+
gemini: listChatFiles
|
|
2504
|
+
};
|
|
2505
|
+
var LABELS2 = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
|
|
2506
|
+
function sourceDir(key) {
|
|
2507
|
+
return ROOTS[key]();
|
|
2508
|
+
}
|
|
2509
|
+
async function probeSource(key) {
|
|
2510
|
+
const dir = ROOTS[key]();
|
|
2511
|
+
const exists = await fs8.stat(dir).then((st) => st.isDirectory()).catch(() => false);
|
|
2512
|
+
if (!exists) return { key, dir, exists: false, projects: 0, transcripts: 0 };
|
|
2513
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2514
|
+
const projects = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).length;
|
|
2515
|
+
const files = await LISTERS[key](dir).catch(() => []);
|
|
2516
|
+
return { key, dir, exists: true, projects, transcripts: files.length };
|
|
2517
|
+
}
|
|
2518
|
+
function overrideVar(key) {
|
|
2519
|
+
return `TOKEN_DERBY_${key.toUpperCase()}_DIR`;
|
|
2520
|
+
}
|
|
2521
|
+
async function confirmEmptySource(opts) {
|
|
2522
|
+
if (opts.probe.transcripts > 0) return true;
|
|
2523
|
+
opts.warn(describeEmptySource(opts.probe));
|
|
2524
|
+
if (!opts.interactive) return true;
|
|
2525
|
+
return opts.ask();
|
|
2526
|
+
}
|
|
2527
|
+
function describeEmptySource(probe) {
|
|
2528
|
+
const label = LABELS2[probe.key];
|
|
2529
|
+
const populated = probe.exists && probe.projects > 0;
|
|
2530
|
+
const reason = !probe.exists ? "does not exist" : populated ? `holds ${probe.projects} project ${probe.projects === 1 ? "directory" : "directories"}, none of which could be read` : "exists, but holds no transcripts";
|
|
2531
|
+
const lines = [
|
|
2532
|
+
`\u26A0 No ${label} transcripts found \u2014 your horse will not move.`,
|
|
2533
|
+
``,
|
|
2534
|
+
` Looked in: ${probe.dir}`,
|
|
2535
|
+
` (${reason})`,
|
|
2536
|
+
``
|
|
2537
|
+
];
|
|
2538
|
+
if (populated) {
|
|
2539
|
+
lines.push(
|
|
2540
|
+
` The directory is there and has history in it, so this is usually a`,
|
|
2541
|
+
` dangling symlink or a permissions problem on one of those projects.`,
|
|
2542
|
+
` To find dangling links:`,
|
|
2543
|
+
` find ${probe.dir} -type l ! -exec test -e {} \\; -print`,
|
|
2544
|
+
``
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
lines.push(
|
|
2548
|
+
` Token Derby counts ${label} usage from this machine's own filesystem.`,
|
|
2549
|
+
` If ${label} runs in a container, over SSH, or on another machine, join`,
|
|
2550
|
+
` the race from there instead.`
|
|
2551
|
+
);
|
|
2552
|
+
if (probe.key === "claude") {
|
|
2553
|
+
lines.push(
|
|
2554
|
+
` If CLAUDE_CONFIG_DIR relocated your config, Token Derby follows it \u2014`,
|
|
2555
|
+
` check it points at the config root, not the projects directory.`
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
lines.push(``, ` To read them from somewhere else: export ${overrideVar(probe.key)}=<dir>`);
|
|
2559
|
+
return lines.join("\n");
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2335
2562
|
// src/runtime/run-race.tsx
|
|
2336
2563
|
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2337
2564
|
function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
@@ -2348,6 +2575,7 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2348
2575
|
const ctrl = useRef(new AbortController());
|
|
2349
2576
|
const [stalled, setStalled] = useState5(false);
|
|
2350
2577
|
const [stallReason, setStallReason] = useState5(null);
|
|
2578
|
+
const [primarySilent, setPrimarySilent] = useState5(false);
|
|
2351
2579
|
useEffect2(() => {
|
|
2352
2580
|
const t = setInterval(() => setTickNow(/* @__PURE__ */ new Date()), 1e3);
|
|
2353
2581
|
return () => clearInterval(t);
|
|
@@ -2379,6 +2607,7 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2379
2607
|
if (pendingRef.current && !isStall(reading)) tracker.reprime();
|
|
2380
2608
|
setStalled(tracker.stalled);
|
|
2381
2609
|
setStallReason(tracker.stalled ? tracker.stallReason : null);
|
|
2610
|
+
setPrimarySilent(tracker.primarySilent);
|
|
2382
2611
|
return tracker.nextBeat();
|
|
2383
2612
|
},
|
|
2384
2613
|
sendBeat: async (snapshot) => {
|
|
@@ -2446,6 +2675,8 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2446
2675
|
lastHeartbeatOk: lastHbOk,
|
|
2447
2676
|
stalled,
|
|
2448
2677
|
stallReason,
|
|
2678
|
+
primarySilent,
|
|
2679
|
+
primarySourceDir: sourceDir(active.primary_model),
|
|
2449
2680
|
primaryModel: active.primary_model
|
|
2450
2681
|
}
|
|
2451
2682
|
),
|
|
@@ -2513,6 +2744,28 @@ async function buildInitialState(args) {
|
|
|
2513
2744
|
};
|
|
2514
2745
|
}
|
|
2515
2746
|
|
|
2747
|
+
// src/ui/prompt.ts
|
|
2748
|
+
async function promptYesNo(question, opts = {}) {
|
|
2749
|
+
const input = opts.input ?? process.stdin;
|
|
2750
|
+
const output = opts.output ?? process.stdout;
|
|
2751
|
+
if (input === process.stdin) resetStdinAfterInk();
|
|
2752
|
+
const readline6 = await import("readline/promises");
|
|
2753
|
+
const rl = readline6.createInterface({ input, output });
|
|
2754
|
+
const a = (await rl.question(question)).trim().toLowerCase();
|
|
2755
|
+
rl.close();
|
|
2756
|
+
if (a === "") return opts.defaultYes !== false;
|
|
2757
|
+
return a === "y" || a === "yes";
|
|
2758
|
+
}
|
|
2759
|
+
function resetStdinAfterInk() {
|
|
2760
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
2761
|
+
process.stdin.setRawMode(false);
|
|
2762
|
+
}
|
|
2763
|
+
while (process.stdin.read() !== null) {
|
|
2764
|
+
}
|
|
2765
|
+
process.stdin.pause();
|
|
2766
|
+
process.stdin.ref();
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2516
2769
|
// src/commands/join.ts
|
|
2517
2770
|
function parsePrimaryFlag(argv) {
|
|
2518
2771
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -2607,6 +2860,19 @@ async function joinCommand(joinCode, argv = []) {
|
|
|
2607
2860
|
if (primaryFlag) chosenPrimary = primaryFlag;
|
|
2608
2861
|
else if (process.stdout.isTTY) chosenPrimary = await pickPrimary();
|
|
2609
2862
|
}
|
|
2863
|
+
const effectivePrimary = ownHorse?.primary_model ?? chosenPrimary;
|
|
2864
|
+
const proceed = await confirmEmptySource({
|
|
2865
|
+
probe: await probeSource(effectivePrimary),
|
|
2866
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
2867
|
+
warn: (text) => console.error(`
|
|
2868
|
+
${text}
|
|
2869
|
+
`),
|
|
2870
|
+
ask: () => promptYesNo("Join anyway? [y/N] ", { defaultYes: false })
|
|
2871
|
+
});
|
|
2872
|
+
if (!proceed) {
|
|
2873
|
+
console.log("Cancelled.");
|
|
2874
|
+
return 1;
|
|
2875
|
+
}
|
|
2610
2876
|
let joinResp;
|
|
2611
2877
|
try {
|
|
2612
2878
|
joinResp = await joinRace(code, { stable_horse_id: chosenStableHorseId, primary_model: chosenPrimary });
|
|
@@ -2907,25 +3173,6 @@ async function webCommand(deps = {}) {
|
|
|
2907
3173
|
return 0;
|
|
2908
3174
|
}
|
|
2909
3175
|
|
|
2910
|
-
// src/ui/prompt.ts
|
|
2911
|
-
async function promptYesNo(question) {
|
|
2912
|
-
resetStdinAfterInk();
|
|
2913
|
-
const readline6 = await import("readline/promises");
|
|
2914
|
-
const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
|
|
2915
|
-
const a = (await rl.question(question)).trim().toLowerCase();
|
|
2916
|
-
rl.close();
|
|
2917
|
-
return a === "" || a === "y" || a === "yes";
|
|
2918
|
-
}
|
|
2919
|
-
function resetStdinAfterInk() {
|
|
2920
|
-
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
2921
|
-
process.stdin.setRawMode(false);
|
|
2922
|
-
}
|
|
2923
|
-
while (process.stdin.read() !== null) {
|
|
2924
|
-
}
|
|
2925
|
-
process.stdin.pause();
|
|
2926
|
-
process.stdin.ref();
|
|
2927
|
-
}
|
|
2928
|
-
|
|
2929
3176
|
// src/commands/login.ts
|
|
2930
3177
|
function parseDeviceNameFlag(argv) {
|
|
2931
3178
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -3943,6 +4190,31 @@ function envCommand(arg) {
|
|
|
3943
4190
|
return 0;
|
|
3944
4191
|
}
|
|
3945
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
|
+
|
|
3946
4218
|
// src/bin.ts
|
|
3947
4219
|
var HELP = `token-derby v${CLI_VERSION}
|
|
3948
4220
|
|
|
@@ -3971,6 +4243,8 @@ Identity:
|
|
|
3971
4243
|
|
|
3972
4244
|
Maintenance:
|
|
3973
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)
|
|
3974
4248
|
|
|
3975
4249
|
Stable management:
|
|
3976
4250
|
token-derby stable create Make a new horse (interactive)
|
|
@@ -4007,9 +4281,25 @@ Environment:
|
|
|
4007
4281
|
TOKEN_DERBY_API_BASE Hard-override API base URL (wins over env)
|
|
4008
4282
|
TOKEN_DERBY_HOME Hard-override identity/stable directory
|
|
4009
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
|
+
}
|
|
4010
4293
|
async function main() {
|
|
4011
4294
|
const argv = process.argv.slice(2);
|
|
4012
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
|
+
});
|
|
4013
4303
|
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
4014
4304
|
console.log(HELP);
|
|
4015
4305
|
return 0;
|
|
@@ -4025,6 +4315,7 @@ async function main() {
|
|
|
4025
4315
|
if (cmd === "login") return loginCommand(argv.slice(1));
|
|
4026
4316
|
if (cmd === "update") return updateCommand();
|
|
4027
4317
|
if (cmd === "env") return envCommand(argv[1]);
|
|
4318
|
+
if (cmd === "logs") return logsCommand(argv.slice(1));
|
|
4028
4319
|
const identity = await loadIdentity();
|
|
4029
4320
|
if (!identity) {
|
|
4030
4321
|
console.error("Run `token-derby login` to set up your identity before using any other command.");
|
|
@@ -4071,9 +4362,33 @@ function parseFlag(args, flag) {
|
|
|
4071
4362
|
}
|
|
4072
4363
|
return void 0;
|
|
4073
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();
|
|
4074
4381
|
main().then(
|
|
4075
|
-
(code) =>
|
|
4382
|
+
(code) => {
|
|
4383
|
+
logInfo("cmd.exit", { code, ms: Date.now() - startedAt });
|
|
4384
|
+
process.exit(code);
|
|
4385
|
+
},
|
|
4076
4386
|
(err) => {
|
|
4387
|
+
logError("cmd.crash", {
|
|
4388
|
+
message: err?.message ?? String(err),
|
|
4389
|
+
stack: err?.stack,
|
|
4390
|
+
ms: Date.now() - startedAt
|
|
4391
|
+
});
|
|
4077
4392
|
console.error(err?.stack ?? err);
|
|
4078
4393
|
process.exit(1);
|
|
4079
4394
|
}
|