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/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