tokensniff 0.1.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/LICENSE +21 -0
- package/README.md +809 -0
- package/bin/tokensniff-status.js +8 -0
- package/bin/tokensniff.js +8 -0
- package/dist/cli/run.d.ts +64 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli.js +2206 -0
- package/dist/cli.js.map +1 -0
- package/dist/collector/config.d.ts +43 -0
- package/dist/collector/config.d.ts.map +1 -0
- package/dist/collector/index.d.ts +14 -0
- package/dist/collector/index.d.ts.map +1 -0
- package/dist/collector/parse.d.ts +42 -0
- package/dist/collector/parse.d.ts.map +1 -0
- package/dist/collector/store.d.ts +76 -0
- package/dist/collector/store.d.ts.map +1 -0
- package/dist/dashboard/heatmap.d.ts +34 -0
- package/dist/dashboard/heatmap.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2474 -0
- package/dist/index.js.map +1 -0
- package/dist/pricing.js +145 -0
- package/dist/pricing.js.map +1 -0
- package/dist/proxy.js +1744 -0
- package/dist/proxy.js.map +1 -0
- package/dist/renderer/format.d.ts +33 -0
- package/dist/renderer/format.d.ts.map +1 -0
- package/dist/renderer/index.d.ts +42 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/schema.js +90 -0
- package/dist/schema.js.map +1 -0
- package/dist/shared/pricing.d.ts +60 -0
- package/dist/shared/pricing.d.ts.map +1 -0
- package/dist/shared/schema.d.ts +122 -0
- package/dist/shared/schema.d.ts.map +1 -0
- package/dist/status.js +521 -0
- package/dist/status.js.map +1 -0
- package/package.json +105 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2206 @@
|
|
|
1
|
+
import { execSync, spawn } from 'child_process';
|
|
2
|
+
import fs2 from 'fs';
|
|
3
|
+
import net from 'net';
|
|
4
|
+
import path2 from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import http from 'http';
|
|
7
|
+
|
|
8
|
+
// src/cli/run.ts
|
|
9
|
+
function toValidPort(value, fallback) {
|
|
10
|
+
const parsed = Number(value);
|
|
11
|
+
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535 ? parsed : fallback;
|
|
12
|
+
}
|
|
13
|
+
function toBoundedInt(value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
16
|
+
const intVal = Math.floor(parsed);
|
|
17
|
+
return Math.max(min, Math.min(max, intVal));
|
|
18
|
+
}
|
|
19
|
+
function toNonEmptyString(value, fallback) {
|
|
20
|
+
if (typeof value !== "string") return fallback;
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
return trimmed.length > 0 ? trimmed : fallback;
|
|
23
|
+
}
|
|
24
|
+
function toSafeBoolean(value, fallback) {
|
|
25
|
+
if (typeof value === "boolean") return value;
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
const lower = value.trim().toLowerCase();
|
|
28
|
+
if (lower === "true" || lower === "1") return true;
|
|
29
|
+
if (lower === "false" || lower === "0") return false;
|
|
30
|
+
}
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
function normalizeDirectory(targetPath) {
|
|
34
|
+
const resolved = path2.resolve(targetPath.trim());
|
|
35
|
+
return path2.normalize(resolved);
|
|
36
|
+
}
|
|
37
|
+
function getGlobalDir() {
|
|
38
|
+
const home = process.env.USERPROFILE || os.homedir();
|
|
39
|
+
return path2.join(home, ".tokensniff");
|
|
40
|
+
}
|
|
41
|
+
function getGlobalConfigPath() {
|
|
42
|
+
return path2.join(getGlobalDir(), "config.json");
|
|
43
|
+
}
|
|
44
|
+
function loadConfig(env = process.env) {
|
|
45
|
+
const targetConfigFile = env.TOKENSNIFF_CONFIG || getGlobalConfigPath();
|
|
46
|
+
let fileConfig = {};
|
|
47
|
+
if (fs2.existsSync(targetConfigFile)) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = fs2.readFileSync(targetConfigFile, "utf8");
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
52
|
+
fileConfig = parsed;
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
56
|
+
console.error(`[tokensniff] ignoring corrupt config at ${targetConfigFile}: ${msg}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const resolve = (envKey, configKey, defaultValue) => {
|
|
60
|
+
return env[envKey] ?? fileConfig[configKey] ?? defaultValue;
|
|
61
|
+
};
|
|
62
|
+
const rootDir = getGlobalDir();
|
|
63
|
+
const defaultStatusDir = path2.join(rootDir, "status");
|
|
64
|
+
const rawStatusDirs = resolve("TOKENSNIFF_STATUS_DIRS", "statusDirs", [defaultStatusDir]);
|
|
65
|
+
const parsedDirs = Array.isArray(rawStatusDirs) ? rawStatusDirs.map(String) : String(rawStatusDirs).split(",");
|
|
66
|
+
const normalizedStatusDirs = parsedDirs.map((d) => d.trim()).filter((d) => d.length > 0).map(normalizeDirectory);
|
|
67
|
+
const statusDirs = normalizedStatusDirs.length > 0 ? normalizedStatusDirs : [normalizeDirectory(defaultStatusDir)];
|
|
68
|
+
const isNoColorSet = typeof env.NO_COLOR === "string" && env.NO_COLOR.length > 0;
|
|
69
|
+
const colorDefault = !isNoColorSet;
|
|
70
|
+
return {
|
|
71
|
+
listenPort: toValidPort(resolve("TOKENSNIFF_PORT", "listenPort", 4e3), 4e3),
|
|
72
|
+
listenHost: toNonEmptyString(
|
|
73
|
+
resolve("TOKENSNIFF_HOST", "listenHost", "127.0.0.1"),
|
|
74
|
+
"127.0.0.1"
|
|
75
|
+
),
|
|
76
|
+
upstreamHost: toNonEmptyString(
|
|
77
|
+
resolve("TOKENSNIFF_UPSTREAM_HOST", "upstreamHost", "localhost"),
|
|
78
|
+
"localhost"
|
|
79
|
+
),
|
|
80
|
+
upstreamPort: toValidPort(resolve("TOKENSNIFF_UPSTREAM_PORT", "upstreamPort", 8085), 8085),
|
|
81
|
+
upstreamTimeoutMs: toBoundedInt(
|
|
82
|
+
resolve("TOKENSNIFF_UPSTREAM_TIMEOUT_MS", "upstreamTimeoutMs", 3e5),
|
|
83
|
+
3e5,
|
|
84
|
+
1e3,
|
|
85
|
+
36e5
|
|
86
|
+
),
|
|
87
|
+
maxBodyBytes: toBoundedInt(
|
|
88
|
+
resolve("TOKENSNIFF_MAX_BODY_BYTES", "maxBodyBytes", 10 * 1024 * 1024),
|
|
89
|
+
10485760,
|
|
90
|
+
1024,
|
|
91
|
+
100 * 1024 * 1024
|
|
92
|
+
),
|
|
93
|
+
statusDirs,
|
|
94
|
+
maxSessions: toBoundedInt(resolve("TOKENSNIFF_MAX_SESSIONS", "maxSessions", 50), 50, 1, 1e3),
|
|
95
|
+
deadSessionMs: toBoundedInt(
|
|
96
|
+
resolve("TOKENSNIFF_DEAD_SESSION_MS", "deadSessionMs", 72 * 3600 * 1e3),
|
|
97
|
+
2592e5,
|
|
98
|
+
3600 * 1e3
|
|
99
|
+
),
|
|
100
|
+
staleAfterS: toBoundedInt(
|
|
101
|
+
resolve("TOKENSNIFF_STALE_AFTER_S", "staleAfterS", 120),
|
|
102
|
+
120,
|
|
103
|
+
5,
|
|
104
|
+
86400
|
|
105
|
+
),
|
|
106
|
+
labelMaxIn: toBoundedInt(resolve("TOKENSNIFF_LABEL_MAX_IN", "labelMaxIn", 2e3), 2e3, 100),
|
|
107
|
+
labelMaxOut: toBoundedInt(resolve("TOKENSNIFF_LABEL_MAX_OUT", "labelMaxOut", 60), 60, 1),
|
|
108
|
+
warnPct: toBoundedInt(resolve("TOKENSNIFF_WARN_PCT", "warnPct", 60), 60, 1, 99),
|
|
109
|
+
critPct: toBoundedInt(resolve("TOKENSNIFF_CRIT_PCT", "critPct", 80), 80, 2, 100),
|
|
110
|
+
color: colorDefault && toSafeBoolean(resolve("TOKENSNIFF_COLOR", "color", true), true),
|
|
111
|
+
upstreamCommand: toNonEmptyString(
|
|
112
|
+
resolve(
|
|
113
|
+
"TOKENSNIFF_UPSTREAM_CMD",
|
|
114
|
+
"upstreamCommand",
|
|
115
|
+
"npx antigravity-claude-proxy@latest start"
|
|
116
|
+
),
|
|
117
|
+
"npx antigravity-claude-proxy@latest start"
|
|
118
|
+
),
|
|
119
|
+
upstreamStopCommand: String(
|
|
120
|
+
resolve("TOKENSNIFF_UPSTREAM_STOP_CMD", "upstreamStopCommand", "")
|
|
121
|
+
).trim(),
|
|
122
|
+
harnessCommand: toNonEmptyString(
|
|
123
|
+
resolve("TOKENSNIFF_HARNESS_CMD", "harnessCommand", "claude"),
|
|
124
|
+
"claude"
|
|
125
|
+
)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function sanitizeSessionId(id) {
|
|
129
|
+
const normalized = String(id || "default").trim().slice(0, 64);
|
|
130
|
+
return /^[A-Za-z0-9_-]+$/.test(normalized) ? normalized : "default";
|
|
131
|
+
}
|
|
132
|
+
function getLocalDateString(d = /* @__PURE__ */ new Date()) {
|
|
133
|
+
const year = d.getFullYear();
|
|
134
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
135
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
136
|
+
return `${year}-${month}-${day}`;
|
|
137
|
+
}
|
|
138
|
+
function syncSleep(ms) {
|
|
139
|
+
try {
|
|
140
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function ensureDir(dir) {
|
|
145
|
+
try {
|
|
146
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error.code !== "EEXIST") {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function atomicWrite(filePath, data, maxRetries = 5) {
|
|
154
|
+
const dir = path2.dirname(filePath);
|
|
155
|
+
ensureDir(dir);
|
|
156
|
+
const tmpPath = path2.join(
|
|
157
|
+
dir,
|
|
158
|
+
`.${path2.basename(filePath)}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`
|
|
159
|
+
);
|
|
160
|
+
fs2.writeFileSync(tmpPath, data, "utf8");
|
|
161
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
162
|
+
try {
|
|
163
|
+
fs2.renameSync(tmpPath, filePath);
|
|
164
|
+
return;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const err = error;
|
|
167
|
+
const isLockError = process.platform === "win32" && (err.code === "EPERM" || err.code === "EBUSY" || err.code === "EACCES");
|
|
168
|
+
if (isLockError && attempt < maxRetries) {
|
|
169
|
+
syncSleep((attempt + 1) * 10);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
fs2.unlinkSync(tmpPath);
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function atomicAppend(filePath, line, maxRetries = 5) {
|
|
181
|
+
const dir = path2.dirname(filePath);
|
|
182
|
+
ensureDir(dir);
|
|
183
|
+
const payload = line.endsWith("\n") ? line : `${line}
|
|
184
|
+
`;
|
|
185
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
186
|
+
try {
|
|
187
|
+
fs2.appendFileSync(filePath, payload, { encoding: "utf8", flag: "a" });
|
|
188
|
+
return;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const err = error;
|
|
191
|
+
const isLockError = process.platform === "win32" && (err.code === "EPERM" || err.code === "EBUSY" || err.code === "EACCES");
|
|
192
|
+
if (isLockError && attempt < maxRetries) {
|
|
193
|
+
syncSleep((attempt + 1) * 10);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function saveLatest(statusDirs, sessionId, latestPayload) {
|
|
201
|
+
const safeId = sanitizeSessionId(sessionId);
|
|
202
|
+
const serialized = JSON.stringify(latestPayload);
|
|
203
|
+
for (const dir of statusDirs) {
|
|
204
|
+
try {
|
|
205
|
+
ensureDir(dir);
|
|
206
|
+
atomicWrite(path2.join(dir, `latest-${safeId}.json`), serialized);
|
|
207
|
+
atomicWrite(path2.join(dir, "latest.json"), serialized);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
210
|
+
console.error(`[tokensniff] failed saving status in ${dir}: ${msg}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function saveTotals(statusDirs, totals, maxSessions) {
|
|
215
|
+
const sessionKeys = Object.keys(totals);
|
|
216
|
+
if (sessionKeys.length > maxSessions) {
|
|
217
|
+
const expiredKeys = sessionKeys.slice(0, sessionKeys.length - maxSessions);
|
|
218
|
+
for (const key of expiredKeys) {
|
|
219
|
+
delete totals[key];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const serialized = JSON.stringify(totals);
|
|
223
|
+
for (const dir of statusDirs) {
|
|
224
|
+
try {
|
|
225
|
+
ensureDir(dir);
|
|
226
|
+
atomicWrite(path2.join(dir, "totals.json"), serialized);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
229
|
+
console.error(`[tokensniff] failed saving totals in ${dir}: ${msg}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return totals;
|
|
233
|
+
}
|
|
234
|
+
function loadTotals(statusDirs) {
|
|
235
|
+
for (const dir of statusDirs) {
|
|
236
|
+
const targetFile = path2.join(dir, "totals.json");
|
|
237
|
+
try {
|
|
238
|
+
if (fs2.existsSync(targetFile)) {
|
|
239
|
+
const parsed = JSON.parse(fs2.readFileSync(targetFile, "utf8"));
|
|
240
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
241
|
+
return parsed;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
function appendHistory(statusDirs, entry) {
|
|
250
|
+
const line = JSON.stringify({
|
|
251
|
+
d: entry.date || getLocalDateString(),
|
|
252
|
+
ts: Date.now(),
|
|
253
|
+
s: sanitizeSessionId(entry.sessionId),
|
|
254
|
+
t: entry.turn || 1,
|
|
255
|
+
m: entry.model || "unknown",
|
|
256
|
+
tk: entry.tokens || 0,
|
|
257
|
+
c: Number(Number(entry.cost || 0).toFixed(6)),
|
|
258
|
+
cr: entry.cacheRead || 0,
|
|
259
|
+
th: entry.thinkingTokens || 0
|
|
260
|
+
});
|
|
261
|
+
for (const dir of statusDirs) {
|
|
262
|
+
try {
|
|
263
|
+
const historyPath = path2.join(dir, "history.ndjson");
|
|
264
|
+
atomicAppend(historyPath, line);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
267
|
+
console.error(`[tokensniff] failed appending history in ${dir}: ${msg}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function loadTodaySpend(statusDirs, targetDate = getLocalDateString()) {
|
|
272
|
+
for (const dir of statusDirs) {
|
|
273
|
+
const historyPath = path2.join(dir, "history.ndjson");
|
|
274
|
+
if (!fs2.existsSync(historyPath)) continue;
|
|
275
|
+
try {
|
|
276
|
+
const content = fs2.readFileSync(historyPath, "utf8");
|
|
277
|
+
let totalCost = 0;
|
|
278
|
+
const lines = content.split("\n");
|
|
279
|
+
for (const line of lines) {
|
|
280
|
+
if (!line.trim()) continue;
|
|
281
|
+
try {
|
|
282
|
+
const row = JSON.parse(line);
|
|
283
|
+
if (typeof row === "object" && row !== null && row.d === targetDate && typeof row.c === "number") {
|
|
284
|
+
totalCost += row.c;
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return totalCost;
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
function pruneDeadSessions(statusDirs, ttlMs) {
|
|
296
|
+
const cutoffTime = Date.now() - ttlMs;
|
|
297
|
+
for (const dir of statusDirs) {
|
|
298
|
+
if (!fs2.existsSync(dir)) continue;
|
|
299
|
+
try {
|
|
300
|
+
const files = fs2.readdirSync(dir);
|
|
301
|
+
for (const file of files) {
|
|
302
|
+
if (!file.startsWith("latest-") || !file.endsWith(".json")) continue;
|
|
303
|
+
const filePath = path2.join(dir, file);
|
|
304
|
+
try {
|
|
305
|
+
const stats = fs2.statSync(filePath);
|
|
306
|
+
if (stats.mtimeMs < cutoffTime) {
|
|
307
|
+
fs2.unlinkSync(filePath);
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/shared/pricing.ts
|
|
318
|
+
var rateCache = /* @__PURE__ */ new Map();
|
|
319
|
+
var catalogPromise = null;
|
|
320
|
+
function normalizeModelName(raw) {
|
|
321
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
322
|
+
return "unknown";
|
|
323
|
+
}
|
|
324
|
+
const trimmed = raw.trim().toLowerCase();
|
|
325
|
+
const withoutNamespace = trimmed.replace(/^~?[a-zA-Z0-9_-]+\//, "");
|
|
326
|
+
return withoutNamespace.replace(/:[a-zA-Z0-9_-]+$/, "").replace(/-(?:tiered|thinking|preview|low|high|latest)$/, "").replace(/-(?:\d{4}-\d{2}-\d{2}|\d{8}|\d{4})$/, "").replace(/\b(\d+)-(\d+)\b/g, "$1.$2");
|
|
327
|
+
}
|
|
328
|
+
function toSafeRate(value, fallback = 0) {
|
|
329
|
+
if (!value) return fallback;
|
|
330
|
+
const parsed = Number.parseFloat(value);
|
|
331
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
332
|
+
}
|
|
333
|
+
async function syncOpenRouterCatalog() {
|
|
334
|
+
if (catalogPromise) {
|
|
335
|
+
return catalogPromise;
|
|
336
|
+
}
|
|
337
|
+
catalogPromise = (async () => {
|
|
338
|
+
try {
|
|
339
|
+
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
|
340
|
+
signal: AbortSignal.timeout(5e3),
|
|
341
|
+
headers: { Accept: "application/json" }
|
|
342
|
+
});
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
const json = await response.json();
|
|
347
|
+
if (!Array.isArray(json?.data)) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
for (const item of json.data) {
|
|
351
|
+
if (!item?.id || !item.pricing) continue;
|
|
352
|
+
const promptRate = toSafeRate(item.pricing.prompt);
|
|
353
|
+
const completionRate = toSafeRate(item.pricing.completion);
|
|
354
|
+
const cacheReadRate = toSafeRate(
|
|
355
|
+
item.pricing.input_cache_read,
|
|
356
|
+
promptRate > 0 ? promptRate * 0.1 : 0
|
|
357
|
+
);
|
|
358
|
+
const cacheWriteRate = toSafeRate(item.pricing.input_cache_write, promptRate);
|
|
359
|
+
const rawWindow = item.context_length;
|
|
360
|
+
const contextWindow = typeof rawWindow === "number" && Number.isFinite(rawWindow) && rawWindow > 0 ? rawWindow : 128e3;
|
|
361
|
+
const resolvedRates = {
|
|
362
|
+
in: promptRate,
|
|
363
|
+
out: completionRate,
|
|
364
|
+
cached: cacheReadRate,
|
|
365
|
+
cacheWrite: cacheWriteRate,
|
|
366
|
+
window: contextWindow
|
|
367
|
+
};
|
|
368
|
+
const fullId = item.id.toLowerCase();
|
|
369
|
+
const bareSlug = item.id.includes("/") ? item.id.split("/")[1]?.toLowerCase() ?? fullId : fullId;
|
|
370
|
+
const cleanSlug = normalizeModelName(item.id);
|
|
371
|
+
rateCache.set(fullId, resolvedRates);
|
|
372
|
+
rateCache.set(bareSlug, resolvedRates);
|
|
373
|
+
rateCache.set(cleanSlug, resolvedRates);
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
} finally {
|
|
379
|
+
catalogPromise = null;
|
|
380
|
+
}
|
|
381
|
+
})();
|
|
382
|
+
return catalogPromise;
|
|
383
|
+
}
|
|
384
|
+
async function resolveModelRates(model, overrides = {}) {
|
|
385
|
+
const clean = normalizeModelName(model);
|
|
386
|
+
const rawKey = model.trim().toLowerCase();
|
|
387
|
+
const matchedOverride = overrides[rawKey] ?? overrides[clean];
|
|
388
|
+
if (matchedOverride) {
|
|
389
|
+
const existing = rateCache.get(clean) ?? rateCache.get(rawKey);
|
|
390
|
+
const resolvedOverride = {
|
|
391
|
+
in: matchedOverride.in ?? existing?.in ?? 0,
|
|
392
|
+
out: matchedOverride.out ?? existing?.out ?? 0,
|
|
393
|
+
cached: matchedOverride.cached ?? existing?.cached ?? 0,
|
|
394
|
+
cacheWrite: matchedOverride.in ?? existing?.cacheWrite ?? 0,
|
|
395
|
+
window: matchedOverride.window ?? existing?.window ?? 128e3
|
|
396
|
+
};
|
|
397
|
+
rateCache.set(clean, resolvedOverride);
|
|
398
|
+
rateCache.set(rawKey, resolvedOverride);
|
|
399
|
+
return resolvedOverride;
|
|
400
|
+
}
|
|
401
|
+
const cached = rateCache.get(clean) ?? rateCache.get(rawKey);
|
|
402
|
+
if (cached) {
|
|
403
|
+
return cached;
|
|
404
|
+
}
|
|
405
|
+
await syncOpenRouterCatalog();
|
|
406
|
+
const postSync = rateCache.get(clean) ?? rateCache.get(rawKey);
|
|
407
|
+
if (postSync) {
|
|
408
|
+
return postSync;
|
|
409
|
+
}
|
|
410
|
+
const candidateKeys = Array.from(rateCache.keys()).sort((a, b) => b.length - a.length);
|
|
411
|
+
for (const key of candidateKeys) {
|
|
412
|
+
if (key.length > 3 && (clean.includes(key) || key.includes(clean))) {
|
|
413
|
+
const rates = rateCache.get(key);
|
|
414
|
+
if (rates) {
|
|
415
|
+
rateCache.set(clean, rates);
|
|
416
|
+
return rates;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const defaultBoundary = {
|
|
421
|
+
in: 0,
|
|
422
|
+
out: 0,
|
|
423
|
+
cached: 0,
|
|
424
|
+
cacheWrite: 0,
|
|
425
|
+
window: 128e3
|
|
426
|
+
};
|
|
427
|
+
rateCache.set(clean, defaultBoundary);
|
|
428
|
+
return defaultBoundary;
|
|
429
|
+
}
|
|
430
|
+
function ratesFor(model) {
|
|
431
|
+
const clean = normalizeModelName(model);
|
|
432
|
+
const rawKey = model.trim().toLowerCase();
|
|
433
|
+
const hit = rateCache.get(clean) ?? rateCache.get(rawKey);
|
|
434
|
+
if (hit) {
|
|
435
|
+
return hit;
|
|
436
|
+
}
|
|
437
|
+
void syncOpenRouterCatalog();
|
|
438
|
+
return {
|
|
439
|
+
in: 0,
|
|
440
|
+
out: 0,
|
|
441
|
+
cached: 0,
|
|
442
|
+
cacheWrite: 0,
|
|
443
|
+
window: 128e3
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function costFor(rates, usage) {
|
|
447
|
+
const inputTokens = Math.max(0, usage.input ?? 0);
|
|
448
|
+
const outputTokens = Math.max(0, usage.output ?? 0);
|
|
449
|
+
const cacheReadTokens = Math.max(0, usage.cacheRead ?? 0);
|
|
450
|
+
const cacheCreateTokens = Math.max(0, usage.cacheCreate ?? 0);
|
|
451
|
+
const cacheWriteRate = rates.cacheWrite > 0 ? rates.cacheWrite : rates.in;
|
|
452
|
+
const total = inputTokens * rates.in + outputTokens * rates.out + cacheReadTokens * rates.cached + cacheCreateTokens * cacheWriteRate;
|
|
453
|
+
if (!Number.isFinite(total) || total < 0) {
|
|
454
|
+
return 0;
|
|
455
|
+
}
|
|
456
|
+
return Math.round(total * 1e8) / 1e8;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/dashboard/heatmap.ts
|
|
460
|
+
function loadDailyRollup(statusDirs) {
|
|
461
|
+
const dailyMap = {};
|
|
462
|
+
for (const dir of statusDirs) {
|
|
463
|
+
const historyPath = path2.join(dir, "history.ndjson");
|
|
464
|
+
if (!fs2.existsSync(historyPath)) continue;
|
|
465
|
+
try {
|
|
466
|
+
const raw = fs2.readFileSync(historyPath, "utf8");
|
|
467
|
+
const lines = raw.split("\n");
|
|
468
|
+
for (const line of lines) {
|
|
469
|
+
if (!line.trim()) continue;
|
|
470
|
+
try {
|
|
471
|
+
const row = JSON.parse(line);
|
|
472
|
+
if (typeof row !== "object" || row === null) continue;
|
|
473
|
+
const record = row;
|
|
474
|
+
const dateStr = record.d;
|
|
475
|
+
if (typeof dateStr !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (!dailyMap[dateStr]) {
|
|
479
|
+
dailyMap[dateStr] = {
|
|
480
|
+
date: dateStr,
|
|
481
|
+
tokens: 0,
|
|
482
|
+
cost: 0,
|
|
483
|
+
cacheRead: 0,
|
|
484
|
+
thinkingTokens: 0,
|
|
485
|
+
turns: 0,
|
|
486
|
+
models: {},
|
|
487
|
+
hourlyTurns: new Array(24).fill(0)
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const bucket = dailyMap[dateStr];
|
|
491
|
+
if (!bucket) continue;
|
|
492
|
+
bucket.tokens += typeof record.tk === "number" ? record.tk : 0;
|
|
493
|
+
bucket.cost += typeof record.c === "number" ? record.c : 0;
|
|
494
|
+
bucket.cacheRead += typeof record.cr === "number" ? record.cr : 0;
|
|
495
|
+
bucket.thinkingTokens += typeof record.th === "number" ? record.th : 0;
|
|
496
|
+
bucket.turns += 1;
|
|
497
|
+
const model = typeof record.m === "string" && record.m ? record.m : "unknown";
|
|
498
|
+
bucket.models[model] = (bucket.models[model] ?? 0) + 1;
|
|
499
|
+
if (typeof record.ts === "number") {
|
|
500
|
+
const hour = new Date(record.ts).getHours();
|
|
501
|
+
if (hour >= 0 && hour < 24 && bucket.hourlyTurns[hour] !== void 0) {
|
|
502
|
+
bucket.hourlyTurns[hour] += 1;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
510
|
+
console.error(`[tokensniff] failed reading history in ${dir}: ${msg}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return dailyMap;
|
|
514
|
+
}
|
|
515
|
+
function importCapturesDirectory(sourceDir, targetStatusDirs) {
|
|
516
|
+
if (!fs2.existsSync(sourceDir)) {
|
|
517
|
+
return { imported: 0, skipped: 0 };
|
|
518
|
+
}
|
|
519
|
+
const files = fs2.readdirSync(sourceDir);
|
|
520
|
+
let imported = 0;
|
|
521
|
+
let skipped = 0;
|
|
522
|
+
for (const file of files) {
|
|
523
|
+
if (!file.endsWith(".json") || file.startsWith("session_")) continue;
|
|
524
|
+
const fullPath = path2.join(sourceDir, file);
|
|
525
|
+
try {
|
|
526
|
+
const parsedData = JSON.parse(fs2.readFileSync(fullPath, "utf8"));
|
|
527
|
+
if (typeof parsedData !== "object" || parsedData === null) {
|
|
528
|
+
skipped++;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const data = parsedData;
|
|
532
|
+
const timestamp = data.timestamp || data.reqLog?.timestamp;
|
|
533
|
+
if (!timestamp) {
|
|
534
|
+
skipped++;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const dateStr = getLocalDateString(new Date(timestamp));
|
|
538
|
+
const res = data.response || data.reqLog?.responseHeaders;
|
|
539
|
+
if (!res) {
|
|
540
|
+
skipped++;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
let rawBody = "";
|
|
544
|
+
if (typeof data.response?.rawBodyString === "string") {
|
|
545
|
+
rawBody = data.response.rawBodyString;
|
|
546
|
+
} else if (typeof data.reqLog?.rawResponseBody === "string") {
|
|
547
|
+
rawBody = data.reqLog.rawResponseBody;
|
|
548
|
+
}
|
|
549
|
+
if (!rawBody) {
|
|
550
|
+
skipped++;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const parsedBody = JSON.parse(rawBody);
|
|
554
|
+
if (typeof parsedBody !== "object" || parsedBody === null) {
|
|
555
|
+
skipped++;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const bodyObj = parsedBody;
|
|
559
|
+
const usage = bodyObj.usage ?? {};
|
|
560
|
+
const model = typeof bodyObj.model === "string" ? bodyObj.model : "gemini-3.7-flash-tiered";
|
|
561
|
+
const rates = ratesFor(model);
|
|
562
|
+
const inTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
|
|
563
|
+
const outTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
|
|
564
|
+
const cacheRead = typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : 0;
|
|
565
|
+
const cacheCreate = typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : 0;
|
|
566
|
+
if (inTokens === 0 && outTokens === 0) {
|
|
567
|
+
skipped++;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const turnCost = costFor(rates, {
|
|
571
|
+
input: inTokens,
|
|
572
|
+
output: outTokens,
|
|
573
|
+
cacheRead,
|
|
574
|
+
cacheCreate
|
|
575
|
+
});
|
|
576
|
+
appendHistory(targetStatusDirs, {
|
|
577
|
+
date: dateStr,
|
|
578
|
+
sessionId: "imported",
|
|
579
|
+
turn: 1,
|
|
580
|
+
model,
|
|
581
|
+
tokens: inTokens + outTokens + cacheRead,
|
|
582
|
+
cost: turnCost,
|
|
583
|
+
cacheRead,
|
|
584
|
+
thinkingTokens: 0
|
|
585
|
+
});
|
|
586
|
+
imported++;
|
|
587
|
+
} catch {
|
|
588
|
+
skipped++;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { imported, skipped };
|
|
592
|
+
}
|
|
593
|
+
function renderHeatmapHtml(dailyMap, targetYear = (/* @__PURE__ */ new Date()).getFullYear()) {
|
|
594
|
+
let totalTokens = 0;
|
|
595
|
+
let totalCost = 0;
|
|
596
|
+
let totalTurns = 0;
|
|
597
|
+
let totalCacheRead = 0;
|
|
598
|
+
for (const bucket of Object.values(dailyMap)) {
|
|
599
|
+
totalTokens += bucket.tokens;
|
|
600
|
+
totalCost += bucket.cost;
|
|
601
|
+
totalTurns += bucket.turns;
|
|
602
|
+
totalCacheRead += bucket.cacheRead;
|
|
603
|
+
}
|
|
604
|
+
const cacheHitPct = totalTokens > 0 ? (totalCacheRead / totalTokens * 100).toFixed(1) : "0.0";
|
|
605
|
+
const formattedTotalTokens = totalTokens >= 1e9 ? `${(totalTokens / 1e9).toFixed(2)}B` : `${(totalTokens / 1e6).toFixed(2)}M`;
|
|
606
|
+
return `<!DOCTYPE html>
|
|
607
|
+
<html lang="en">
|
|
608
|
+
<head>
|
|
609
|
+
<meta charset="utf-8">
|
|
610
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
611
|
+
<title>tokensniff \u2022 ${targetYear} Token Spend Heatmap</title>
|
|
612
|
+
<style>
|
|
613
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
614
|
+
|
|
615
|
+
:root {
|
|
616
|
+
--bg-deep: #05070c;
|
|
617
|
+
--panel-surface: #0a0e17;
|
|
618
|
+
--panel-border: rgba(255, 255, 255, 0.07);
|
|
619
|
+
--panel-highlight: rgba(255, 255, 255, 0.12);
|
|
620
|
+
--text-main: #f8fafc;
|
|
621
|
+
--text-muted: #64748b;
|
|
622
|
+
--text-subtle: #475569;
|
|
623
|
+
--emerald-bright: #34d399;
|
|
624
|
+
--emerald-vivid: #10b981;
|
|
625
|
+
--emerald-deep: #047857;
|
|
626
|
+
--emerald-dark: #064e3b;
|
|
627
|
+
--tile-size: 16px;
|
|
628
|
+
--tile-gap: 4.5px;
|
|
629
|
+
--tile-radius: 3px;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
body {
|
|
633
|
+
overflow-x: hidden;
|
|
634
|
+
background: radial-gradient(circle at 50% -10%, rgba(16, 185, 129, 0.12), transparent 70%),
|
|
635
|
+
radial-gradient(circle at 10% 100%, rgba(5, 150, 105, 0.05), transparent 50%),
|
|
636
|
+
var(--bg-deep);
|
|
637
|
+
color: var(--text-main);
|
|
638
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", "Geist", sans-serif;
|
|
639
|
+
min-height: 100vh;
|
|
640
|
+
display: flex;
|
|
641
|
+
flex-direction: column;
|
|
642
|
+
align-items: center;
|
|
643
|
+
justify-content: center;
|
|
644
|
+
padding: 48px 24px;
|
|
645
|
+
-webkit-font-smoothing: antialiased;
|
|
646
|
+
-moz-osx-font-smoothing: grayscale;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.container {
|
|
650
|
+
width: 100%;
|
|
651
|
+
max-width: 1220px;
|
|
652
|
+
background: var(--panel-surface);
|
|
653
|
+
border: 1px solid var(--panel-border);
|
|
654
|
+
border-radius: 20px;
|
|
655
|
+
padding: 38px 44px;
|
|
656
|
+
box-shadow: 0 40px 100px -20px rgba(0, 0, 0, 0.95),
|
|
657
|
+
0 0 0 1px rgba(255, 255, 255, 0.03) inset,
|
|
658
|
+
0 0 80px rgba(16, 185, 129, 0.04);
|
|
659
|
+
position: relative;
|
|
660
|
+
overflow: hidden;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.container::before {
|
|
664
|
+
content: "";
|
|
665
|
+
position: absolute;
|
|
666
|
+
top: 0;
|
|
667
|
+
left: 0;
|
|
668
|
+
right: 0;
|
|
669
|
+
height: 1px;
|
|
670
|
+
background: linear-gradient(90deg, transparent, rgba(52, 211, 153, 0.4), transparent);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/* Header Bar */
|
|
674
|
+
.header {
|
|
675
|
+
display: flex;
|
|
676
|
+
justify-content: space-between;
|
|
677
|
+
align-items: center;
|
|
678
|
+
padding-bottom: 30px;
|
|
679
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
680
|
+
flex-wrap: wrap;
|
|
681
|
+
gap: 24px;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.title-group {
|
|
685
|
+
display: flex;
|
|
686
|
+
align-items: center;
|
|
687
|
+
gap: 16px;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.badge-icon {
|
|
691
|
+
width: 44px;
|
|
692
|
+
height: 44px;
|
|
693
|
+
border-radius: 12px;
|
|
694
|
+
background: linear-gradient(135deg, rgba(16, 185, 129, 0.25), rgba(4, 120, 87, 0.08));
|
|
695
|
+
border: 1px solid rgba(52, 211, 153, 0.4);
|
|
696
|
+
display: flex;
|
|
697
|
+
align-items: center;
|
|
698
|
+
justify-content: center;
|
|
699
|
+
color: var(--emerald-bright);
|
|
700
|
+
font-weight: 800;
|
|
701
|
+
font-size: 17px;
|
|
702
|
+
font-family: ui-monospace, "SF Mono", monospace;
|
|
703
|
+
box-shadow: 0 0 24px rgba(16, 185, 129, 0.2), inset 0 1px 1px rgba(255, 255, 255, 0.2);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
h1 {
|
|
707
|
+
font-size: 21px;
|
|
708
|
+
font-weight: 700;
|
|
709
|
+
color: #ffffff;
|
|
710
|
+
letter-spacing: -0.025em;
|
|
711
|
+
line-height: 1.2;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.subtext {
|
|
715
|
+
font-size: 13px;
|
|
716
|
+
color: var(--text-muted);
|
|
717
|
+
margin-top: 4px;
|
|
718
|
+
font-family: ui-monospace, "SF Mono", monospace;
|
|
719
|
+
display: flex;
|
|
720
|
+
align-items: center;
|
|
721
|
+
gap: 8px;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.status-pulse {
|
|
725
|
+
display: inline-flex;
|
|
726
|
+
align-items: center;
|
|
727
|
+
gap: 6px;
|
|
728
|
+
font-size: 11px;
|
|
729
|
+
color: var(--emerald-bright);
|
|
730
|
+
background: rgba(16, 185, 129, 0.1);
|
|
731
|
+
border: 1px solid rgba(52, 211, 153, 0.25);
|
|
732
|
+
border-radius: 20px;
|
|
733
|
+
padding: 2px 10px;
|
|
734
|
+
text-transform: uppercase;
|
|
735
|
+
letter-spacing: 0.06em;
|
|
736
|
+
font-weight: 600;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.status-pulse-dot {
|
|
740
|
+
width: 6px;
|
|
741
|
+
height: 6px;
|
|
742
|
+
border-radius: 50%;
|
|
743
|
+
background: var(--emerald-bright);
|
|
744
|
+
box-shadow: 0 0 8px var(--emerald-bright);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/* Structured Metric Bays */
|
|
748
|
+
.summary-metrics {
|
|
749
|
+
display: flex;
|
|
750
|
+
gap: 14px;
|
|
751
|
+
font-family: ui-monospace, "SF Mono", monospace;
|
|
752
|
+
flex-wrap: wrap;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.summary-item {
|
|
756
|
+
display: flex;
|
|
757
|
+
flex-direction: column;
|
|
758
|
+
align-items: flex-end;
|
|
759
|
+
background: rgba(255, 255, 255, 0.02);
|
|
760
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
761
|
+
border-radius: 12px;
|
|
762
|
+
padding: 12px 20px;
|
|
763
|
+
min-width: 125px;
|
|
764
|
+
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.03);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.summary-label {
|
|
768
|
+
font-size: 10px;
|
|
769
|
+
text-transform: uppercase;
|
|
770
|
+
letter-spacing: 0.1em;
|
|
771
|
+
color: var(--text-muted);
|
|
772
|
+
font-weight: 600;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.summary-val {
|
|
776
|
+
color: #ffffff;
|
|
777
|
+
font-size: 18px;
|
|
778
|
+
font-weight: 700;
|
|
779
|
+
margin-top: 5px;
|
|
780
|
+
letter-spacing: -0.02em;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.summary-val.emerald {
|
|
784
|
+
color: var(--emerald-bright);
|
|
785
|
+
text-shadow: 0 0 20px rgba(52, 211, 153, 0.45);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/* Heatmap Layout & Viewport */
|
|
789
|
+
.heatmap-wrapper {
|
|
790
|
+
padding-top: 34px;
|
|
791
|
+
overflow-x: auto;
|
|
792
|
+
scrollbar-width: thin;
|
|
793
|
+
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.heatmap-wrapper::-webkit-scrollbar {
|
|
797
|
+
height: 6px;
|
|
798
|
+
}
|
|
799
|
+
.heatmap-wrapper::-webkit-scrollbar-thumb {
|
|
800
|
+
background: rgba(255, 255, 255, 0.1);
|
|
801
|
+
border-radius: 3px;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.months-row {
|
|
805
|
+
position: relative;
|
|
806
|
+
height: 22px;
|
|
807
|
+
margin-left: 42px;
|
|
808
|
+
margin-bottom: 10px;
|
|
809
|
+
font-size: 12px;
|
|
810
|
+
font-weight: 600;
|
|
811
|
+
color: var(--text-muted);
|
|
812
|
+
font-family: ui-monospace, "SF Mono", monospace;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.month-label {
|
|
816
|
+
position: absolute;
|
|
817
|
+
user-select: none;
|
|
818
|
+
letter-spacing: -0.01em;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.grid-container {
|
|
822
|
+
display: flex;
|
|
823
|
+
gap: 12px;
|
|
824
|
+
align-items: start;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.weekday-labels {
|
|
828
|
+
display: grid;
|
|
829
|
+
grid-template-rows: repeat(7, var(--tile-size));
|
|
830
|
+
gap: var(--tile-gap);
|
|
831
|
+
font-size: 11px;
|
|
832
|
+
color: var(--text-subtle);
|
|
833
|
+
font-family: ui-monospace, "SF Mono", monospace;
|
|
834
|
+
font-weight: 600;
|
|
835
|
+
user-select: none;
|
|
836
|
+
line-height: var(--tile-size);
|
|
837
|
+
text-align: right;
|
|
838
|
+
padding-right: 6px;
|
|
839
|
+
width: 34px;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.tiles-grid {
|
|
843
|
+
display: grid;
|
|
844
|
+
grid-template-rows: repeat(7, var(--tile-size));
|
|
845
|
+
grid-auto-flow: column;
|
|
846
|
+
grid-auto-columns: var(--tile-size);
|
|
847
|
+
gap: var(--tile-gap);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.tile {
|
|
851
|
+
width: var(--tile-size);
|
|
852
|
+
height: var(--tile-size);
|
|
853
|
+
border-radius: var(--tile-radius);
|
|
854
|
+
cursor: pointer;
|
|
855
|
+
transition: transform 0.16s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.16s ease, border-color 0.16s ease;
|
|
856
|
+
outline: 1px solid transparent;
|
|
857
|
+
position: relative;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.tile:hover {
|
|
861
|
+
transform: scale(1.38);
|
|
862
|
+
z-index: 10;
|
|
863
|
+
outline: 1px solid #ffffff;
|
|
864
|
+
box-shadow: 0 0 16px rgba(255, 255, 255, 0.65);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/* 5-Tier Competitive Millions/Billions Jewel Palette */
|
|
868
|
+
.tier-0 {
|
|
869
|
+
background: #0d121c;
|
|
870
|
+
border: 1px solid rgba(255, 255, 255, 0.04);
|
|
871
|
+
}
|
|
872
|
+
.tier-1 {
|
|
873
|
+
background: #064e3b;
|
|
874
|
+
border: 1px solid #047857;
|
|
875
|
+
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
|
876
|
+
}
|
|
877
|
+
.tier-2 {
|
|
878
|
+
background: #047857;
|
|
879
|
+
border: 1px solid #10b981;
|
|
880
|
+
box-shadow: 0 0 8px rgba(4, 120, 87, 0.45), inset 0 1px 1px rgba(255, 255, 255, 0.2);
|
|
881
|
+
}
|
|
882
|
+
.tier-3 {
|
|
883
|
+
background: #10b981;
|
|
884
|
+
border: 1px solid #34d399;
|
|
885
|
+
box-shadow: 0 0 14px rgba(16, 185, 129, 0.65), inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
|
886
|
+
}
|
|
887
|
+
.tier-4 {
|
|
888
|
+
background: #34d399;
|
|
889
|
+
border: 1px solid #a7f3d0;
|
|
890
|
+
box-shadow: 0 0 20px rgba(52, 211, 153, 0.9), inset 0 0 4px #ffffff;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/* Footer Milestone Legend */
|
|
894
|
+
.footer {
|
|
895
|
+
display: flex;
|
|
896
|
+
justify-content: space-between;
|
|
897
|
+
align-items: center;
|
|
898
|
+
margin-top: 32px;
|
|
899
|
+
padding-top: 22px;
|
|
900
|
+
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
901
|
+
font-size: 12px;
|
|
902
|
+
color: var(--text-muted);
|
|
903
|
+
font-family: ui-monospace, "SF Mono", monospace;
|
|
904
|
+
flex-wrap: wrap;
|
|
905
|
+
gap: 16px;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.legend-container {
|
|
909
|
+
display: flex;
|
|
910
|
+
align-items: center;
|
|
911
|
+
gap: 12px;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.legend-label {
|
|
915
|
+
font-size: 11px;
|
|
916
|
+
color: var(--text-muted);
|
|
917
|
+
text-transform: uppercase;
|
|
918
|
+
letter-spacing: 0.06em;
|
|
919
|
+
font-weight: 600;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
.legend {
|
|
923
|
+
display: flex;
|
|
924
|
+
align-items: center;
|
|
925
|
+
gap: 6px;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.legend-tile {
|
|
929
|
+
width: 15px;
|
|
930
|
+
height: 15px;
|
|
931
|
+
border-radius: var(--tile-radius);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.legend-bounds {
|
|
935
|
+
font-size: 11px;
|
|
936
|
+
color: var(--text-muted);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/* Floating Glassmorphic HUD Tooltip */
|
|
940
|
+
#tooltip {
|
|
941
|
+
position: fixed;
|
|
942
|
+
display: none;
|
|
943
|
+
pointer-events: none;
|
|
944
|
+
background: rgba(10, 14, 23, 0.97);
|
|
945
|
+
backdrop-filter: blur(16px);
|
|
946
|
+
-webkit-backdrop-filter: blur(16px);
|
|
947
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
948
|
+
border-radius: 12px;
|
|
949
|
+
padding: 14px 18px;
|
|
950
|
+
font-size: 12px;
|
|
951
|
+
color: #f1f5f9;
|
|
952
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.95), 0 0 30px rgba(16, 185, 129, 0.12);
|
|
953
|
+
font-family: ui-monospace, "SF Mono", monospace;
|
|
954
|
+
z-index: 1000;
|
|
955
|
+
line-height: 1.6;
|
|
956
|
+
min-width: 220px;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.tt-header {
|
|
960
|
+
display: flex;
|
|
961
|
+
justify-content: space-between;
|
|
962
|
+
align-items: center;
|
|
963
|
+
margin-bottom: 8px;
|
|
964
|
+
padding-bottom: 6px;
|
|
965
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
.tt-date {
|
|
969
|
+
font-weight: 700;
|
|
970
|
+
color: #ffffff;
|
|
971
|
+
font-size: 13px;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.tt-row {
|
|
975
|
+
display: flex;
|
|
976
|
+
justify-content: space-between;
|
|
977
|
+
margin-top: 3px;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.tt-label {
|
|
981
|
+
color: var(--text-muted);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
.tt-cost {
|
|
985
|
+
color: var(--emerald-bright);
|
|
986
|
+
font-weight: 700;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.tt-val {
|
|
990
|
+
font-weight: 600;
|
|
991
|
+
color: #ffffff;
|
|
992
|
+
}
|
|
993
|
+
</style>
|
|
994
|
+
</head>
|
|
995
|
+
<body>
|
|
996
|
+
<div class="container">
|
|
997
|
+
<div class="header">
|
|
998
|
+
<div class="title-group">
|
|
999
|
+
<div class="badge-icon">TS</div>
|
|
1000
|
+
<div>
|
|
1001
|
+
<h1>tokensniff dashboard</h1>
|
|
1002
|
+
<div class="subtext">
|
|
1003
|
+
<span>Year ${targetYear} Token Telemetry & Spend Ledger</span>
|
|
1004
|
+
<span class="status-pulse"><span class="status-pulse-dot"></span>Online</span>
|
|
1005
|
+
</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
</div>
|
|
1008
|
+
<div class="summary-metrics">
|
|
1009
|
+
<div class="summary-item">
|
|
1010
|
+
<span class="summary-label">Total Spend</span>
|
|
1011
|
+
<span class="summary-val emerald">$${totalCost.toFixed(3)}</span>
|
|
1012
|
+
</div>
|
|
1013
|
+
<div class="summary-item">
|
|
1014
|
+
<span class="summary-label">Total Tokens</span>
|
|
1015
|
+
<span class="summary-val">${formattedTotalTokens}</span>
|
|
1016
|
+
</div>
|
|
1017
|
+
<div class="summary-item">
|
|
1018
|
+
<span class="summary-label">Cache Ratio</span>
|
|
1019
|
+
<span class="summary-val">${cacheHitPct}%</span>
|
|
1020
|
+
</div>
|
|
1021
|
+
<div class="summary-item">
|
|
1022
|
+
<span class="summary-label">Total Turns</span>
|
|
1023
|
+
<span class="summary-val">${totalTurns}</span>
|
|
1024
|
+
</div>
|
|
1025
|
+
</div>
|
|
1026
|
+
</div>
|
|
1027
|
+
|
|
1028
|
+
<div class="heatmap-wrapper">
|
|
1029
|
+
<div id="months-row" class="months-row"></div>
|
|
1030
|
+
<div class="grid-container">
|
|
1031
|
+
<div class="weekday-labels">
|
|
1032
|
+
<span></span>
|
|
1033
|
+
<span>Mon</span>
|
|
1034
|
+
<span></span>
|
|
1035
|
+
<span>Wed</span>
|
|
1036
|
+
<span></span>
|
|
1037
|
+
<span>Fri</span>
|
|
1038
|
+
<span></span>
|
|
1039
|
+
</div>
|
|
1040
|
+
<div id="tiles" class="tiles-grid"></div>
|
|
1041
|
+
</div>
|
|
1042
|
+
</div>
|
|
1043
|
+
|
|
1044
|
+
<div class="footer">
|
|
1045
|
+
<span>Auto-updated live ledger \u2022 port 4000</span>
|
|
1046
|
+
<div class="legend-container">
|
|
1047
|
+
<span class="legend-label">Daily Volume</span>
|
|
1048
|
+
<span class="legend-bounds">0</span>
|
|
1049
|
+
<div class="legend">
|
|
1050
|
+
<div class="legend-tile tier-0" title="0 tokens"></div>
|
|
1051
|
+
<div class="legend-tile tier-1" title="1M - 5M tokens"></div>
|
|
1052
|
+
<div class="legend-tile tier-2" title="5M - 25M tokens"></div>
|
|
1053
|
+
<div class="legend-tile tier-3" title="25M - 100M tokens"></div>
|
|
1054
|
+
<div class="legend-tile tier-4" title="> 100M tokens"></div>
|
|
1055
|
+
</div>
|
|
1056
|
+
<span class="legend-bounds">100M+</span>
|
|
1057
|
+
</div>
|
|
1058
|
+
</div>
|
|
1059
|
+
</div>
|
|
1060
|
+
|
|
1061
|
+
<div id="tooltip"></div>
|
|
1062
|
+
|
|
1063
|
+
<script>
|
|
1064
|
+
const data = ${JSON.stringify(dailyMap)};
|
|
1065
|
+
const year = ${targetYear};
|
|
1066
|
+
const tilesContainer = document.getElementById('tiles');
|
|
1067
|
+
const monthsRow = document.getElementById('months-row');
|
|
1068
|
+
const tooltip = document.getElementById('tooltip');
|
|
1069
|
+
|
|
1070
|
+
const startDate = new Date(year, 0, 1);
|
|
1071
|
+
const startDay = startDate.getDay(); // 0 is Sunday
|
|
1072
|
+
|
|
1073
|
+
// Pad beginning of the year so rows align to Sunday-Saturday
|
|
1074
|
+
for (let p = 0; p < startDay; p++) {
|
|
1075
|
+
const empty = document.createElement('div');
|
|
1076
|
+
empty.style.width = '16px';
|
|
1077
|
+
empty.style.height = '16px';
|
|
1078
|
+
tilesContainer.appendChild(empty);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
|
1082
|
+
const totalDays = isLeapYear ? 366 : 365;
|
|
1083
|
+
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
1084
|
+
let lastMonth = -1;
|
|
1085
|
+
|
|
1086
|
+
function formatHumanTokens(t) {
|
|
1087
|
+
if (!t || t === 0) return '0';
|
|
1088
|
+
if (t >= 1e9) return (t / 1e9).toFixed(2) + 'B';
|
|
1089
|
+
if (t >= 1e6) return (t / 1e6).toFixed(2) + 'M';
|
|
1090
|
+
if (t >= 1e3) return (t / 1e3).toFixed(1) + 'k';
|
|
1091
|
+
return t.toLocaleString();
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
for (let i = 0; i < totalDays; i++) {
|
|
1095
|
+
const cur = new Date(year, 0, 1 + i);
|
|
1096
|
+
const m = cur.getMonth();
|
|
1097
|
+
const yearStr = cur.getFullYear();
|
|
1098
|
+
const monthStr = String(cur.getMonth() + 1).padStart(2, '0');
|
|
1099
|
+
const dayStr = String(cur.getDate()).padStart(2, '0');
|
|
1100
|
+
const dStr = yearStr + '-' + monthStr + '-' + dayStr;
|
|
1101
|
+
|
|
1102
|
+
// Position month labels dynamically along the horizontal week track
|
|
1103
|
+
// 16px tile + 4.5px gap = 20.5px step
|
|
1104
|
+
if (m !== lastMonth) {
|
|
1105
|
+
lastMonth = m;
|
|
1106
|
+
const weekCol = Math.floor((i + startDay) / 7);
|
|
1107
|
+
const span = document.createElement('span');
|
|
1108
|
+
span.className = 'month-label';
|
|
1109
|
+
span.innerText = monthNames[m];
|
|
1110
|
+
span.style.left = (weekCol * 20.5) + 'px';
|
|
1111
|
+
monthsRow.appendChild(span);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const record = data[dStr];
|
|
1115
|
+
const tokens = record ? record.tokens : 0;
|
|
1116
|
+
const cost = record ? record.cost : 0;
|
|
1117
|
+
const turns = record ? record.turns : 0;
|
|
1118
|
+
const cacheRead = record ? record.cacheRead : 0;
|
|
1119
|
+
|
|
1120
|
+
// Competitive Millions & Billions Tiers
|
|
1121
|
+
let tier = 'tier-0';
|
|
1122
|
+
if (tokens >= 100000000) tier = 'tier-4'; // > 100M tokens
|
|
1123
|
+
else if (tokens >= 25000000) tier = 'tier-3'; // 25M - 100M tokens
|
|
1124
|
+
else if (tokens >= 5000000) tier = 'tier-2'; // 5M - 25M tokens
|
|
1125
|
+
else if (tokens > 0) tier = 'tier-1'; // 1M - 5M tokens
|
|
1126
|
+
|
|
1127
|
+
const tile = document.createElement('div');
|
|
1128
|
+
tile.className = 'tile ' + tier;
|
|
1129
|
+
|
|
1130
|
+
function positionTooltip(e) {
|
|
1131
|
+
const tipWidth = tooltip.offsetWidth || 240;
|
|
1132
|
+
const tipHeight = tooltip.offsetHeight || 160;
|
|
1133
|
+
let x = e.clientX + 16;
|
|
1134
|
+
let y = e.clientY + 16;
|
|
1135
|
+
|
|
1136
|
+
// If hovering near the right edge of the screen (e.g. December), flip to the left of the cursor
|
|
1137
|
+
if (x + tipWidth > window.innerWidth - 16) {
|
|
1138
|
+
x = e.clientX - tipWidth - 16;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// If hovering near the bottom of the screen, flip upward above the cursor
|
|
1142
|
+
if (y + tipHeight > window.innerHeight - 16) {
|
|
1143
|
+
y = e.clientY - tipHeight - 16;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
tooltip.style.left = Math.max(12, x) + 'px';
|
|
1147
|
+
tooltip.style.top = Math.max(12, y) + 'px';
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
tile.onmouseenter = (e) => {
|
|
1151
|
+
const cachePct = tokens > 0 ? ((cacheRead / tokens) * 100).toFixed(1) : '0.0';
|
|
1152
|
+
tooltip.innerHTML =
|
|
1153
|
+
'<div class="tt-header">' +
|
|
1154
|
+
'<span class="tt-date">' + cur.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }) + '</span>' +
|
|
1155
|
+
'<span class="tt-cost">$' + cost.toFixed(4) + '</span>' +
|
|
1156
|
+
'</div>' +
|
|
1157
|
+
'<div class="tt-row"><span class="tt-label">Volume</span><span class="tt-val">' + formatHumanTokens(tokens) + '</span></div>' +
|
|
1158
|
+
'<div class="tt-row"><span class="tt-label">Exact Tokens</span><span class="tt-val">' + tokens.toLocaleString() + '</span></div>' +
|
|
1159
|
+
'<div class="tt-row"><span class="tt-label">Cache Leverage</span><span class="tt-val">' + formatHumanTokens(cacheRead) + ' (' + cachePct + '%)</span></div>' +
|
|
1160
|
+
'<div class="tt-row"><span class="tt-label">Turns Logged</span><span class="tt-val">' + turns + '</span></div>';
|
|
1161
|
+
positionTooltip(e);
|
|
1162
|
+
tooltip.style.display = 'block';
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
tile.onmousemove = (e) => {
|
|
1166
|
+
positionTooltip(e);
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
tile.onmouseleave = () => {
|
|
1170
|
+
tooltip.style.display = 'none';
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
tilesContainer.appendChild(tile);
|
|
1174
|
+
}
|
|
1175
|
+
</script>
|
|
1176
|
+
</body>
|
|
1177
|
+
</html>`;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// src/shared/schema.ts
|
|
1181
|
+
var SCHEMA_VERSION = 1;
|
|
1182
|
+
function toFiniteNumber(value, fallback = 0) {
|
|
1183
|
+
const num = Number(value);
|
|
1184
|
+
return Number.isFinite(num) ? num : fallback;
|
|
1185
|
+
}
|
|
1186
|
+
function buildLatest(partial) {
|
|
1187
|
+
return {
|
|
1188
|
+
v: SCHEMA_VERSION,
|
|
1189
|
+
session_id: partial.session_id,
|
|
1190
|
+
turn_index: Math.max(1, Math.floor(toFiniteNumber(partial.turn_index, 1))),
|
|
1191
|
+
ts: toFiniteNumber(partial.ts, Date.now()),
|
|
1192
|
+
model: typeof partial.model === "string" && partial.model.length > 0 ? partial.model : "unknown",
|
|
1193
|
+
is_bg: Boolean(partial.is_bg),
|
|
1194
|
+
ctx: Math.max(0, toFiniteNumber(partial.ctx, 0)),
|
|
1195
|
+
window: Math.max(1, toFiniteNumber(partial.window, 1e6)),
|
|
1196
|
+
pct: Math.max(0, Math.min(100, toFiniteNumber(partial.pct, 0))),
|
|
1197
|
+
cache_read: Math.max(0, toFiniteNumber(partial.cache_read, 0)),
|
|
1198
|
+
cache_create: Math.max(0, toFiniteNumber(partial.cache_create, 0)),
|
|
1199
|
+
delta_in: toFiniteNumber(partial.delta_in, 0),
|
|
1200
|
+
in_tokens: Math.max(0, toFiniteNumber(partial.in_tokens, 0)),
|
|
1201
|
+
out_tokens: Math.max(0, toFiniteNumber(partial.out_tokens, 0)),
|
|
1202
|
+
thinking: Boolean(partial.thinking),
|
|
1203
|
+
thinking_tokens: Math.max(0, toFiniteNumber(partial.thinking_tokens, 0)),
|
|
1204
|
+
tool: typeof partial.tool === "string" && partial.tool.length > 0 ? partial.tool : null,
|
|
1205
|
+
tool_tk: Math.max(0, toFiniteNumber(partial.tool_tk, 0)),
|
|
1206
|
+
tools_summary: Array.isArray(partial.tools_summary) ? partial.tools_summary : [],
|
|
1207
|
+
stop_reason: typeof partial.stop_reason === "string" ? partial.stop_reason : "",
|
|
1208
|
+
is_label: Boolean(partial.is_label),
|
|
1209
|
+
label: typeof partial.label === "string" ? partial.label : "",
|
|
1210
|
+
ttft_ms: Math.max(0, toFiniteNumber(partial.ttft_ms, 0)),
|
|
1211
|
+
tps: Math.max(0, toFiniteNumber(partial.tps, 0)),
|
|
1212
|
+
dur_s: Math.max(0, toFiniteNumber(partial.dur_s, 0)),
|
|
1213
|
+
cost_turn: Math.max(0, toFiniteNumber(partial.cost_turn, 0)),
|
|
1214
|
+
cost_session: Math.max(0, toFiniteNumber(partial.cost_session, 0)),
|
|
1215
|
+
cost_today: Math.max(0, toFiniteNumber(partial.cost_today, 0)),
|
|
1216
|
+
error: typeof partial.error === "number" && Number.isFinite(partial.error) ? partial.error : null,
|
|
1217
|
+
status: Math.max(0, toFiniteNumber(partial.status, 0)),
|
|
1218
|
+
quota_pct: typeof partial.quota_pct === "number" && Number.isFinite(partial.quota_pct) ? Math.max(0, Math.min(100, partial.quota_pct)) : null,
|
|
1219
|
+
quota_reset: typeof partial.quota_reset === "string" ? partial.quota_reset : null,
|
|
1220
|
+
tier: typeof partial.tier === "string" ? partial.tier : null
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
function extractCleanLabel(text) {
|
|
1224
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
1225
|
+
return "";
|
|
1226
|
+
}
|
|
1227
|
+
const trimmed = text.trim();
|
|
1228
|
+
if (trimmed.startsWith("{")) {
|
|
1229
|
+
if (trimmed.endsWith("}")) {
|
|
1230
|
+
try {
|
|
1231
|
+
const parsed = JSON.parse(trimmed);
|
|
1232
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1233
|
+
const record = parsed;
|
|
1234
|
+
if (record.state && record.detail) {
|
|
1235
|
+
return `${String(record.state).trim()}: ${String(record.detail).trim()}`;
|
|
1236
|
+
}
|
|
1237
|
+
const candidate = record.title ?? record.summary ?? record.label ?? record.message ?? record.detail ?? record.state;
|
|
1238
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
1239
|
+
return candidate.trim();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
} catch {
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const stateMatch = trimmed.match(/"state"\s*:\s*"([^"\\]*)(?:"|$)/);
|
|
1246
|
+
const detailMatch = trimmed.match(/"detail"\s*:\s*"([^"\\]*)(?:"|$)/);
|
|
1247
|
+
if (stateMatch?.[1] && detailMatch?.[1]) {
|
|
1248
|
+
return `${stateMatch[1].trim()}: ${detailMatch[1].trim()}`;
|
|
1249
|
+
}
|
|
1250
|
+
const singleMatch = trimmed.match(
|
|
1251
|
+
/"(?:title|summary|label|message|detail|state)"\s*:\s*"([^"\\]*)(?:"|$)/
|
|
1252
|
+
);
|
|
1253
|
+
if (singleMatch?.[1]) {
|
|
1254
|
+
return singleMatch[1].trim();
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return trimmed.replace(/^["']+|["']+$/g, "").trim();
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/collector/parse.ts
|
|
1261
|
+
function parseSafeJson(raw) {
|
|
1262
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
return JSON.parse(raw);
|
|
1267
|
+
} catch {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function extractFirstInt(pattern, target) {
|
|
1272
|
+
const match = target.match(pattern);
|
|
1273
|
+
if (!match?.[1]) return 0;
|
|
1274
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
1275
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
1276
|
+
}
|
|
1277
|
+
function groupTools(tools) {
|
|
1278
|
+
if (tools.length === 0) return [];
|
|
1279
|
+
const map = /* @__PURE__ */ new Map();
|
|
1280
|
+
for (const t of tools) {
|
|
1281
|
+
if (!t.name) continue;
|
|
1282
|
+
const existing = map.get(t.name);
|
|
1283
|
+
if (existing) {
|
|
1284
|
+
existing.count += 1;
|
|
1285
|
+
existing.arg_tk += t.argTk;
|
|
1286
|
+
} else {
|
|
1287
|
+
map.set(t.name, {
|
|
1288
|
+
name: t.name,
|
|
1289
|
+
count: 1,
|
|
1290
|
+
arg_tk: t.argTk
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return Array.from(map.values()).map((v) => ({
|
|
1295
|
+
name: v.name,
|
|
1296
|
+
count: v.count,
|
|
1297
|
+
arg_tk: v.arg_tk
|
|
1298
|
+
}));
|
|
1299
|
+
}
|
|
1300
|
+
function parseSseStream(fallbackModel, payload) {
|
|
1301
|
+
const input = extractFirstInt(/"input_tokens"\s*:\s*(\d+)/, payload);
|
|
1302
|
+
const cacheReadMatches = Array.from(payload.matchAll(/"cache_read_input_tokens"\s*:\s*(\d+)/g));
|
|
1303
|
+
const cacheRead = cacheReadMatches.length > 0 ? Math.max(
|
|
1304
|
+
...cacheReadMatches.map((m) => {
|
|
1305
|
+
const val = m[1] ? Number.parseInt(m[1], 10) : 0;
|
|
1306
|
+
return Number.isFinite(val) ? val : 0;
|
|
1307
|
+
})
|
|
1308
|
+
) : 0;
|
|
1309
|
+
const cacheCreateMatches = Array.from(
|
|
1310
|
+
payload.matchAll(/"cache_creation_input_tokens"\s*:\s*(\d+)/g)
|
|
1311
|
+
);
|
|
1312
|
+
const cacheCreate = cacheCreateMatches.length > 0 ? Math.max(
|
|
1313
|
+
...cacheCreateMatches.map((m) => {
|
|
1314
|
+
const val = m[1] ? Number.parseInt(m[1], 10) : 0;
|
|
1315
|
+
return Number.isFinite(val) ? val : 0;
|
|
1316
|
+
})
|
|
1317
|
+
) : 0;
|
|
1318
|
+
const outputMatches = Array.from(payload.matchAll(/"output_tokens"\s*:\s*(\d+)/g));
|
|
1319
|
+
const lastOutputMatch = outputMatches[outputMatches.length - 1];
|
|
1320
|
+
const output = lastOutputMatch?.[1] ? Number.parseInt(lastOutputMatch[1], 10) : 0;
|
|
1321
|
+
const stopMatches = Array.from(payload.matchAll(/"stop_reason"\s*:\s*"([^"]*)"/g));
|
|
1322
|
+
const lastStopMatch = stopMatches[stopMatches.length - 1];
|
|
1323
|
+
const stopReason = lastStopMatch?.[1] ?? "";
|
|
1324
|
+
const toolNames = Array.from(
|
|
1325
|
+
payload.matchAll(/"type"\s*:\s*"tool_use"[^}]*?"name"\s*:\s*"([^"]+)"/g)
|
|
1326
|
+
).map((m) => m[1] ?? "");
|
|
1327
|
+
const partialChunks = Array.from(
|
|
1328
|
+
payload.matchAll(/"partial_json"\s*:\s*"((?:\\.|[^"\\])*)"/g)
|
|
1329
|
+
).map((m) => m[1] ?? "");
|
|
1330
|
+
let argChars = 0;
|
|
1331
|
+
try {
|
|
1332
|
+
argChars = partialChunks.join("").replace(/\\./g, "X").length;
|
|
1333
|
+
} catch {
|
|
1334
|
+
argChars = 0;
|
|
1335
|
+
}
|
|
1336
|
+
const estimatedTotalArgTk = Math.max(0, Math.round(argChars / 4));
|
|
1337
|
+
const avgArgTk = toolNames.length > 0 ? Math.max(1, Math.round(estimatedTotalArgTk / toolNames.length)) : 0;
|
|
1338
|
+
const tools = toolNames.map((name) => ({
|
|
1339
|
+
name,
|
|
1340
|
+
argTk: avgArgTk
|
|
1341
|
+
}));
|
|
1342
|
+
let thinkingTokens = extractFirstInt(/"thinking_tokens"\s*:\s*(\d+)/, payload) || extractFirstInt(/"reasoning_tokens"\s*:\s*(\d+)/, payload) || extractFirstInt(/"reasoningTokenCount"\s*:\s*(\d+)/, payload);
|
|
1343
|
+
if (thinkingTokens === 0) {
|
|
1344
|
+
const thinkingDeltas = Array.from(
|
|
1345
|
+
payload.matchAll(/"thinking"\s*:\s*"((?:\\.|[^"\\])*)"/g)
|
|
1346
|
+
).map((m) => m[1] ?? "");
|
|
1347
|
+
if (thinkingDeltas.length > 0) {
|
|
1348
|
+
const thinkingChars = thinkingDeltas.join("").length;
|
|
1349
|
+
thinkingTokens = Math.max(1, Math.round(thinkingChars / 4));
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const thinking = thinkingTokens > 0 || payload.includes("thinking_delta") || payload.includes('"type":"thinking"') || payload.includes('"thought":true');
|
|
1353
|
+
const kind = tools.length > 0 ? "tool" : thinking ? "thinking" : "text";
|
|
1354
|
+
return {
|
|
1355
|
+
model: fallbackModel,
|
|
1356
|
+
kind,
|
|
1357
|
+
input,
|
|
1358
|
+
output,
|
|
1359
|
+
cacheRead,
|
|
1360
|
+
cacheCreate,
|
|
1361
|
+
tools,
|
|
1362
|
+
toolsSummary: groupTools(tools),
|
|
1363
|
+
thinking,
|
|
1364
|
+
thinkingTokens,
|
|
1365
|
+
stopReason,
|
|
1366
|
+
label: ""
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
function parseJsonPayload(fallbackModel, payload) {
|
|
1370
|
+
const parsed = parseSafeJson(payload);
|
|
1371
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1372
|
+
const data = parsed;
|
|
1373
|
+
const usage = data.usage ?? {};
|
|
1374
|
+
const tools = [];
|
|
1375
|
+
let rawText = "";
|
|
1376
|
+
let thinking = false;
|
|
1377
|
+
let thinkingChars = 0;
|
|
1378
|
+
if (Array.isArray(data.content)) {
|
|
1379
|
+
for (const item of data.content) {
|
|
1380
|
+
if (!item) continue;
|
|
1381
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
1382
|
+
rawText += item.text;
|
|
1383
|
+
} else if (item.type === "tool_use" && typeof item.name === "string") {
|
|
1384
|
+
const argStr = JSON.stringify(item.input ?? {});
|
|
1385
|
+
tools.push({
|
|
1386
|
+
name: item.name,
|
|
1387
|
+
argTk: Math.max(1, Math.round(argStr.length / 4))
|
|
1388
|
+
});
|
|
1389
|
+
} else if (item.type === "thinking") {
|
|
1390
|
+
thinking = true;
|
|
1391
|
+
if (typeof item.thinking === "string") {
|
|
1392
|
+
thinkingChars += item.thinking.length;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
} else if (typeof data.content === "string") {
|
|
1397
|
+
rawText = data.content;
|
|
1398
|
+
}
|
|
1399
|
+
if (Array.isArray(data.candidates)) {
|
|
1400
|
+
for (const candidate of data.candidates) {
|
|
1401
|
+
const parts = candidate?.content?.parts;
|
|
1402
|
+
if (Array.isArray(parts)) {
|
|
1403
|
+
for (const part of parts) {
|
|
1404
|
+
if (!part) continue;
|
|
1405
|
+
if (typeof part.text === "string") {
|
|
1406
|
+
if (part.thought) {
|
|
1407
|
+
thinking = true;
|
|
1408
|
+
thinkingChars += part.text.length;
|
|
1409
|
+
} else {
|
|
1410
|
+
rawText += part.text;
|
|
1411
|
+
}
|
|
1412
|
+
} else if (part.functionCall?.name) {
|
|
1413
|
+
const argStr = JSON.stringify(part.functionCall.args ?? {});
|
|
1414
|
+
tools.push({
|
|
1415
|
+
name: part.functionCall.name,
|
|
1416
|
+
argTk: Math.max(1, Math.round(argStr.length / 4))
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
let thinkingTokens = usage.thinking_tokens ?? usage.reasoning_tokens ?? data.usageMetadata?.candidatesTokenDetails?.[0]?.reasoningTokenCount ?? 0;
|
|
1424
|
+
if (!thinkingTokens && thinkingChars > 0) {
|
|
1425
|
+
thinkingTokens = Math.max(1, Math.round(thinkingChars / 4));
|
|
1426
|
+
}
|
|
1427
|
+
if (thinkingTokens > 0) {
|
|
1428
|
+
thinking = true;
|
|
1429
|
+
}
|
|
1430
|
+
const input = usage.input_tokens ?? usage.prompt_tokens ?? data.usageMetadata?.promptTokenCount ?? 0;
|
|
1431
|
+
const output = usage.output_tokens ?? usage.completion_tokens ?? data.usageMetadata?.candidatesTokenCount ?? 0;
|
|
1432
|
+
const cacheRead = usage.cache_read_input_tokens ?? data.usageMetadata?.cachedContentTokenCount ?? 0;
|
|
1433
|
+
const cacheCreate = usage.cache_creation_input_tokens ?? 0;
|
|
1434
|
+
const isMicro = input < 2e3 && output < 60 && tools.length === 0 && !thinking;
|
|
1435
|
+
let kind = "text";
|
|
1436
|
+
if (tools.length > 0) {
|
|
1437
|
+
kind = "tool";
|
|
1438
|
+
} else if (thinking) {
|
|
1439
|
+
kind = "thinking";
|
|
1440
|
+
} else if (isMicro) {
|
|
1441
|
+
kind = "label";
|
|
1442
|
+
}
|
|
1443
|
+
const cleanLabel = isMicro ? extractCleanLabel(rawText).slice(0, 120) : "";
|
|
1444
|
+
return {
|
|
1445
|
+
model: data.model || fallbackModel,
|
|
1446
|
+
kind,
|
|
1447
|
+
input,
|
|
1448
|
+
output,
|
|
1449
|
+
cacheRead,
|
|
1450
|
+
cacheCreate,
|
|
1451
|
+
tools,
|
|
1452
|
+
toolsSummary: groupTools(tools),
|
|
1453
|
+
thinking,
|
|
1454
|
+
thinkingTokens,
|
|
1455
|
+
stopReason: data.stop_reason ?? data.candidates?.[0]?.finishReason ?? "end_turn",
|
|
1456
|
+
label: cleanLabel
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
function parseTurn(interaction) {
|
|
1460
|
+
const fallbackResult = {
|
|
1461
|
+
model: "unknown",
|
|
1462
|
+
kind: "unknown",
|
|
1463
|
+
input: 0,
|
|
1464
|
+
output: 0,
|
|
1465
|
+
cacheRead: 0,
|
|
1466
|
+
cacheCreate: 0,
|
|
1467
|
+
tools: [],
|
|
1468
|
+
toolsSummary: [],
|
|
1469
|
+
thinking: false,
|
|
1470
|
+
thinkingTokens: 0,
|
|
1471
|
+
stopReason: "",
|
|
1472
|
+
label: ""
|
|
1473
|
+
};
|
|
1474
|
+
try {
|
|
1475
|
+
let resolvedModel = "unknown";
|
|
1476
|
+
const parsedReq = parseSafeJson(interaction.reqBody);
|
|
1477
|
+
if (parsedReq && typeof parsedReq === "object" && "model" in parsedReq && typeof parsedReq.model === "string") {
|
|
1478
|
+
resolvedModel = parsedReq.model;
|
|
1479
|
+
}
|
|
1480
|
+
const resString = interaction.resBody ?? "";
|
|
1481
|
+
const isError = (interaction.statusCode ?? 0) === 429 || (interaction.statusCode ?? 0) >= 500;
|
|
1482
|
+
const isSse = (interaction.contentType ?? "").includes("text/event-stream") || resString.includes("event: message_start");
|
|
1483
|
+
if (isError) {
|
|
1484
|
+
const base = isSse ? parseSseStream(resolvedModel, resString) : parseJsonPayload(resolvedModel, resString) ?? fallbackResult;
|
|
1485
|
+
return { ...base, kind: "error" };
|
|
1486
|
+
}
|
|
1487
|
+
if (isSse) {
|
|
1488
|
+
return parseSseStream(resolvedModel, resString);
|
|
1489
|
+
}
|
|
1490
|
+
return parseJsonPayload(resolvedModel, resString) ?? fallbackResult;
|
|
1491
|
+
} catch {
|
|
1492
|
+
return fallbackResult;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
function extractSessionId(headers, rawReqBody) {
|
|
1496
|
+
let candidate = "default";
|
|
1497
|
+
if (headers && typeof headers["x-claude-code-session-id"] === "string") {
|
|
1498
|
+
candidate = headers["x-claude-code-session-id"];
|
|
1499
|
+
} else {
|
|
1500
|
+
const parsedBody = parseSafeJson(rawReqBody);
|
|
1501
|
+
if (parsedBody && typeof parsedBody === "object" && "metadata" in parsedBody && typeof parsedBody.metadata === "object" && parsedBody.metadata !== null) {
|
|
1502
|
+
const meta = parsedBody.metadata;
|
|
1503
|
+
if (typeof meta.session_id === "string" && meta.session_id.trim().length > 0) {
|
|
1504
|
+
candidate = meta.session_id;
|
|
1505
|
+
} else if (typeof meta.user_id === "string") {
|
|
1506
|
+
const userMeta = parseSafeJson(meta.user_id);
|
|
1507
|
+
if (userMeta && typeof userMeta === "object" && "session_id" in userMeta && typeof userMeta.session_id === "string") {
|
|
1508
|
+
candidate = userMeta.session_id;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const sanitized = candidate.trim().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
1514
|
+
return sanitized.length > 0 ? sanitized : "default";
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/collector/index.ts
|
|
1518
|
+
var MAX_TRACKED_SESSIONS = 500;
|
|
1519
|
+
function setBoundedLRU(map, key, value) {
|
|
1520
|
+
if (!map.has(key) && map.size >= MAX_TRACKED_SESSIONS) {
|
|
1521
|
+
const oldestKey = map.keys().next().value;
|
|
1522
|
+
if (oldestKey !== void 0) {
|
|
1523
|
+
map.delete(oldestKey);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
map.set(key, value);
|
|
1527
|
+
}
|
|
1528
|
+
function getUpstreamCandidates(host) {
|
|
1529
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
|
|
1530
|
+
return ["127.0.0.1", "::1"];
|
|
1531
|
+
}
|
|
1532
|
+
return [host];
|
|
1533
|
+
}
|
|
1534
|
+
function start(userConfig) {
|
|
1535
|
+
const config = { ...loadConfig(), ...userConfig || {} };
|
|
1536
|
+
for (const dir of config.statusDirs) {
|
|
1537
|
+
ensureDir(dir);
|
|
1538
|
+
}
|
|
1539
|
+
pruneDeadSessions(config.statusDirs, config.deadSessionMs);
|
|
1540
|
+
const contextHistory = /* @__PURE__ */ new Map();
|
|
1541
|
+
const turnIndexBySession = /* @__PURE__ */ new Map();
|
|
1542
|
+
const sessionSpending = loadTotals(config.statusDirs);
|
|
1543
|
+
let activeLocalDate = getLocalDateString();
|
|
1544
|
+
let todayTotalSpend = loadTodaySpend(config.statusDirs, activeLocalDate);
|
|
1545
|
+
const upstreamHosts = getUpstreamCandidates(config.upstreamHost);
|
|
1546
|
+
const server = http.createServer((clientReq, clientRes) => {
|
|
1547
|
+
const reqUrl = clientReq.url || "/";
|
|
1548
|
+
const reqMethod = clientReq.method?.toUpperCase() || "GET";
|
|
1549
|
+
if (reqMethod === "GET" && (reqUrl === "/" || reqUrl === "/dashboard")) {
|
|
1550
|
+
const dailyMap = loadDailyRollup(config.statusDirs);
|
|
1551
|
+
const html = renderHeatmapHtml(dailyMap, (/* @__PURE__ */ new Date()).getFullYear());
|
|
1552
|
+
clientRes.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1553
|
+
clientRes.end(html);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
if (reqMethod === "GET" && reqUrl === "/api/daily") {
|
|
1557
|
+
const dailyMap = loadDailyRollup(config.statusDirs);
|
|
1558
|
+
clientRes.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
1559
|
+
clientRes.end(JSON.stringify(dailyMap));
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const startTime = Date.now();
|
|
1563
|
+
const headers = clientReq.headers || {};
|
|
1564
|
+
const isModelExecution = reqUrl.includes("/v1/messages");
|
|
1565
|
+
const isBackgroundAgent = headers["x-app"] === "cli-bg";
|
|
1566
|
+
const contentLength = Number(headers["content-length"]);
|
|
1567
|
+
if (Number.isFinite(contentLength) && contentLength > config.maxBodyBytes) {
|
|
1568
|
+
clientRes.writeHead(413, { "content-type": "text/plain" });
|
|
1569
|
+
clientRes.end("Payload Too Large");
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
let resolvedSessionId = sanitizeSessionId(
|
|
1573
|
+
extractSessionId(headers, "")
|
|
1574
|
+
);
|
|
1575
|
+
const requestChunks = [];
|
|
1576
|
+
let receivedBytes = 0;
|
|
1577
|
+
let isPayloadExceeded = false;
|
|
1578
|
+
let isRequestComplete = false;
|
|
1579
|
+
let proxyClientReq = null;
|
|
1580
|
+
let isResponseCommitted = false;
|
|
1581
|
+
const dispatch502 = (failureReason) => {
|
|
1582
|
+
console.error(
|
|
1583
|
+
`[tokensniff] upstream ${config.upstreamHost}:${config.upstreamPort} unavailable: ${failureReason}`
|
|
1584
|
+
);
|
|
1585
|
+
try {
|
|
1586
|
+
const fallbackStatus = buildLatest({
|
|
1587
|
+
session_id: resolvedSessionId,
|
|
1588
|
+
model: "unknown",
|
|
1589
|
+
ctx: contextHistory.get(resolvedSessionId) ?? 0,
|
|
1590
|
+
error: 502,
|
|
1591
|
+
status: 502,
|
|
1592
|
+
cost_session: sessionSpending[resolvedSessionId] ?? 0,
|
|
1593
|
+
cost_today: todayTotalSpend,
|
|
1594
|
+
is_bg: isBackgroundAgent
|
|
1595
|
+
});
|
|
1596
|
+
saveLatest(config.statusDirs, resolvedSessionId, fallbackStatus);
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
if (!clientRes.headersSent) {
|
|
1600
|
+
clientRes.writeHead(502, { "content-type": "text/plain" });
|
|
1601
|
+
}
|
|
1602
|
+
try {
|
|
1603
|
+
clientRes.end("Bad Gateway: Upstream Unreachable");
|
|
1604
|
+
} catch {
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
const handleProxyResponse = (proxyRes) => {
|
|
1608
|
+
isResponseCommitted = true;
|
|
1609
|
+
const responseChunks = [];
|
|
1610
|
+
let responseBytes = 0;
|
|
1611
|
+
let isResponseCapped = false;
|
|
1612
|
+
let timeToFirstToken = 0;
|
|
1613
|
+
const contentType = String(proxyRes.headers["content-type"] || "").toLowerCase();
|
|
1614
|
+
const isStreaming = contentType.includes("text/event-stream");
|
|
1615
|
+
clientRes.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
|
|
1616
|
+
proxyRes.on("data", (chunk) => {
|
|
1617
|
+
if (isStreaming && timeToFirstToken === 0) {
|
|
1618
|
+
const text = chunk.toString("utf8");
|
|
1619
|
+
if (text.includes("content_block_start") || text.includes("content_block_delta") || text.includes('"delta"')) {
|
|
1620
|
+
timeToFirstToken = Date.now();
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (!isResponseCapped) {
|
|
1624
|
+
if (responseBytes + chunk.length <= config.maxBodyBytes) {
|
|
1625
|
+
responseChunks.push(chunk);
|
|
1626
|
+
responseBytes += chunk.length;
|
|
1627
|
+
} else {
|
|
1628
|
+
isResponseCapped = true;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
clientRes.write(chunk);
|
|
1632
|
+
});
|
|
1633
|
+
proxyRes.on("aborted", () => {
|
|
1634
|
+
try {
|
|
1635
|
+
clientRes.destroy();
|
|
1636
|
+
} catch {
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
proxyRes.on("error", () => {
|
|
1640
|
+
try {
|
|
1641
|
+
if (!clientRes.writableEnded) clientRes.end();
|
|
1642
|
+
} catch {
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
proxyRes.on("end", async () => {
|
|
1646
|
+
const endTime = Date.now();
|
|
1647
|
+
try {
|
|
1648
|
+
if (!clientRes.writableEnded) clientRes.end();
|
|
1649
|
+
} catch {
|
|
1650
|
+
}
|
|
1651
|
+
if (!isModelExecution) return;
|
|
1652
|
+
const rawRequestBody = Buffer.concat(requestChunks);
|
|
1653
|
+
try {
|
|
1654
|
+
resolvedSessionId = sanitizeSessionId(
|
|
1655
|
+
extractSessionId(
|
|
1656
|
+
headers,
|
|
1657
|
+
rawRequestBody.toString("utf8")
|
|
1658
|
+
)
|
|
1659
|
+
);
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
const rawResponseBody = Buffer.concat(responseChunks);
|
|
1663
|
+
try {
|
|
1664
|
+
const turn = parseTurn({
|
|
1665
|
+
reqBody: rawRequestBody.toString("utf8"),
|
|
1666
|
+
resBody: rawResponseBody.toString("utf8"),
|
|
1667
|
+
contentType: String(proxyRes.headers["content-type"] || ""),
|
|
1668
|
+
statusCode: proxyRes.statusCode || 200
|
|
1669
|
+
});
|
|
1670
|
+
const rates = await resolveModelRates(turn.model);
|
|
1671
|
+
const currentContextTokens = turn.input > turn.cacheRead ? turn.input + turn.cacheCreate : turn.input + turn.cacheRead + turn.cacheCreate;
|
|
1672
|
+
const previousContextTokens = contextHistory.has(resolvedSessionId) ? contextHistory.get(resolvedSessionId) ?? currentContextTokens : currentContextTokens;
|
|
1673
|
+
const durationSeconds = (endTime - startTime) / 1e3;
|
|
1674
|
+
const streamGenerationMs = timeToFirstToken && isStreaming ? endTime - timeToFirstToken : 0;
|
|
1675
|
+
let tokensPerSecond = 0;
|
|
1676
|
+
if (turn.output > 0) {
|
|
1677
|
+
if (isStreaming && streamGenerationMs >= 20) {
|
|
1678
|
+
tokensPerSecond = Math.round(turn.output / streamGenerationMs * 1e3);
|
|
1679
|
+
} else if (endTime > startTime) {
|
|
1680
|
+
const totalTurnMs = Math.max(20, endTime - startTime);
|
|
1681
|
+
tokensPerSecond = Math.round(turn.output / totalTurnMs * 1e3);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
const ttftMs = timeToFirstToken && isStreaming ? timeToFirstToken - startTime : endTime - startTime;
|
|
1685
|
+
const uncachedInput = turn.input > turn.cacheRead ? Math.max(0, turn.input - turn.cacheRead) : turn.input;
|
|
1686
|
+
const turnCostUsd = costFor(rates, {
|
|
1687
|
+
input: uncachedInput,
|
|
1688
|
+
output: turn.output,
|
|
1689
|
+
cacheRead: turn.cacheRead,
|
|
1690
|
+
cacheCreate: turn.cacheCreate
|
|
1691
|
+
});
|
|
1692
|
+
const priorTurns = turnIndexBySession.get(resolvedSessionId) || 0;
|
|
1693
|
+
const currentTurnIndex = priorTurns + 1;
|
|
1694
|
+
setBoundedLRU(turnIndexBySession, resolvedSessionId, currentTurnIndex);
|
|
1695
|
+
const accumulatedSessionCost = (sessionSpending[resolvedSessionId] || 0) + turnCostUsd;
|
|
1696
|
+
sessionSpending[resolvedSessionId] = accumulatedSessionCost;
|
|
1697
|
+
saveTotals(config.statusDirs, sessionSpending, config.maxSessions);
|
|
1698
|
+
const nowLocalDate = getLocalDateString();
|
|
1699
|
+
if (nowLocalDate !== activeLocalDate) {
|
|
1700
|
+
activeLocalDate = nowLocalDate;
|
|
1701
|
+
todayTotalSpend = 0;
|
|
1702
|
+
}
|
|
1703
|
+
todayTotalSpend += turnCostUsd;
|
|
1704
|
+
const isMicroLabel = (turn.kind === "label" || turn.tools.length === 0 && turn.output > 0 && turn.output < config.labelMaxOut) && currentContextTokens < config.labelMaxIn;
|
|
1705
|
+
if (!isMicroLabel) {
|
|
1706
|
+
setBoundedLRU(contextHistory, resolvedSessionId, currentContextTokens);
|
|
1707
|
+
}
|
|
1708
|
+
const displayCtxTokens = isMicroLabel && contextHistory.has(resolvedSessionId) ? contextHistory.get(resolvedSessionId) ?? currentContextTokens : currentContextTokens;
|
|
1709
|
+
const statusSnapshot = buildLatest({
|
|
1710
|
+
session_id: resolvedSessionId,
|
|
1711
|
+
turn_index: currentTurnIndex,
|
|
1712
|
+
model: turn.model,
|
|
1713
|
+
is_bg: isBackgroundAgent,
|
|
1714
|
+
ctx: displayCtxTokens,
|
|
1715
|
+
window: rates.window,
|
|
1716
|
+
pct: Number((displayCtxTokens / rates.window * 100).toFixed(1)),
|
|
1717
|
+
cache_read: turn.cacheRead,
|
|
1718
|
+
cache_create: turn.cacheCreate,
|
|
1719
|
+
delta_in: isMicroLabel ? 0 : currentContextTokens - previousContextTokens,
|
|
1720
|
+
in_tokens: uncachedInput,
|
|
1721
|
+
out_tokens: turn.output,
|
|
1722
|
+
tool: turn.tools[0]?.name ?? null,
|
|
1723
|
+
tool_tk: turn.tools[0]?.argTk ?? 0,
|
|
1724
|
+
tools_summary: turn.toolsSummary,
|
|
1725
|
+
thinking: turn.thinking,
|
|
1726
|
+
thinking_tokens: turn.thinkingTokens,
|
|
1727
|
+
stop_reason: turn.stopReason,
|
|
1728
|
+
is_label: isMicroLabel,
|
|
1729
|
+
label: isMicroLabel ? turn.label : "",
|
|
1730
|
+
ttft_ms: ttftMs,
|
|
1731
|
+
tps: tokensPerSecond,
|
|
1732
|
+
dur_s: Number(durationSeconds.toFixed(1)),
|
|
1733
|
+
cost_turn: turnCostUsd,
|
|
1734
|
+
cost_session: accumulatedSessionCost,
|
|
1735
|
+
cost_today: todayTotalSpend,
|
|
1736
|
+
error: null,
|
|
1737
|
+
status: proxyRes.statusCode || 200
|
|
1738
|
+
});
|
|
1739
|
+
saveLatest(config.statusDirs, resolvedSessionId, statusSnapshot);
|
|
1740
|
+
if (!isMicroLabel && (turn.input > 0 || turn.output > 0)) {
|
|
1741
|
+
appendHistory(config.statusDirs, {
|
|
1742
|
+
date: nowLocalDate,
|
|
1743
|
+
sessionId: resolvedSessionId,
|
|
1744
|
+
turn: currentTurnIndex,
|
|
1745
|
+
model: turn.model,
|
|
1746
|
+
tokens: currentContextTokens + turn.output,
|
|
1747
|
+
cost: turnCostUsd,
|
|
1748
|
+
cacheRead: turn.cacheRead,
|
|
1749
|
+
thinkingTokens: turn.thinkingTokens
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1754
|
+
console.error(`[tokensniff] turn parse/record failure: ${msg}`);
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
};
|
|
1758
|
+
const tryForwardToHost = (hostIndex) => {
|
|
1759
|
+
if (hostIndex >= upstreamHosts.length) {
|
|
1760
|
+
dispatch502("all loopback candidates exhausted");
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
const hostCandidate = upstreamHosts[hostIndex] ?? "127.0.0.1";
|
|
1764
|
+
const forwardHeaders = { ...headers };
|
|
1765
|
+
forwardHeaders.host = `${hostCandidate}:${config.upstreamPort}`;
|
|
1766
|
+
const proxyRequestOptions = {
|
|
1767
|
+
hostname: hostCandidate,
|
|
1768
|
+
port: config.upstreamPort,
|
|
1769
|
+
path: clientReq.url,
|
|
1770
|
+
method: clientReq.method,
|
|
1771
|
+
headers: forwardHeaders,
|
|
1772
|
+
timeout: config.upstreamTimeoutMs
|
|
1773
|
+
};
|
|
1774
|
+
proxyClientReq = http.request(proxyRequestOptions, (proxyRes) => {
|
|
1775
|
+
handleProxyResponse(proxyRes);
|
|
1776
|
+
});
|
|
1777
|
+
proxyClientReq.on("timeout", () => {
|
|
1778
|
+
try {
|
|
1779
|
+
proxyClientReq?.destroy();
|
|
1780
|
+
} catch {
|
|
1781
|
+
}
|
|
1782
|
+
if (!isResponseCommitted) {
|
|
1783
|
+
tryForwardToHost(hostIndex + 1);
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
proxyClientReq.on("error", () => {
|
|
1787
|
+
if (!isResponseCommitted) {
|
|
1788
|
+
tryForwardToHost(hostIndex + 1);
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
for (const chunk of requestChunks) {
|
|
1792
|
+
proxyClientReq.write(chunk);
|
|
1793
|
+
}
|
|
1794
|
+
if (isRequestComplete) {
|
|
1795
|
+
proxyClientReq.end();
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
clientReq.on("data", (chunk) => {
|
|
1799
|
+
receivedBytes += chunk.length;
|
|
1800
|
+
if (receivedBytes > config.maxBodyBytes) {
|
|
1801
|
+
if (!isPayloadExceeded) {
|
|
1802
|
+
isPayloadExceeded = true;
|
|
1803
|
+
clientRes.writeHead(413, { "content-type": "text/plain" });
|
|
1804
|
+
clientRes.end("Payload Too Large");
|
|
1805
|
+
}
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
requestChunks.push(chunk);
|
|
1809
|
+
if (proxyClientReq) {
|
|
1810
|
+
proxyClientReq.write(chunk);
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
clientReq.on("end", () => {
|
|
1814
|
+
isRequestComplete = true;
|
|
1815
|
+
if (proxyClientReq) {
|
|
1816
|
+
proxyClientReq.end();
|
|
1817
|
+
} else if (!isPayloadExceeded) {
|
|
1818
|
+
tryForwardToHost(0);
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
clientRes.on("close", () => {
|
|
1822
|
+
if (!clientRes.writableEnded && proxyClientReq) {
|
|
1823
|
+
try {
|
|
1824
|
+
proxyClientReq.destroy();
|
|
1825
|
+
} catch {
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
1830
|
+
server.listen(config.listenPort, config.listenHost, () => {
|
|
1831
|
+
console.log(
|
|
1832
|
+
`[tokensniff] proxy listening on http://${config.listenHost}:${config.listenPort} -> ${config.upstreamHost}:${config.upstreamPort}`
|
|
1833
|
+
);
|
|
1834
|
+
});
|
|
1835
|
+
return server;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/cli/run.ts
|
|
1839
|
+
function waitForTcp(host, port, timeoutMs = 3e4) {
|
|
1840
|
+
const hosts = host === "localhost" || host === "127.0.0.1" || host === "::1" ? ["127.0.0.1", "::1"] : [host];
|
|
1841
|
+
const startTime = Date.now();
|
|
1842
|
+
return new Promise((resolve, reject) => {
|
|
1843
|
+
let hostIndex = 0;
|
|
1844
|
+
const probe = () => {
|
|
1845
|
+
const candidateHost = hosts[hostIndex % hosts.length] ?? "127.0.0.1";
|
|
1846
|
+
hostIndex++;
|
|
1847
|
+
const socket = net.connect(port, candidateHost, () => {
|
|
1848
|
+
socket.destroy();
|
|
1849
|
+
resolve();
|
|
1850
|
+
});
|
|
1851
|
+
socket.on("error", () => {
|
|
1852
|
+
socket.destroy();
|
|
1853
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
1854
|
+
reject(
|
|
1855
|
+
new Error(`Timed out waiting for upstream network availability at ${host}:${port}`)
|
|
1856
|
+
);
|
|
1857
|
+
} else {
|
|
1858
|
+
setTimeout(probe, 250);
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
};
|
|
1862
|
+
probe();
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
function isPortActive(host, port) {
|
|
1866
|
+
return new Promise((resolve) => {
|
|
1867
|
+
const socket = net.connect(port, host, () => {
|
|
1868
|
+
socket.destroy();
|
|
1869
|
+
resolve(true);
|
|
1870
|
+
});
|
|
1871
|
+
socket.on("error", () => {
|
|
1872
|
+
socket.destroy();
|
|
1873
|
+
resolve(false);
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
function writeInitFile() {
|
|
1878
|
+
const destination = getGlobalConfigPath();
|
|
1879
|
+
if (fs2.existsSync(destination)) {
|
|
1880
|
+
console.log(`[tokensniff] configuration already present: ${destination}`);
|
|
1881
|
+
} else {
|
|
1882
|
+
const template = {
|
|
1883
|
+
upstreamCommand: "npx antigravity-claude-proxy@latest start",
|
|
1884
|
+
upstreamHost: "localhost",
|
|
1885
|
+
upstreamPort: 8085,
|
|
1886
|
+
listenPort: 4e3,
|
|
1887
|
+
harnessCommand: "claude"
|
|
1888
|
+
};
|
|
1889
|
+
fs2.mkdirSync(path2.dirname(destination), { recursive: true });
|
|
1890
|
+
fs2.writeFileSync(destination, JSON.stringify(template, null, 2), "utf8");
|
|
1891
|
+
console.log(`[tokensniff] generated global config: ${destination}`);
|
|
1892
|
+
}
|
|
1893
|
+
printClaudeInstructions();
|
|
1894
|
+
return destination;
|
|
1895
|
+
}
|
|
1896
|
+
function printClaudeInstructions() {
|
|
1897
|
+
console.log("\n------------------------------------------------------------");
|
|
1898
|
+
console.log("To route Claude Code through tokensniff and enable the HUD,");
|
|
1899
|
+
console.log("add this to your ~/.claude/settings.json file:\n");
|
|
1900
|
+
console.log(' "env": {');
|
|
1901
|
+
console.log(' "ANTHROPIC_BASE_URL": "http://localhost:4000"');
|
|
1902
|
+
console.log(" },");
|
|
1903
|
+
console.log(' "statusLine": {');
|
|
1904
|
+
console.log(' "type": "command",');
|
|
1905
|
+
console.log(' "command": "tokensniff-status",');
|
|
1906
|
+
console.log(' "padding": 0');
|
|
1907
|
+
console.log(" }");
|
|
1908
|
+
console.log("------------------------------------------------------------\n");
|
|
1909
|
+
}
|
|
1910
|
+
function terminateProcessTree(pid) {
|
|
1911
|
+
if (!pid) return;
|
|
1912
|
+
if (process.platform === "win32") {
|
|
1913
|
+
try {
|
|
1914
|
+
execSync(`taskkill /pid ${pid} /T /F`, { stdio: "ignore" });
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
} else {
|
|
1918
|
+
try {
|
|
1919
|
+
process.kill(-pid, "SIGKILL");
|
|
1920
|
+
} catch {
|
|
1921
|
+
try {
|
|
1922
|
+
process.kill(pid, "SIGKILL");
|
|
1923
|
+
} catch {
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function killProcessOnPort(port) {
|
|
1929
|
+
if (!port || !Number.isFinite(port) || port <= 0) return;
|
|
1930
|
+
if (process.platform === "win32") {
|
|
1931
|
+
try {
|
|
1932
|
+
const output = execSync("netstat -ano -p tcp", { encoding: "utf8" });
|
|
1933
|
+
const lines = output.split(/\r?\n/);
|
|
1934
|
+
const portPattern = new RegExp(`:${port}\\s+.*LISTENING\\s+(\\d+)`, "i");
|
|
1935
|
+
const pidsToKill = /* @__PURE__ */ new Set();
|
|
1936
|
+
for (const line of lines) {
|
|
1937
|
+
const match = line.match(portPattern);
|
|
1938
|
+
if (match?.[1]) {
|
|
1939
|
+
const pid = match[1].trim();
|
|
1940
|
+
if (pid && pid !== "0" && pid !== "4" && pid !== String(process.pid)) {
|
|
1941
|
+
pidsToKill.add(pid);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
for (const pid of pidsToKill) {
|
|
1946
|
+
try {
|
|
1947
|
+
execSync(`taskkill /pid ${pid} /T /F`, { stdio: "ignore" });
|
|
1948
|
+
} catch {
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
} catch {
|
|
1952
|
+
}
|
|
1953
|
+
} else {
|
|
1954
|
+
try {
|
|
1955
|
+
execSync(`lsof -ti tcp:${port} | xargs kill -9 2>/dev/null || true`, {
|
|
1956
|
+
stdio: "ignore"
|
|
1957
|
+
});
|
|
1958
|
+
} catch {
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function isProcessAlive(pid) {
|
|
1963
|
+
if (!pid || !Number.isFinite(pid) || pid <= 0) return false;
|
|
1964
|
+
try {
|
|
1965
|
+
process.kill(pid, 0);
|
|
1966
|
+
return true;
|
|
1967
|
+
} catch (error) {
|
|
1968
|
+
const err = error;
|
|
1969
|
+
return err.code === "EPERM";
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
function getSessionsDir() {
|
|
1973
|
+
return path2.join(getGlobalDir(), "sessions");
|
|
1974
|
+
}
|
|
1975
|
+
function registerSession(pid = process.pid) {
|
|
1976
|
+
const sessionsDir = getSessionsDir();
|
|
1977
|
+
try {
|
|
1978
|
+
fs2.mkdirSync(sessionsDir, { recursive: true });
|
|
1979
|
+
const sessionFile = path2.join(sessionsDir, `${pid}.json`);
|
|
1980
|
+
fs2.writeFileSync(sessionFile, JSON.stringify({ pid, startedAt: Date.now() }), "utf8");
|
|
1981
|
+
} catch {
|
|
1982
|
+
}
|
|
1983
|
+
let unregistered = false;
|
|
1984
|
+
return () => {
|
|
1985
|
+
if (unregistered) return;
|
|
1986
|
+
unregistered = true;
|
|
1987
|
+
try {
|
|
1988
|
+
const sessionFile = path2.join(sessionsDir, `${pid}.json`);
|
|
1989
|
+
if (fs2.existsSync(sessionFile)) {
|
|
1990
|
+
fs2.unlinkSync(sessionFile);
|
|
1991
|
+
}
|
|
1992
|
+
} catch {
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
function countActiveSessions(excludePid = process.pid) {
|
|
1997
|
+
const sessionsDir = getSessionsDir();
|
|
1998
|
+
if (!fs2.existsSync(sessionsDir)) return 0;
|
|
1999
|
+
let activeCount = 0;
|
|
2000
|
+
try {
|
|
2001
|
+
const files = fs2.readdirSync(sessionsDir);
|
|
2002
|
+
for (const file of files) {
|
|
2003
|
+
if (!file.endsWith(".json")) continue;
|
|
2004
|
+
const pidStr = file.replace(".json", "");
|
|
2005
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
2006
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2007
|
+
try {
|
|
2008
|
+
fs2.unlinkSync(path2.join(sessionsDir, file));
|
|
2009
|
+
} catch {
|
|
2010
|
+
}
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
if (pid === excludePid) {
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
if (isProcessAlive(pid)) {
|
|
2017
|
+
activeCount++;
|
|
2018
|
+
} else {
|
|
2019
|
+
try {
|
|
2020
|
+
fs2.unlinkSync(path2.join(sessionsDir, file));
|
|
2021
|
+
} catch {
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
} catch {
|
|
2026
|
+
}
|
|
2027
|
+
return activeCount;
|
|
2028
|
+
}
|
|
2029
|
+
async function main(argv = process.argv.slice(2), deps = {}) {
|
|
2030
|
+
const command = argv[0];
|
|
2031
|
+
const config = deps.config || loadConfig();
|
|
2032
|
+
if (command === "init") {
|
|
2033
|
+
writeInitFile();
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
if (command === "dashboard") {
|
|
2037
|
+
const proxyHost2 = config.listenHost === "localhost" ? "127.0.0.1" : config.listenHost;
|
|
2038
|
+
const dashboardUrl = `http://${proxyHost2}:${config.listenPort}/dashboard`;
|
|
2039
|
+
console.log(`[tokensniff] live heatmap dashboard: ${dashboardUrl}`);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (command === "import") {
|
|
2043
|
+
const targetDir = argv[1];
|
|
2044
|
+
if (!targetDir) {
|
|
2045
|
+
console.error(
|
|
2046
|
+
"[tokensniff] Error: directory path required.\nUsage: tokensniff import <path-to-captures-dir>"
|
|
2047
|
+
);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
const resolvedDir = path2.resolve(process.cwd(), targetDir);
|
|
2051
|
+
console.log(`[tokensniff] importing probe captures from: ${resolvedDir}`);
|
|
2052
|
+
const result = importCapturesDirectory(resolvedDir, config.statusDirs);
|
|
2053
|
+
console.log(
|
|
2054
|
+
`[tokensniff] import complete: ${result.imported} turns recorded, ${result.skipped} non-token files skipped.`
|
|
2055
|
+
);
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (command === "--help" || command === "-h") {
|
|
2059
|
+
console.log("Usage: tokensniff [init|dashboard|import] [-- <harness...>]");
|
|
2060
|
+
console.log(" tokensniff Run proxy + live dashboard + Claude Code");
|
|
2061
|
+
console.log(" tokensniff init Generate global config at ~/.tokensniff/config.json");
|
|
2062
|
+
console.log(" tokensniff dashboard Show live calendar spend heatmap URL");
|
|
2063
|
+
console.log(" tokensniff import [dir] Import raw probe captures into daily heatmap ledger");
|
|
2064
|
+
console.log(
|
|
2065
|
+
" tokensniff -- <cmd> Run with an alternate harness command (e.g. codex)\n"
|
|
2066
|
+
);
|
|
2067
|
+
printClaudeInstructions();
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
const unregisterSession = registerSession(process.pid);
|
|
2071
|
+
const startProxy = deps.startProxy || start;
|
|
2072
|
+
const separatorIndex = argv.indexOf("--");
|
|
2073
|
+
const customHarnessArgs = separatorIndex >= 0 ? argv.slice(separatorIndex + 1) : argv.length > 0 ? argv : String(config.harnessCommand || "claude").split(" ");
|
|
2074
|
+
const [harnessBinary, ...harnessArgs] = customHarnessArgs.filter(Boolean);
|
|
2075
|
+
if (!harnessBinary) {
|
|
2076
|
+
unregisterSession();
|
|
2077
|
+
throw new Error("No executable harness binary resolved. Verify config.harnessCommand.");
|
|
2078
|
+
}
|
|
2079
|
+
const childProcesses = [];
|
|
2080
|
+
let isCleaningUp = false;
|
|
2081
|
+
let didSpawnUpstream = false;
|
|
2082
|
+
let proxyServerInstance = null;
|
|
2083
|
+
const cleanup = (exitCode2) => {
|
|
2084
|
+
if (isCleaningUp) return;
|
|
2085
|
+
isCleaningUp = true;
|
|
2086
|
+
for (const proc of childProcesses) {
|
|
2087
|
+
terminateProcessTree(proc.pid);
|
|
2088
|
+
}
|
|
2089
|
+
unregisterSession();
|
|
2090
|
+
const remainingSessions = countActiveSessions(process.pid);
|
|
2091
|
+
if (remainingSessions > 0) {
|
|
2092
|
+
console.log(
|
|
2093
|
+
`[tokensniff] session detached (${remainingSessions} active session(s) remaining, proxy maintained).`
|
|
2094
|
+
);
|
|
2095
|
+
} else {
|
|
2096
|
+
if (proxyServerInstance) {
|
|
2097
|
+
try {
|
|
2098
|
+
proxyServerInstance.close();
|
|
2099
|
+
} catch {
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
if (didSpawnUpstream && config.upstreamCommand) {
|
|
2103
|
+
if (config.upstreamStopCommand) {
|
|
2104
|
+
try {
|
|
2105
|
+
execSync(config.upstreamStopCommand, {
|
|
2106
|
+
stdio: "ignore",
|
|
2107
|
+
env: {
|
|
2108
|
+
...process.env,
|
|
2109
|
+
PORT: process.env.PORT || String(config.upstreamPort)
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
} catch {
|
|
2113
|
+
}
|
|
2114
|
+
} else if (config.upstreamCommand.includes("antigravity-claude-proxy")) {
|
|
2115
|
+
try {
|
|
2116
|
+
execSync("npx antigravity-claude-proxy@latest stop", {
|
|
2117
|
+
stdio: "ignore",
|
|
2118
|
+
env: {
|
|
2119
|
+
...process.env,
|
|
2120
|
+
PORT: process.env.PORT || String(config.upstreamPort)
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
} catch {
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
if (config.upstreamPort) {
|
|
2127
|
+
killProcessOnPort(config.upstreamPort);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
process.exit(exitCode2);
|
|
2132
|
+
};
|
|
2133
|
+
process.once("SIGINT", () => {
|
|
2134
|
+
cleanup(130);
|
|
2135
|
+
});
|
|
2136
|
+
process.once("SIGTERM", () => {
|
|
2137
|
+
cleanup(143);
|
|
2138
|
+
});
|
|
2139
|
+
const proxyHost = config.listenHost === "localhost" ? "127.0.0.1" : config.listenHost;
|
|
2140
|
+
const baseUrl = `http://${proxyHost}:${config.listenPort}`;
|
|
2141
|
+
const isAlreadyRunning = await isPortActive(proxyHost, config.listenPort);
|
|
2142
|
+
if (isAlreadyRunning) {
|
|
2143
|
+
console.log(`[tokensniff] attaching to active tokensniff proxy on port ${config.listenPort}`);
|
|
2144
|
+
console.log(`[tokensniff] live heatmap dashboard available at: ${baseUrl}/dashboard`);
|
|
2145
|
+
} else {
|
|
2146
|
+
const isUpstreamAlreadyActive = await isPortActive(config.upstreamHost, config.upstreamPort);
|
|
2147
|
+
if (isUpstreamAlreadyActive) {
|
|
2148
|
+
console.log(
|
|
2149
|
+
`[tokensniff] upstream proxy already active on ${config.upstreamHost}:${config.upstreamPort}`
|
|
2150
|
+
);
|
|
2151
|
+
} else if (config.upstreamCommand) {
|
|
2152
|
+
console.log(`[tokensniff] initiating upstream command: ${config.upstreamCommand}`);
|
|
2153
|
+
const upstreamProcess = spawn(config.upstreamCommand, {
|
|
2154
|
+
shell: true,
|
|
2155
|
+
stdio: "inherit",
|
|
2156
|
+
detached: process.platform !== "win32",
|
|
2157
|
+
env: {
|
|
2158
|
+
...process.env,
|
|
2159
|
+
PORT: process.env.PORT || String(config.upstreamPort)
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
childProcesses.push(upstreamProcess);
|
|
2163
|
+
didSpawnUpstream = true;
|
|
2164
|
+
upstreamProcess.on("exit", (code) => {
|
|
2165
|
+
if (code !== null && code !== 0 && !isCleaningUp) {
|
|
2166
|
+
console.log(`[tokensniff] upstream daemon exited with code ${code}`);
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
try {
|
|
2170
|
+
await waitForTcp(config.upstreamHost, config.upstreamPort, deps.waitMs || 3e4);
|
|
2171
|
+
console.log(
|
|
2172
|
+
`[tokensniff] upstream proxy responsive on ${config.upstreamHost}:${config.upstreamPort}`
|
|
2173
|
+
);
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2176
|
+
console.error(`[tokensniff] upstream proxy failed health check: ${msg}`);
|
|
2177
|
+
console.error(
|
|
2178
|
+
"[tokensniff] continuing pipeline; downstream calls will receive 502 until ready."
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
proxyServerInstance = startProxy(config);
|
|
2183
|
+
if (!proxyServerInstance.listening) {
|
|
2184
|
+
await new Promise((resolve, reject) => {
|
|
2185
|
+
proxyServerInstance?.once("listening", () => resolve());
|
|
2186
|
+
proxyServerInstance?.once("error", (err) => reject(err));
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
console.log(`[tokensniff] live heatmap dashboard available at: ${baseUrl}/dashboard`);
|
|
2190
|
+
}
|
|
2191
|
+
console.log(`[tokensniff] launching harness: ${harnessBinary} ${harnessArgs.join(" ")}`.trim());
|
|
2192
|
+
const harnessProcess = spawn(harnessBinary, harnessArgs, {
|
|
2193
|
+
shell: true,
|
|
2194
|
+
stdio: "inherit",
|
|
2195
|
+
env: process.env
|
|
2196
|
+
});
|
|
2197
|
+
childProcesses.push(harnessProcess);
|
|
2198
|
+
const exitCode = await new Promise(
|
|
2199
|
+
(resolve) => harnessProcess.on("close", (code) => resolve(code))
|
|
2200
|
+
);
|
|
2201
|
+
cleanup(exitCode ?? 0);
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
export { countActiveSessions, getSessionsDir, isPortActive, isProcessAlive, killProcessOnPort, main, printClaudeInstructions, registerSession, terminateProcessTree, waitForTcp, writeInitFile };
|
|
2205
|
+
//# sourceMappingURL=cli.js.map
|
|
2206
|
+
//# sourceMappingURL=cli.js.map
|