tokelytics 0.1.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +9 -3
  2. package/bin/tokelytics.mjs +1262 -368
  3. package/package.json +17 -5
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/config.ts
4
- import { promises as fs3 } from "node:fs";
5
- import * as path5 from "node:path";
4
+ import { promises as fs4 } from "node:fs";
5
+ import * as path6 from "node:path";
6
6
 
7
7
  // src/paths.ts
8
- import { promises as fs2 } from "node:fs";
8
+ import { promises as fs3 } from "node:fs";
9
9
  import * as os3 from "node:os";
10
- import * as path4 from "node:path";
10
+ import * as path5 from "node:path";
11
11
  import { randomUUID } from "node:crypto";
12
12
 
13
13
  // ../packages/core/dist/types.js
@@ -63,15 +63,80 @@ function hourOf(ts) {
63
63
  function rollupId(provider, day) {
64
64
  return `${provider}_${day}`;
65
65
  }
66
- function aggregateSession(sessionId, turns) {
67
- const agg = {
68
- sessionId,
69
- provider: turns[0]?.provider ?? "claude",
70
- project: "unknown",
71
- gitBranch: "",
66
+ var TIMELINE_BUCKET_MS = 5 * 60 * 1e3;
67
+ var TIMELINE_RETENTION_MS = 8 * 24 * 60 * 60 * 1e3;
68
+
69
+ // ../packages/core/dist/snapshot.js
70
+ var SNAPSHOT_RECENT_TURNS = 32;
71
+ var SNAPSHOT_SESSIONS = 100;
72
+ var SNAPSHOT_RETENTION_DAYS = 120;
73
+ function emptyDashboardSnapshot(now = /* @__PURE__ */ new Date()) {
74
+ return {
75
+ version: 2,
76
+ updatedAt: now.toISOString(),
77
+ recentTurns: [],
78
+ sessions: [],
79
+ rollups: [],
80
+ limits: []
81
+ };
82
+ }
83
+ function mergeDashboardSnapshot(previous, turns, limits, device, now = /* @__PURE__ */ new Date()) {
84
+ const next = structuredClone(previous ?? emptyDashboardSnapshot(now));
85
+ next.version = 2;
86
+ if (turns.length > 0) {
87
+ next.recentTurns = mergeRecentTurns(next.recentTurns, turns);
88
+ next.sessions = mergeSessions(next.sessions, turns);
89
+ next.rollups = mergeRollups(next.rollups, turns, now);
90
+ }
91
+ if (limits.length > 0)
92
+ next.limits = mergeLimits(next.limits, limits);
93
+ if (device)
94
+ next.device = device;
95
+ if (turns.length > 0 || limits.length > 0 || device)
96
+ next.updatedAt = now.toISOString();
97
+ return next;
98
+ }
99
+ function mergeRecentTurns(existing, incoming) {
100
+ const byId = new Map(existing.map((turn) => [turn.turnId, turn]));
101
+ for (const turn of incoming)
102
+ byId.set(turn.turnId, turn);
103
+ return [...byId.values()].sort((a, b) => a.ts.localeCompare(b.ts)).slice(-SNAPSHOT_RECENT_TURNS);
104
+ }
105
+ function mergeSessions(existing, turns) {
106
+ const byId = new Map(existing.map((session) => [session.sessionId, { ...session }]));
107
+ for (const turn of turns) {
108
+ const session = byId.get(turn.sessionId) ?? emptySession(turn);
109
+ session.turnCount++;
110
+ session.inputTokens += turn.inputTokens;
111
+ session.outputTokens += turn.outputTokens;
112
+ session.cacheReadTokens += turn.cacheReadTokens;
113
+ session.cacheCreationTokens += turn.cacheCreationTokens;
114
+ session.cachedInputTokens += turn.cachedInputTokens;
115
+ session.reasoningTokens += turn.reasoningTokens;
116
+ if (!session.firstTs || turn.ts < session.firstTs)
117
+ session.firstTs = turn.ts;
118
+ if (!session.lastTs || turn.ts > session.lastTs)
119
+ session.lastTs = turn.ts;
120
+ if (turn.project && session.project === "unknown")
121
+ session.project = turn.project;
122
+ if (turn.gitBranch && !session.gitBranch)
123
+ session.gitBranch = turn.gitBranch;
124
+ if (turn.model && (!session.model || modelPriority(turn.model) > modelPriority(session.model))) {
125
+ session.model = turn.model;
126
+ }
127
+ byId.set(turn.sessionId, session);
128
+ }
129
+ return [...byId.values()].sort((a, b) => b.lastTs.localeCompare(a.lastTs)).slice(0, SNAPSHOT_SESSIONS);
130
+ }
131
+ function emptySession(turn) {
132
+ return {
133
+ sessionId: turn.sessionId,
134
+ provider: turn.provider,
135
+ project: turn.project || "unknown",
136
+ gitBranch: turn.gitBranch || "",
72
137
  firstTs: "",
73
138
  lastTs: "",
74
- model: "",
139
+ model: turn.model || "",
75
140
  turnCount: 0,
76
141
  inputTokens: 0,
77
142
  outputTokens: 0,
@@ -80,88 +145,59 @@ function aggregateSession(sessionId, turns) {
80
145
  cachedInputTokens: 0,
81
146
  reasoningTokens: 0
82
147
  };
83
- for (const t of turns) {
84
- agg.turnCount++;
85
- agg.inputTokens += t.inputTokens;
86
- agg.outputTokens += t.outputTokens;
87
- agg.cacheReadTokens += t.cacheReadTokens;
88
- agg.cacheCreationTokens += t.cacheCreationTokens;
89
- agg.cachedInputTokens += t.cachedInputTokens;
90
- agg.reasoningTokens += t.reasoningTokens;
91
- if (t.ts && (!agg.firstTs || t.ts < agg.firstTs))
92
- agg.firstTs = t.ts;
93
- if (t.ts && (!agg.lastTs || t.ts > agg.lastTs))
94
- agg.lastTs = t.ts;
95
- if (t.project && agg.project === "unknown")
96
- agg.project = t.project;
97
- if (t.gitBranch && !agg.gitBranch)
98
- agg.gitBranch = t.gitBranch;
99
- if (t.model && modelPriority(t.model) > modelPriority(agg.model))
100
- agg.model = t.model;
101
- }
102
- if (!agg.model) {
103
- const withModel = turns.find((t) => t.model);
104
- if (withModel)
105
- agg.model = withModel.model;
106
- }
107
- return agg;
108
- }
109
- function emptyModelTotals(model) {
110
- return {
111
- model,
112
- turns: 0,
113
- inputTokens: 0,
114
- outputTokens: 0,
115
- cacheReadTokens: 0,
116
- cacheCreationTokens: 0,
117
- cachedInputTokens: 0,
118
- reasoningTokens: 0
119
- };
120
148
  }
121
- function buildRollup(provider, day, turns) {
122
- const models = {};
123
- const hours = /* @__PURE__ */ new Map();
124
- for (const t of turns) {
125
- const key = t.model || "unknown";
126
- const m = models[key] ??= emptyModelTotals(key);
127
- m.turns++;
128
- m.inputTokens += t.inputTokens;
129
- m.outputTokens += t.outputTokens;
130
- m.cacheReadTokens += t.cacheReadTokens;
131
- m.cacheCreationTokens += t.cacheCreationTokens;
132
- m.cachedInputTokens += t.cachedInputTokens;
133
- m.reasoningTokens += t.reasoningTokens;
134
- const h = hourOf(t.ts);
135
- if (h !== null) {
136
- const bucket = hours.get(h) ?? { hour: h, output: 0, turns: 0 };
137
- bucket.output += t.outputTokens;
149
+ function mergeRollups(existing, turns, now) {
150
+ const byId = new Map(existing.map((rollup) => [rollup.id, structuredClone(rollup)]));
151
+ for (const turn of turns) {
152
+ const day = dayOf(turn.ts);
153
+ if (!day)
154
+ continue;
155
+ const id = rollupId(turn.provider, day);
156
+ const rollup = byId.get(id) ?? { id, provider: turn.provider, day, models: {}, hourly: [] };
157
+ const model = turn.model || "unknown";
158
+ const totals = rollup.models[model] ??= {
159
+ model,
160
+ turns: 0,
161
+ inputTokens: 0,
162
+ outputTokens: 0,
163
+ cacheReadTokens: 0,
164
+ cacheCreationTokens: 0,
165
+ cachedInputTokens: 0,
166
+ reasoningTokens: 0
167
+ };
168
+ totals.turns++;
169
+ totals.inputTokens += turn.inputTokens;
170
+ totals.outputTokens += turn.outputTokens;
171
+ totals.cacheReadTokens += turn.cacheReadTokens;
172
+ totals.cacheCreationTokens += turn.cacheCreationTokens;
173
+ totals.cachedInputTokens += turn.cachedInputTokens;
174
+ totals.reasoningTokens += turn.reasoningTokens;
175
+ const hour = hourOf(turn.ts);
176
+ if (hour !== null) {
177
+ const bucket = rollup.hourly.find((item) => item.hour === hour) ?? { hour, output: 0, turns: 0 };
178
+ bucket.output += turn.outputTokens;
138
179
  bucket.turns++;
139
- hours.set(h, bucket);
180
+ if (!rollup.hourly.some((item) => item.hour === hour))
181
+ rollup.hourly.push(bucket);
182
+ rollup.hourly.sort((a, b) => a.hour - b.hour);
140
183
  }
184
+ byId.set(id, rollup);
141
185
  }
142
- return {
143
- id: rollupId(provider, day),
144
- provider,
145
- day,
146
- models,
147
- hourly: [...hours.values()].sort((a, b) => a.hour - b.hour)
148
- };
186
+ const cutoff = new Date(now);
187
+ cutoff.setUTCDate(cutoff.getUTCDate() - (SNAPSHOT_RETENTION_DAYS - 1));
188
+ const cutoffDay = cutoff.toISOString().slice(0, 10);
189
+ return [...byId.values()].filter((rollup) => rollup.day >= cutoffDay).sort((a, b) => a.day.localeCompare(b.day) || a.provider.localeCompare(b.provider));
149
190
  }
150
- function affectedKeys(turns) {
151
- const sessionIds = /* @__PURE__ */ new Set();
152
- const dayBuckets = /* @__PURE__ */ new Map();
153
- for (const t of turns) {
154
- sessionIds.add(t.sessionId);
155
- const day = dayOf(t.ts);
156
- if (day)
157
- dayBuckets.set(rollupId(t.provider, day), { provider: t.provider, day });
158
- }
159
- return { sessionIds: [...sessionIds], dayBuckets: [...dayBuckets.values()] };
191
+ function mergeLimits(existing, incoming) {
192
+ const byProvider = new Map(existing.map((limit) => [limit.provider, limit]));
193
+ for (const limit of incoming)
194
+ byProvider.set(limit.provider, limit);
195
+ return [...byProvider.values()];
160
196
  }
161
197
 
162
198
  // ../packages/core/dist/providers/claude.js
163
199
  import * as os from "node:os";
164
- import * as path2 from "node:path";
200
+ import * as path3 from "node:path";
165
201
 
166
202
  // ../packages/core/dist/fs-scan.js
167
203
  import { promises as fs } from "node:fs";
@@ -225,13 +261,144 @@ async function readJsonlFrom(filePath, fromLine) {
225
261
  return { totalLines: lines.length, entries };
226
262
  }
227
263
 
264
+ // ../packages/core/dist/providers/file-read.js
265
+ import { statSync } from "node:fs";
266
+ import * as path2 from "node:path";
267
+ var BYTES_PER_TOKEN = 4;
268
+ var APPROX_BYTES_PER_LINE = 120;
269
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
270
+ ".7z",
271
+ ".avif",
272
+ ".bmp",
273
+ ".class",
274
+ ".dll",
275
+ ".dylib",
276
+ ".exe",
277
+ ".gif",
278
+ ".gz",
279
+ ".ico",
280
+ ".jar",
281
+ ".jpeg",
282
+ ".jpg",
283
+ ".mov",
284
+ ".mp3",
285
+ ".mp4",
286
+ ".pdf",
287
+ ".png",
288
+ ".so",
289
+ ".tar",
290
+ ".wav",
291
+ ".webm",
292
+ ".webp",
293
+ ".zip"
294
+ ]);
295
+ function isGeneratedPath(value) {
296
+ const normalized = value.replace(/\\/g, "/").toLowerCase();
297
+ return /(^|\/)(node_modules|dist|build|out|coverage|target|vendor|\.next|\.nuxt|\.cache)(\/|$)/.test(normalized) || /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|cargo\.lock)$/.test(normalized) || /\.(min\.(js|css)|map|bundle\.(js|css))$/.test(normalized) || /(^|\/)[^/]*bundle[^/]*\.(js|css)$/.test(normalized);
298
+ }
299
+ function isTextTokenCandidatePath(value) {
300
+ return !BINARY_EXTENSIONS.has(path2.extname(value).toLowerCase());
301
+ }
302
+ function displayPath(absolutePath, cwd) {
303
+ const normalized = absolutePath.replace(/\\/g, "/");
304
+ const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
305
+ if (normalizedCwd && normalized.toLowerCase().startsWith(`${normalizedCwd.toLowerCase()}/`)) {
306
+ return normalized.slice(normalizedCwd.length + 1);
307
+ }
308
+ const parts = normalized.split("/").filter(Boolean);
309
+ return parts.slice(-3).join("/") || path2.basename(absolutePath);
310
+ }
311
+ function fileReadFromPath(rawPath, cwd, limitLines) {
312
+ if (!rawPath.trim())
313
+ return null;
314
+ if (!isTextTokenCandidatePath(rawPath))
315
+ return null;
316
+ const rawIsAbsolute = path2.isAbsolute(rawPath);
317
+ const absolutePath = rawIsAbsolute ? path2.normalize(rawPath) : path2.resolve(cwd || process.cwd(), rawPath);
318
+ let bytes = 0;
319
+ try {
320
+ const stat4 = statSync(absolutePath);
321
+ if (!stat4.isFile())
322
+ return null;
323
+ bytes = stat4.size;
324
+ } catch {
325
+ }
326
+ if (limitLines && limitLines > 0 && bytes > 0) {
327
+ bytes = Math.min(bytes, limitLines * APPROX_BYTES_PER_LINE);
328
+ }
329
+ const normalizedRaw = rawPath.replace(/\\/g, "/").replace(/^\.\//, "");
330
+ const safePath = !rawIsAbsolute && !normalizedRaw.startsWith("../") ? normalizedRaw : displayPath(absolutePath, cwd);
331
+ return {
332
+ path: safePath,
333
+ bytes,
334
+ estimatedTokens: bytes > 0 ? Math.ceil(bytes / BYTES_PER_TOKEN) : 0,
335
+ generated: isGeneratedPath(safePath)
336
+ };
337
+ }
338
+ function parseJsonObject(value) {
339
+ if (typeof value !== "string")
340
+ return {};
341
+ try {
342
+ const parsed = JSON.parse(value);
343
+ return parsed && typeof parsed === "object" ? parsed : {};
344
+ } catch {
345
+ return {};
346
+ }
347
+ }
348
+ function candidateAfterCommand(command, tool) {
349
+ const escaped = tool.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
350
+ const rx = new RegExp(`(?:^|[;|&]\\s*)${escaped}\\s+(?:(?:-[\\w-]+)(?:\\s+\\d+)?\\s+)*(?:"([^"]+)"|'([^']+)'|([^\\s;|&]+))`, "gim");
351
+ const out = [];
352
+ for (const match of command.matchAll(rx)) {
353
+ const value = match[1] ?? match[2] ?? match[3];
354
+ if (value && !value.startsWith("-") && !value.startsWith("$") && !value.startsWith("(") && !value.startsWith("%")) {
355
+ out.push(value);
356
+ }
357
+ }
358
+ return out;
359
+ }
360
+ function fileReadsFromCodexCall(name, argsValue, fallbackCwd) {
361
+ const args = parseJsonObject(argsValue);
362
+ const cwd = typeof args["workdir"] === "string" ? args["workdir"] : fallbackCwd;
363
+ if (name === "view_image")
364
+ return [];
365
+ if (name !== "shell_command")
366
+ return [];
367
+ const command = typeof args["command"] === "string" ? args["command"] : "";
368
+ const candidates = [
369
+ ...candidateAfterCommand(command, "Get-Content"),
370
+ ...candidateAfterCommand(command, "gc"),
371
+ ...candidateAfterCommand(command, "cat"),
372
+ ...candidateAfterCommand(command, "type"),
373
+ ...candidateAfterCommand(command, "more"),
374
+ ...candidateAfterCommand(command, "head"),
375
+ ...candidateAfterCommand(command, "tail")
376
+ ];
377
+ return [...new Set(candidates)].flatMap((candidate) => {
378
+ const read = fileReadFromPath(candidate, cwd);
379
+ return read ? [read] : [];
380
+ });
381
+ }
382
+ function fillMissingReadEstimate(reads, output) {
383
+ if (typeof output !== "string" || output.length === 0)
384
+ return;
385
+ const missing = reads.filter((read) => read.estimatedTokens === 0);
386
+ if (missing.length === 0)
387
+ return;
388
+ const each = Math.max(1, Math.ceil(output.length / BYTES_PER_TOKEN / missing.length));
389
+ for (const read of missing) {
390
+ read.estimatedTokens = each;
391
+ read.bytes = each * BYTES_PER_TOKEN;
392
+ }
393
+ }
394
+
228
395
  // ../packages/core/dist/providers/claude.js
229
396
  var MTIME_EPSILON_MS = 1;
230
397
  function defaultClaudeDirs(home = os.homedir()) {
231
398
  return [
232
- path2.join(home, ".claude", "projects"),
399
+ path3.join(home, ".claude", "projects"),
233
400
  // Xcode coding-assistant integration (macOS); silently skipped if absent.
234
- path2.join(home, "Library", "Developer", "Xcode", "CodingAssistant", "ClaudeAgentConfig", "projects")
401
+ path3.join(home, "Library", "Developer", "Xcode", "CodingAssistant", "ClaudeAgentConfig", "projects")
235
402
  ];
236
403
  }
237
404
  function parseClaudeEntries(entries) {
@@ -256,14 +423,20 @@ function parseClaudeEntries(entries) {
256
423
  if (inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens === 0)
257
424
  continue;
258
425
  let toolName = null;
426
+ const fileReads = [];
427
+ const cwd = rec.cwd ?? "";
259
428
  for (const item of msg.content ?? []) {
260
429
  if (item && typeof item === "object" && item.type === "tool_use") {
261
- toolName = item.name ?? null;
262
- break;
430
+ toolName ??= item.name ?? null;
431
+ if (item.name === "Read" && typeof item.input?.file_path === "string") {
432
+ const limit = typeof item.input.limit === "number" ? item.input.limit : void 0;
433
+ const read = fileReadFromPath(item.input.file_path, cwd, limit);
434
+ if (read)
435
+ fileReads.push(read);
436
+ }
263
437
  }
264
438
  }
265
439
  const messageId = msg.id ?? "";
266
- const cwd = rec.cwd ?? "";
267
440
  const turn = {
268
441
  provider: "claude",
269
442
  turnId: messageId ? `claude:${messageId}` : `claude:noid:${sessionId}:${lineNo}`,
@@ -277,6 +450,7 @@ function parseClaudeEntries(entries) {
277
450
  cachedInputTokens: 0,
278
451
  reasoningTokens: 0,
279
452
  toolName,
453
+ fileReads,
280
454
  project: projectNameFromCwd(cwd),
281
455
  cwd,
282
456
  gitBranch: rec.gitBranch ?? ""
@@ -288,6 +462,63 @@ function parseClaudeEntries(entries) {
288
462
  }
289
463
  return [...noId, ...seen.values()];
290
464
  }
465
+ function asNumber(v) {
466
+ const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
467
+ return Number.isFinite(n) ? n : void 0;
468
+ }
469
+ function labelClaudeWindow(id) {
470
+ const k = id.toLowerCase();
471
+ if (k.includes("5") || k.includes("session"))
472
+ return "Session (5hr)";
473
+ if (k.includes("7") || k.includes("week"))
474
+ return "Weekly (7 day)";
475
+ return "Usage";
476
+ }
477
+ function collectClaudeWindows(value, prefix = "") {
478
+ if (!value || typeof value !== "object")
479
+ return [];
480
+ const obj = value;
481
+ const usedPercent = asNumber(obj["used_percentage"] ?? obj["usedPercent"] ?? obj["used_percent"]);
482
+ const resetsRaw = obj["resets_at"] ?? obj["resetsAt"] ?? obj["reset_at"];
483
+ const resetsAt = typeof resetsRaw === "string" ? resetsRaw : asNumber(resetsRaw) !== void 0 ? new Date(asNumber(resetsRaw) * 1e3).toISOString() : void 0;
484
+ const resetAfterSeconds = asNumber(obj["resets_in_seconds"] ?? obj["reset_after_seconds"] ?? obj["resetAfterSeconds"]);
485
+ const current = usedPercent === void 0 ? [] : [
486
+ {
487
+ id: prefix || "usage",
488
+ label: labelClaudeWindow(prefix || "usage"),
489
+ usedPercent,
490
+ resetsAt,
491
+ resetAfterSeconds
492
+ }
493
+ ];
494
+ for (const [key, child] of Object.entries(obj)) {
495
+ if (!child || typeof child !== "object")
496
+ continue;
497
+ current.push(...collectClaudeWindows(child, prefix ? `${prefix}.${key}` : key));
498
+ }
499
+ return current;
500
+ }
501
+ function parseClaudeLimitSnapshots(entries, syncedAt = /* @__PURE__ */ new Date()) {
502
+ let latest = null;
503
+ for (const { record } of entries) {
504
+ if (!record || typeof record !== "object")
505
+ continue;
506
+ const rec = record;
507
+ const rateLimits = rec.rateLimits ?? rec["rate_limits"];
508
+ if (!rateLimits)
509
+ continue;
510
+ const windows = collectClaudeWindows(rateLimits).filter((w, idx, arr) => arr.findIndex((x) => x.id === w.id) === idx);
511
+ if (windows.length === 0)
512
+ continue;
513
+ latest = {
514
+ provider: "claude",
515
+ source: "claude.rateLimits",
516
+ updatedAt: syncedAt.toISOString(),
517
+ windows
518
+ };
519
+ }
520
+ return latest ? [latest] : [];
521
+ }
291
522
  var ClaudeConnector = class {
292
523
  id = "claude";
293
524
  dirs;
@@ -300,6 +531,7 @@ var ClaudeConnector = class {
300
531
  async collect(state) {
301
532
  const nextFiles = { ...state.files };
302
533
  const turns = [];
534
+ const scannedEntries = [];
303
535
  for (const dir of this.dirs) {
304
536
  const files = await walkFiles(dir, (name) => name.endsWith(".jsonl"));
305
537
  for (const filePath of files) {
@@ -312,22 +544,30 @@ var ClaudeConnector = class {
312
544
  }
313
545
  const fromLine = cursor ? cursor.lines : 0;
314
546
  const { totalLines, entries } = await readJsonlFrom(filePath, fromLine);
547
+ scannedEntries.push(...entries);
315
548
  if (entries.length > 0) {
316
549
  turns.push(...parseClaudeEntries(entries));
317
550
  }
318
551
  nextFiles[filePath] = { mtimeMs, lines: totalLines };
319
552
  }
320
553
  }
321
- return { turns, state: { files: nextFiles } };
554
+ return { turns, limits: parseClaudeLimitSnapshots(scannedEntries), state: { files: nextFiles } };
322
555
  }
323
556
  };
324
557
 
325
558
  // ../packages/core/dist/providers/codex.js
559
+ import { promises as fs2 } from "node:fs";
326
560
  import * as os2 from "node:os";
327
- import * as path3 from "node:path";
561
+ import * as path4 from "node:path";
328
562
  var MTIME_EPSILON_MS2 = 1;
563
+ var RATE_LIMIT_REFRESH_MS = 6e4;
564
+ var MAX_RATE_LIMIT_LOG_BYTES = 16 * 1024 * 1024;
329
565
  function defaultCodexDirs(home = os2.homedir()) {
330
- return [path3.join(home, ".codex", "sessions")];
566
+ return [path4.join(home, ".codex", "sessions")];
567
+ }
568
+ function defaultCodexRateLimitFiles(home = os2.homedir()) {
569
+ const base = path4.join(home, ".codex");
570
+ return [path4.join(base, "logs_2.sqlite"), path4.join(base, "logs_2.sqlite-wal")];
331
571
  }
332
572
  function usageFromObj(obj) {
333
573
  if (!obj || typeof obj !== "object")
@@ -416,8 +656,29 @@ function parseCodexEntries(fileId, entries) {
416
656
  let prevCumulative = null;
417
657
  let seq = 0;
418
658
  let runningModel = meta.model;
659
+ let pendingReads = [];
660
+ let pendingToolName = null;
661
+ const readsByCall = /* @__PURE__ */ new Map();
419
662
  for (const { record } of entries) {
420
- const recModel = pickModel(record?.["payload"]) ?? pickModel(record);
663
+ const rec = record;
664
+ const payload = rec["payload"];
665
+ if (payload?.["type"] === "function_call") {
666
+ const name = typeof payload["name"] === "string" ? payload["name"] : "";
667
+ pendingToolName = name || pendingToolName;
668
+ const reads = fileReadsFromCodexCall(name, payload["arguments"], meta.cwd);
669
+ if (reads.length > 0) {
670
+ pendingReads = [...pendingReads, ...reads];
671
+ const callId = typeof payload["call_id"] === "string" ? payload["call_id"] : "";
672
+ if (callId)
673
+ readsByCall.set(callId, reads);
674
+ }
675
+ } else if (payload?.["type"] === "function_call_output") {
676
+ const callId = typeof payload["call_id"] === "string" ? payload["call_id"] : "";
677
+ const reads = readsByCall.get(callId);
678
+ if (reads)
679
+ fillMissingReadEstimate(reads, payload["output"]);
680
+ }
681
+ const recModel = pickModel(payload) ?? pickModel(record);
421
682
  if (recModel)
422
683
  runningModel = recModel;
423
684
  const ev = extractUsageEvent(record);
@@ -453,26 +714,148 @@ function parseCodexEntries(fileId, entries) {
453
714
  cacheCreationTokens: 0,
454
715
  cachedInputTokens: delta.cachedInput,
455
716
  reasoningTokens: delta.reasoning,
456
- toolName: null,
717
+ toolName: pendingToolName,
718
+ fileReads: pendingReads,
457
719
  project: meta.project,
458
720
  cwd: meta.cwd,
459
721
  gitBranch: meta.gitBranch
460
722
  });
723
+ pendingReads = [];
724
+ pendingToolName = null;
725
+ readsByCall.clear();
461
726
  seq++;
462
727
  }
463
728
  return turns;
464
729
  }
465
730
  function codexFileId(filePath) {
466
- return path3.basename(filePath).replace(/\.jsonl$/i, "");
731
+ return path4.basename(filePath).replace(/\.jsonl$/i, "");
732
+ }
733
+ function asNumber2(v) {
734
+ const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
735
+ return Number.isFinite(n) ? n : void 0;
736
+ }
737
+ function codexWindowLabel(id, minutes) {
738
+ if (minutes === 300)
739
+ return "5 hour usage limit";
740
+ if (minutes === 10080)
741
+ return "Weekly usage limit";
742
+ return id === "primary" ? "Primary usage limit" : id === "secondary" ? "Secondary usage limit" : "Usage limit";
743
+ }
744
+ function toNativeWindow(id, raw) {
745
+ if (!raw)
746
+ return null;
747
+ const usedPercent = asNumber2(raw.used_percent);
748
+ if (usedPercent === void 0)
749
+ return null;
750
+ const windowMinutes = asNumber2(raw.window_minutes);
751
+ const resetAfterSeconds = asNumber2(raw.reset_after_seconds);
752
+ const resetAtSeconds = asNumber2(raw.reset_at);
753
+ return {
754
+ id,
755
+ label: codexWindowLabel(id, windowMinutes),
756
+ usedPercent,
757
+ windowMinutes,
758
+ resetsAt: resetAtSeconds ? new Date(resetAtSeconds * 1e3).toISOString() : void 0,
759
+ resetAfterSeconds
760
+ };
761
+ }
762
+ function extractJsonObject(text, start) {
763
+ let depth = 0;
764
+ let inString = false;
765
+ let escaped = false;
766
+ for (let i = start; i < text.length; i++) {
767
+ const ch = text[i];
768
+ if (inString) {
769
+ if (escaped) {
770
+ escaped = false;
771
+ } else if (ch === "\\") {
772
+ escaped = true;
773
+ } else if (ch === '"') {
774
+ inString = false;
775
+ }
776
+ continue;
777
+ }
778
+ if (ch === '"') {
779
+ inString = true;
780
+ } else if (ch === "{") {
781
+ depth++;
782
+ } else if (ch === "}") {
783
+ depth--;
784
+ if (depth === 0)
785
+ return text.slice(start, i + 1);
786
+ }
787
+ }
788
+ return null;
789
+ }
790
+ function parseCodexRateLimitSnapshot(text, syncedAt = /* @__PURE__ */ new Date()) {
791
+ const needle = '"type":"codex.rate_limits"';
792
+ let pos = -1;
793
+ let latest = null;
794
+ while ((pos = text.indexOf(needle, pos + 1)) !== -1) {
795
+ const start = text.lastIndexOf("{", pos);
796
+ if (start < 0)
797
+ continue;
798
+ const json = extractJsonObject(text, start);
799
+ if (!json)
800
+ continue;
801
+ let payload;
802
+ try {
803
+ payload = JSON.parse(json);
804
+ } catch {
805
+ continue;
806
+ }
807
+ if (payload.type !== "codex.rate_limits")
808
+ continue;
809
+ const windows = [
810
+ toNativeWindow("primary", payload.rate_limits?.primary),
811
+ toNativeWindow("secondary", payload.rate_limits?.secondary)
812
+ ].filter((w) => Boolean(w));
813
+ if (windows.length === 0)
814
+ continue;
815
+ latest = {
816
+ provider: "codex",
817
+ source: "codex.rate_limits",
818
+ planType: typeof payload.plan_type === "string" ? payload.plan_type : void 0,
819
+ updatedAt: syncedAt.toISOString(),
820
+ windows
821
+ };
822
+ }
823
+ return latest;
824
+ }
825
+ async function readCodexRateLimits(files) {
826
+ const chunks = [];
827
+ for (const file of files) {
828
+ try {
829
+ const stat4 = await fs2.stat(file);
830
+ const length = Math.min(stat4.size, MAX_RATE_LIMIT_LOG_BYTES);
831
+ const handle = await fs2.open(file, "r");
832
+ try {
833
+ const buffer = Buffer.alloc(length);
834
+ await handle.read(buffer, 0, length, Math.max(0, stat4.size - length));
835
+ chunks.push(buffer.toString("latin1"));
836
+ } finally {
837
+ await handle.close();
838
+ }
839
+ } catch {
840
+ }
841
+ }
842
+ if (chunks.length === 0)
843
+ return [];
844
+ const snapshot2 = parseCodexRateLimitSnapshot(chunks.join("\n"), /* @__PURE__ */ new Date());
845
+ return snapshot2 ? [snapshot2] : [];
467
846
  }
468
847
  var CodexConnector = class {
469
848
  id = "codex";
470
849
  dirs;
471
- constructor(dirs = defaultCodexDirs()) {
850
+ rateLimitFiles;
851
+ cachedLimits = [];
852
+ nextLimitReadAt = 0;
853
+ constructor(dirs = defaultCodexDirs(), rateLimitFiles = defaultCodexRateLimitFiles()) {
472
854
  this.dirs = dirs;
855
+ this.rateLimitFiles = rateLimitFiles;
473
856
  }
474
857
  watchPaths() {
475
- return this.dirs;
858
+ return [...this.dirs, ...this.rateLimitFiles];
476
859
  }
477
860
  async collect(state) {
478
861
  const nextFiles = { ...state.files };
@@ -489,39 +872,51 @@ var CodexConnector = class {
489
872
  }
490
873
  const { totalLines, entries } = await readJsonlFrom(filePath, 0);
491
874
  if (entries.length > 0) {
492
- turns.push(...parseCodexEntries(codexFileId(filePath), entries));
875
+ const parsed = parseCodexEntries(codexFileId(filePath), entries);
876
+ if (cursor && totalLines >= cursor.lines) {
877
+ const previousCount = parseCodexEntries(codexFileId(filePath), entries.filter((entry) => entry.lineNo <= cursor.lines)).length;
878
+ turns.push(...parsed.slice(previousCount));
879
+ } else {
880
+ turns.push(...parsed);
881
+ }
493
882
  }
494
883
  nextFiles[filePath] = { mtimeMs, lines: totalLines };
495
884
  }
496
885
  }
497
- return { turns, state: { files: nextFiles } };
886
+ if (Date.now() >= this.nextLimitReadAt) {
887
+ const latest = await readCodexRateLimits(this.rateLimitFiles);
888
+ if (latest.length > 0)
889
+ this.cachedLimits = latest;
890
+ this.nextLimitReadAt = Date.now() + RATE_LIMIT_REFRESH_MS;
891
+ }
892
+ return { turns, limits: this.cachedLimits, state: { files: nextFiles } };
498
893
  }
499
894
  };
500
895
 
501
896
  // src/paths.ts
502
897
  function configDir() {
503
- return process.env.TOKELYTICS_HOME ?? path4.join(os3.homedir(), ".tokelytics");
898
+ return process.env.TOKELYTICS_HOME ?? path5.join(os3.homedir(), ".tokelytics");
504
899
  }
505
- var statePath = () => path4.join(configDir(), "state.json");
506
- var credsPath = () => path4.join(configDir(), "credentials.json");
900
+ var statePath = () => path5.join(configDir(), "state.json");
901
+ var credsPath = () => path5.join(configDir(), "credentials.json");
507
902
  async function ensureDir() {
508
- await fs2.mkdir(configDir(), { recursive: true });
903
+ await fs3.mkdir(configDir(), { recursive: true });
509
904
  }
510
905
  async function readJson(file) {
511
906
  try {
512
- return JSON.parse(await fs2.readFile(file, "utf-8"));
907
+ return JSON.parse(await fs3.readFile(file, "utf-8"));
513
908
  } catch {
514
909
  return null;
515
910
  }
516
911
  }
517
912
  async function writeJson(file, value) {
518
913
  await ensureDir();
519
- await fs2.writeFile(file, JSON.stringify(value, null, 2), "utf-8");
914
+ await fs3.writeFile(file, JSON.stringify(value, null, 2), "utf-8");
520
915
  }
521
916
  async function loadState() {
522
917
  const s = await readJson(statePath());
523
918
  if (s && s.deviceId && s.scan) return s;
524
- const fresh = { deviceId: randomUUID(), scan: emptyScanState() };
919
+ const fresh = { deviceId: randomUUID(), scan: emptyScanState(), publication: {} };
525
920
  await writeJson(statePath(), fresh);
526
921
  return fresh;
527
922
  }
@@ -534,13 +929,13 @@ async function loadCredentials() {
534
929
  async function saveCredentials(creds) {
535
930
  await writeJson(credsPath(), creds);
536
931
  try {
537
- await fs2.chmod(credsPath(), 384);
932
+ await fs3.chmod(credsPath(), 384);
538
933
  } catch {
539
934
  }
540
935
  }
541
936
  async function clearCredentials() {
542
937
  try {
543
- await fs2.rm(credsPath(), { force: true });
938
+ await fs3.rm(credsPath(), { force: true });
544
939
  } catch {
545
940
  }
546
941
  }
@@ -577,7 +972,7 @@ function fromEnv() {
577
972
  }
578
973
  async function fromFile() {
579
974
  try {
580
- const raw = await fs3.readFile(path5.join(configDir(), "config.json"), "utf-8");
975
+ const raw = await fs4.readFile(path6.join(configDir(), "config.json"), "utf-8");
581
976
  return JSON.parse(raw);
582
977
  } catch {
583
978
  return {};
@@ -620,7 +1015,7 @@ function openBrowser(url) {
620
1015
  }
621
1016
  }
622
1017
  function captureOAuthCode(buildUrl) {
623
- return new Promise((resolve3, reject) => {
1018
+ return new Promise((resolve4, reject) => {
624
1019
  const server = http.createServer((req, res) => {
625
1020
  const u = new URL(req.url ?? "/", "http://localhost");
626
1021
  const code = u.searchParams.get("code");
@@ -630,7 +1025,7 @@ function captureOAuthCode(buildUrl) {
630
1025
  `<html><body style="font-family:system-ui;padding:3rem;text-align:center"><h2>Tokelytics</h2><p>${code ? "You're signed in. You can close this tab." : "Sign-in failed: " + error}</p></body></html>`
631
1026
  );
632
1027
  server.close();
633
- if (code) resolve3({ code });
1028
+ if (code) resolve4({ code });
634
1029
  else reject(new Error(`OAuth failed: ${error ?? "no code returned"}`));
635
1030
  });
636
1031
  server.listen(0, "127.0.0.1", () => {
@@ -725,13 +1120,13 @@ var CORS_HEADERS = {
725
1120
  "Access-Control-Allow-Headers": "Content-Type"
726
1121
  };
727
1122
  function readBody(req) {
728
- return new Promise((resolve3, reject) => {
1123
+ return new Promise((resolve4, reject) => {
729
1124
  let data = "";
730
1125
  req.on("data", (chunk) => {
731
1126
  data += chunk;
732
1127
  if (data.length > 1e6) req.destroy();
733
1128
  });
734
- req.on("end", () => resolve3(data));
1129
+ req.on("end", () => resolve4(data));
735
1130
  req.on("error", reject);
736
1131
  });
737
1132
  }
@@ -748,10 +1143,9 @@ async function exchangeRefreshToken(cfg, refreshToken) {
748
1143
  async function browserHandoffLogin(cfg) {
749
1144
  const nonce = randomBytes(5).toString("hex").toUpperCase();
750
1145
  const TIMEOUT_MS = 5 * 6e4;
751
- const payload = await new Promise((resolve3, reject) => {
752
- let timer;
1146
+ const payload = await new Promise((resolve4, reject) => {
753
1147
  function finish(act) {
754
- if (timer) clearTimeout(timer);
1148
+ clearTimeout(timer);
755
1149
  server.close();
756
1150
  act();
757
1151
  }
@@ -781,10 +1175,10 @@ async function browserHandoffLogin(cfg) {
781
1175
  }
782
1176
  res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
783
1177
  res.end(JSON.stringify({ ok: true }));
784
- finish(() => resolve3(body));
1178
+ finish(() => resolve4(body));
785
1179
  }).catch((err) => finish(() => reject(err)));
786
1180
  });
787
- timer = setTimeout(
1181
+ const timer = setTimeout(
788
1182
  () => finish(() => reject(new Error('Timed out waiting for browser sign-in. Run "tokelytics login" again.'))),
789
1183
  TIMEOUT_MS
790
1184
  );
@@ -840,13 +1234,300 @@ async function restoreSession(cfg) {
840
1234
  }
841
1235
 
842
1236
  // src/runner.ts
843
- import * as os4 from "node:os";
1237
+ import * as os5 from "node:os";
844
1238
 
845
1239
  // src/connectors.ts
846
1240
  function buildConnectors() {
847
1241
  return [new ClaudeConnector(), new CodexConnector()];
848
1242
  }
849
1243
 
1244
+ // src/provider-limits.ts
1245
+ import { promises as fs5 } from "node:fs";
1246
+ import * as os4 from "node:os";
1247
+ import * as path7 from "node:path";
1248
+ var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
1249
+ var CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
1250
+ var REQUEST_TIMEOUT_MS = 1e4;
1251
+ var REFRESH_INTERVAL_MS = 6e4;
1252
+ function asNumber3(value) {
1253
+ const number = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
1254
+ return Number.isFinite(number) ? number : void 0;
1255
+ }
1256
+ function asIso(value) {
1257
+ if (typeof value === "string") {
1258
+ const date = new Date(value);
1259
+ return Number.isFinite(date.getTime()) ? date.toISOString() : void 0;
1260
+ }
1261
+ const seconds = asNumber3(value);
1262
+ if (seconds === void 0 || seconds <= 0) return void 0;
1263
+ return new Date(seconds * 1e3).toISOString();
1264
+ }
1265
+ function snapshot(provider, source, status, message, updatedAt, windows = [], planType) {
1266
+ return {
1267
+ provider,
1268
+ source,
1269
+ status,
1270
+ statusMessage: message,
1271
+ planType,
1272
+ updatedAt: updatedAt.toISOString(),
1273
+ windows
1274
+ };
1275
+ }
1276
+ function parseClaudeUsageResponse(value, updatedAt = /* @__PURE__ */ new Date(), planType) {
1277
+ const response = value ?? {};
1278
+ const windows = [];
1279
+ const add = (id, label, minutes, bucket) => {
1280
+ const usedPercent = asNumber3(bucket?.utilization);
1281
+ if (usedPercent === void 0) return;
1282
+ windows.push({
1283
+ id,
1284
+ label,
1285
+ usedPercent,
1286
+ windowMinutes: minutes,
1287
+ resetsAt: asIso(bucket?.resets_at)
1288
+ });
1289
+ };
1290
+ add("session", "5-hour", 300, response.five_hour);
1291
+ add("weekly", "Weekly", 10080, response.seven_day);
1292
+ return snapshot(
1293
+ "claude",
1294
+ "claude.oauth_usage",
1295
+ windows.length > 0 ? "available" : "unavailable",
1296
+ windows.length > 0 ? void 0 : "Claude did not return allowance windows. Retrying automatically.",
1297
+ updatedAt,
1298
+ windows,
1299
+ planType
1300
+ );
1301
+ }
1302
+ function parseCodexUsageResponse(value, updatedAt = /* @__PURE__ */ new Date()) {
1303
+ const response = value ?? {};
1304
+ const details = response.rate_limit;
1305
+ const windows = [];
1306
+ const add = (id, label, fallbackMinutes, window) => {
1307
+ const usedPercent = asNumber3(window?.used_percent);
1308
+ if (usedPercent === void 0) return;
1309
+ const windowSeconds = asNumber3(window?.limit_window_seconds);
1310
+ windows.push({
1311
+ id,
1312
+ label,
1313
+ usedPercent,
1314
+ windowMinutes: windowSeconds ? Math.round(windowSeconds / 60) : fallbackMinutes,
1315
+ resetsAt: asIso(window?.reset_at)
1316
+ });
1317
+ };
1318
+ add("primary", "5-hour", 300, details?.primary_window);
1319
+ add("secondary", "Weekly", 10080, details?.secondary_window);
1320
+ return snapshot(
1321
+ "codex",
1322
+ "codex.oauth_usage",
1323
+ windows.length > 0 ? "available" : "unavailable",
1324
+ windows.length > 0 ? void 0 : "Codex did not return allowance windows. Retrying automatically.",
1325
+ updatedAt,
1326
+ windows,
1327
+ typeof response.plan_type === "string" ? response.plan_type : void 0
1328
+ );
1329
+ }
1330
+ async function readJson2(file) {
1331
+ try {
1332
+ return JSON.parse(await fs5.readFile(file, "utf-8"));
1333
+ } catch {
1334
+ return null;
1335
+ }
1336
+ }
1337
+ async function readClaudeCredentials(home, env) {
1338
+ const configDir2 = env.CLAUDE_CONFIG_DIR || path7.join(home, ".claude");
1339
+ const root = await readJson2(path7.join(configDir2, ".credentials.json"));
1340
+ const oauth = root?.["claudeAiOauth"];
1341
+ if (!oauth || typeof oauth !== "object") return null;
1342
+ const data = oauth;
1343
+ const accessToken = data["accessToken"];
1344
+ if (typeof accessToken !== "string" || !accessToken) return null;
1345
+ return {
1346
+ accessToken,
1347
+ expiresAt: asNumber3(data["expiresAt"]),
1348
+ planType: typeof data["subscriptionType"] === "string" ? data["subscriptionType"] : void 0
1349
+ };
1350
+ }
1351
+ async function readCodexCredentials(home, env) {
1352
+ const configDir2 = env.CODEX_HOME || path7.join(home, ".codex");
1353
+ const root = await readJson2(path7.join(configDir2, "auth.json"));
1354
+ const tokens = root?.["tokens"];
1355
+ if (!tokens || typeof tokens !== "object") return null;
1356
+ const data = tokens;
1357
+ const accessToken = data["access_token"];
1358
+ if (typeof accessToken !== "string" || !accessToken) return null;
1359
+ return {
1360
+ accessToken,
1361
+ accountId: typeof data["account_id"] === "string" ? data["account_id"] : void 0
1362
+ };
1363
+ }
1364
+ async function requestJson(fetcher, url, headers) {
1365
+ const response = await fetcher(url, {
1366
+ headers,
1367
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
1368
+ });
1369
+ if (!response.ok) return { status: response.status };
1370
+ try {
1371
+ return { status: response.status, body: await response.json() };
1372
+ } catch {
1373
+ return { status: response.status };
1374
+ }
1375
+ }
1376
+ function authMessage(provider) {
1377
+ return `${provider} sign-in needs refresh. Open ${provider} Code once; Tokelytics will retry automatically.`;
1378
+ }
1379
+ var ClaudeLimitCollector = class {
1380
+ constructor(options) {
1381
+ this.options = options;
1382
+ }
1383
+ provider = "claude";
1384
+ cached = null;
1385
+ nextRefreshAt = 0;
1386
+ async collect() {
1387
+ const updatedAt = this.options.now();
1388
+ if (this.cached && updatedAt.getTime() < this.nextRefreshAt) return this.cached;
1389
+ this.nextRefreshAt = updatedAt.getTime() + REFRESH_INTERVAL_MS;
1390
+ const fresh = await this.collectFresh(updatedAt);
1391
+ if (fresh.status === "error" && this.cached?.status === "available") {
1392
+ return {
1393
+ ...this.cached,
1394
+ statusMessage: "Latest refresh was delayed; showing the last provider-reported values."
1395
+ };
1396
+ }
1397
+ this.cached = fresh;
1398
+ return fresh;
1399
+ }
1400
+ async collectFresh(updatedAt) {
1401
+ try {
1402
+ const credentials = await readClaudeCredentials(this.options.home, this.options.env);
1403
+ if (!credentials) {
1404
+ return snapshot(
1405
+ "claude",
1406
+ "claude.oauth_usage",
1407
+ "unavailable",
1408
+ "Claude Code is not signed in on this machine.",
1409
+ updatedAt
1410
+ );
1411
+ }
1412
+ const result = await requestJson(this.options.fetcher, CLAUDE_USAGE_URL, {
1413
+ Authorization: `Bearer ${credentials.accessToken}`,
1414
+ "anthropic-beta": "oauth-2025-04-20"
1415
+ });
1416
+ if (result.status === 401 || result.status === 403) {
1417
+ return snapshot("claude", "claude.oauth_usage", "error", authMessage("Claude"), updatedAt);
1418
+ }
1419
+ if (result.status === 429) {
1420
+ return snapshot(
1421
+ "claude",
1422
+ "claude.oauth_usage",
1423
+ "error",
1424
+ "Claude temporarily limited usage checks. Retrying automatically.",
1425
+ updatedAt
1426
+ );
1427
+ }
1428
+ if (!result.body) {
1429
+ return snapshot(
1430
+ "claude",
1431
+ "claude.oauth_usage",
1432
+ "error",
1433
+ "Claude usage service could not be reached. Retrying automatically.",
1434
+ updatedAt
1435
+ );
1436
+ }
1437
+ return parseClaudeUsageResponse(result.body, updatedAt, credentials.planType);
1438
+ } catch {
1439
+ return snapshot(
1440
+ "claude",
1441
+ "claude.oauth_usage",
1442
+ "error",
1443
+ "Claude usage service could not be reached. Retrying automatically.",
1444
+ updatedAt
1445
+ );
1446
+ }
1447
+ }
1448
+ };
1449
+ var CodexLimitCollector = class {
1450
+ constructor(options) {
1451
+ this.options = options;
1452
+ }
1453
+ provider = "codex";
1454
+ cached = null;
1455
+ nextRefreshAt = 0;
1456
+ async collect() {
1457
+ const updatedAt = this.options.now();
1458
+ if (this.cached && updatedAt.getTime() < this.nextRefreshAt) return this.cached;
1459
+ this.nextRefreshAt = updatedAt.getTime() + REFRESH_INTERVAL_MS;
1460
+ const fresh = await this.collectFresh(updatedAt);
1461
+ if (fresh.status === "error" && this.cached?.status === "available") {
1462
+ return {
1463
+ ...this.cached,
1464
+ statusMessage: "Latest refresh was delayed; showing the last provider-reported values."
1465
+ };
1466
+ }
1467
+ this.cached = fresh;
1468
+ return fresh;
1469
+ }
1470
+ async collectFresh(updatedAt) {
1471
+ try {
1472
+ const credentials = await readCodexCredentials(this.options.home, this.options.env);
1473
+ if (!credentials) {
1474
+ return snapshot(
1475
+ "codex",
1476
+ "codex.oauth_usage",
1477
+ "unavailable",
1478
+ "Codex is not signed in on this machine.",
1479
+ updatedAt
1480
+ );
1481
+ }
1482
+ const headers = {
1483
+ Authorization: `Bearer ${credentials.accessToken}`,
1484
+ "User-Agent": "codex-cli"
1485
+ };
1486
+ if (credentials.accountId) headers["ChatGPT-Account-Id"] = credentials.accountId;
1487
+ const result = await requestJson(this.options.fetcher, CODEX_USAGE_URL, headers);
1488
+ if (result.status === 401 || result.status === 403) {
1489
+ return snapshot("codex", "codex.oauth_usage", "error", authMessage("Codex"), updatedAt);
1490
+ }
1491
+ if (result.status === 429) {
1492
+ return snapshot(
1493
+ "codex",
1494
+ "codex.oauth_usage",
1495
+ "error",
1496
+ "Codex temporarily limited usage checks. Retrying automatically.",
1497
+ updatedAt
1498
+ );
1499
+ }
1500
+ if (!result.body) {
1501
+ return snapshot(
1502
+ "codex",
1503
+ "codex.oauth_usage",
1504
+ "error",
1505
+ "Codex usage service could not be reached. Retrying automatically.",
1506
+ updatedAt
1507
+ );
1508
+ }
1509
+ return parseCodexUsageResponse(result.body, updatedAt);
1510
+ } catch {
1511
+ return snapshot(
1512
+ "codex",
1513
+ "codex.oauth_usage",
1514
+ "error",
1515
+ "Codex usage service could not be reached. Retrying automatically.",
1516
+ updatedAt
1517
+ );
1518
+ }
1519
+ }
1520
+ };
1521
+ function buildNativeLimitCollectors(options = {}) {
1522
+ const resolved = {
1523
+ home: options.home ?? os4.homedir(),
1524
+ env: options.env ?? process.env,
1525
+ fetcher: options.fetcher ?? fetch,
1526
+ now: options.now ?? (() => /* @__PURE__ */ new Date())
1527
+ };
1528
+ return [new ClaudeLimitCollector(resolved), new CodexLimitCollector(resolved)];
1529
+ }
1530
+
850
1531
  // src/firestore-rest.ts
851
1532
  function encodeValue(v) {
852
1533
  if (v === null || v === void 0) return { nullValue: null };
@@ -882,6 +1563,14 @@ function decodeFields(fields) {
882
1563
  for (const [k, val] of Object.entries(fields)) out[k] = decodeValue(val);
883
1564
  return out;
884
1565
  }
1566
+ var FirestoreRestError = class extends Error {
1567
+ constructor(message, status, retryAfterMs) {
1568
+ super(message);
1569
+ this.status = status;
1570
+ this.retryAfterMs = retryAfterMs;
1571
+ this.name = "FirestoreRestError";
1572
+ }
1573
+ };
885
1574
  var FirestoreRest = class {
886
1575
  projectId;
887
1576
  getToken;
@@ -911,9 +1600,22 @@ var FirestoreRest = class {
911
1600
  const res = await fetch(this.url(`${this.docsRoot()}:commit`), {
912
1601
  method: "POST",
913
1602
  headers: await this.headers(),
914
- body: JSON.stringify({ writes })
1603
+ body: JSON.stringify({ writes }),
1604
+ signal: AbortSignal.timeout(15e3)
1605
+ });
1606
+ if (!res.ok) throw await firestoreError("commit", res);
1607
+ }
1608
+ /** Fetch a single document's decoded fields, or null if it doesn't exist. */
1609
+ async getDoc(name) {
1610
+ const res = await fetch(this.url(name), {
1611
+ method: "GET",
1612
+ headers: await this.headers(),
1613
+ signal: AbortSignal.timeout(15e3)
915
1614
  });
916
- if (!res.ok) throw new Error(`Firestore commit failed (${res.status}): ${await res.text()}`);
1615
+ if (res.status === 404) return null;
1616
+ if (!res.ok) throw await firestoreError("get", res);
1617
+ const doc = await res.json();
1618
+ return doc.fields ? decodeFields(doc.fields) : {};
917
1619
  }
918
1620
  /** Run an equality query under a parent path, returning decoded documents. */
919
1621
  async queryEqual(parentSegments, collectionId, filters) {
@@ -929,94 +1631,170 @@ var FirestoreRest = class {
929
1631
  const res = await fetch(this.url(`${parent}:runQuery`), {
930
1632
  method: "POST",
931
1633
  headers: await this.headers(),
932
- body: JSON.stringify({ structuredQuery: { from: [{ collectionId }], where } })
1634
+ body: JSON.stringify({ structuredQuery: { from: [{ collectionId }], where } }),
1635
+ signal: AbortSignal.timeout(15e3)
933
1636
  });
934
- if (!res.ok) throw new Error(`Firestore query failed (${res.status}): ${await res.text()}`);
1637
+ if (!res.ok) throw await firestoreError("query", res);
935
1638
  const rows = await res.json();
936
1639
  return rows.filter((r) => r.document?.fields).map((r) => decodeFields(r.document.fields));
937
1640
  }
938
1641
  };
1642
+ async function firestoreError(operation, response) {
1643
+ const body = (await response.text()).slice(0, 800);
1644
+ const retryAfter = response.headers.get("retry-after");
1645
+ const seconds = retryAfter ? Number(retryAfter) : Number.NaN;
1646
+ return new FirestoreRestError(
1647
+ `Firestore ${operation} failed (${response.status}): ${body}`,
1648
+ response.status,
1649
+ Number.isFinite(seconds) ? seconds * 1e3 : void 0
1650
+ );
1651
+ }
939
1652
 
940
1653
  // src/firestore-sink.ts
941
- var BATCH = 400;
942
1654
  var FirestoreSink = class {
943
- constructor(fs4, uid) {
944
- this.fs = fs4;
1655
+ constructor(fs6, uid) {
1656
+ this.fs = fs6;
945
1657
  this.uid = uid;
946
1658
  }
947
- async writeTurns(turns) {
948
- for (let i = 0; i < turns.length; i += BATCH) {
949
- const slice = turns.slice(i, i + BATCH);
950
- await this.fs.upsert(
951
- slice.map((t) => ({
952
- name: this.fs.docName("users", this.uid, "turns", t.turnId),
953
- fields: { ...t, day: dayOf(t.ts) }
954
- }))
955
- );
956
- }
957
- }
958
- async turnsForSession(sessionId) {
959
- const rows = await this.fs.queryEqual(["users", this.uid], "turns", [
960
- { field: "sessionId", op: "EQUAL", value: sessionId }
961
- ]);
962
- return rows;
963
- }
964
- async turnsForBucket(provider, day) {
965
- const rows = await this.fs.queryEqual(["users", this.uid], "turns", [
966
- { field: "provider", op: "EQUAL", value: provider },
967
- { field: "day", op: "EQUAL", value: day }
968
- ]);
969
- return rows;
970
- }
971
- async recomputeSessions(sessionIds) {
972
- for (const sid of sessionIds) {
973
- const turns = await this.turnsForSession(sid);
974
- if (!turns.length) continue;
975
- const agg = aggregateSession(sid, turns);
976
- await this.fs.upsert([{ name: this.fs.docName("users", this.uid, "sessions", sid), fields: { ...agg } }]);
977
- }
978
- }
979
- async recomputeRollups(buckets) {
980
- for (const { provider, day } of buckets) {
981
- const turns = await this.turnsForBucket(provider, day);
982
- if (!turns.length) continue;
983
- const rollup = buildRollup(provider, day, turns);
984
- await this.fs.upsert([
985
- { name: this.fs.docName("users", this.uid, "rollups", rollupId(provider, day)), fields: { ...rollup } }
986
- ]);
987
- }
988
- }
989
- async touchDevice(meta) {
1659
+ async writeDashboardSnapshot(snapshot2) {
1660
+ if (!snapshot2.device?.deviceId) throw new Error("Dashboard snapshot is missing its device id.");
990
1661
  await this.fs.upsert([
991
- { name: this.fs.docName("users", this.uid, "devices", meta.deviceId), fields: { ...meta } }
1662
+ {
1663
+ name: this.fs.docName("users", this.uid, "machines", snapshot2.device.deviceId),
1664
+ fields: { ...snapshot2 }
1665
+ }
992
1666
  ]);
993
1667
  }
994
1668
  };
995
1669
 
996
1670
  // src/sync.ts
997
- async function runSync(connectors, sink, state, device) {
1671
+ async function runSync(connectors, sink, state, device, limitCollectors = [], options = {}) {
1672
+ const now = options.now ?? /* @__PURE__ */ new Date();
1673
+ const refreshLimits = options.refreshLimits ?? true;
1674
+ const publication = {
1675
+ ...options.publication,
1676
+ limitFingerprints: { ...options.publication?.limitFingerprints }
1677
+ };
1678
+ const cloudSyncIntervalMs = options.cloudSyncIntervalMs ?? 0;
1679
+ const maxCloudWritesPerDay = options.maxCloudWritesPerDay ?? 16;
998
1680
  let st = state;
999
1681
  const collected = [];
1682
+ const limits = [];
1000
1683
  const byProvider = {};
1001
1684
  for (const c of connectors) {
1002
- const { turns: turns2, state: next } = await c.collect(st);
1685
+ const { turns: turns2, limits: nativeLimits2 = [], state: next } = await c.collect(st);
1003
1686
  st = next;
1687
+ if (refreshLimits) limits.push(...nativeLimits2);
1004
1688
  for (const t of turns2) {
1005
1689
  collected.push(t);
1006
1690
  byProvider[t.provider] = (byProvider[t.provider] ?? 0) + 1;
1007
1691
  }
1008
1692
  }
1693
+ if (refreshLimits) {
1694
+ const providerLimits = await Promise.all(
1695
+ limitCollectors.map(async (collector) => {
1696
+ try {
1697
+ return await collector.collect();
1698
+ } catch {
1699
+ return null;
1700
+ }
1701
+ })
1702
+ );
1703
+ limits.push(...providerLimits.filter((limit) => Boolean(limit)));
1704
+ }
1009
1705
  const unique = /* @__PURE__ */ new Map();
1010
1706
  for (const t of collected) unique.set(t.turnId, t);
1011
1707
  const turns = [...unique.values()];
1012
- if (turns.length > 0) {
1013
- await sink.writeTurns(turns);
1014
- const { sessionIds, dayBuckets } = affectedKeys(turns);
1015
- await sink.recomputeSessions(sessionIds);
1016
- await sink.recomputeRollups(dayBuckets);
1708
+ const nativeLimits = refreshLimits ? dedupeLimits(limits) : [];
1709
+ const changedLimits = nativeLimits.filter(
1710
+ (limit) => publication.limitFingerprints?.[limit.provider] !== limitFingerprint(limit)
1711
+ );
1712
+ if (changedLimits.length > 0) {
1713
+ publication.limitFingerprints ??= {};
1714
+ for (const limit of changedLimits) {
1715
+ publication.limitFingerprints[limit.provider] = limitFingerprint(limit);
1716
+ }
1717
+ }
1718
+ if (refreshLimits) publication.lastLimitCheckAt = now.toISOString();
1719
+ const heartbeatMs = options.deviceHeartbeatMs ?? 0;
1720
+ const previousHeartbeat = Date.parse(publication.lastDeviceAt ?? "");
1721
+ const heartbeatDue = heartbeatMs === 0 || !Number.isFinite(previousHeartbeat) || now.getTime() - previousHeartbeat >= heartbeatMs;
1722
+ const updateDevice = turns.length > 0 || changedLimits.length > 0 || !options.dashboard?.device || options.forceDeviceHeartbeat || heartbeatDue;
1723
+ const dashboard = mergeDashboardSnapshot(
1724
+ options.dashboard,
1725
+ turns,
1726
+ changedLimits,
1727
+ updateDevice ? { ...device, lastSyncAt: now.toISOString() } : void 0,
1728
+ now
1729
+ );
1730
+ if (updateDevice) {
1731
+ publication.lastDeviceAt = now.toISOString();
1732
+ }
1733
+ const changed = turns.length > 0 || changedLimits.length > 0 || updateDevice;
1734
+ publication.dashboardDirty = Boolean(publication.dashboardDirty || changed);
1735
+ resetDailyBudget(publication, now);
1736
+ const lastCloudWrite = Date.parse(publication.lastCloudWriteAt ?? "");
1737
+ const cloudWriteDue = options.forceCloudPublish || !Number.isFinite(lastCloudWrite) || now.getTime() - lastCloudWrite >= cloudSyncIntervalMs;
1738
+ const hasBudget = (publication.cloudWritesToday ?? 0) < maxCloudWritesPerDay;
1739
+ let published = false;
1740
+ let publicationError;
1741
+ if (publication.dashboardDirty && cloudWriteDue && hasBudget) {
1742
+ try {
1743
+ await sink.writeDashboardSnapshot(dashboard);
1744
+ publication.cloudWritesToday = (publication.cloudWritesToday ?? 0) + 1;
1745
+ publication.lastCloudWriteAt = now.toISOString();
1746
+ publication.dashboardDirty = false;
1747
+ published = true;
1748
+ } catch (error) {
1749
+ publicationError = error;
1750
+ }
1017
1751
  }
1018
- await sink.touchDevice({ ...device, lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
1019
- return { newTurns: turns.length, byProvider, state: st };
1752
+ return {
1753
+ processedTurns: turns.length,
1754
+ processedLimits: changedLimits.length,
1755
+ byProvider,
1756
+ state: st,
1757
+ dashboard,
1758
+ publication,
1759
+ published,
1760
+ publicationError
1761
+ };
1762
+ }
1763
+ function dedupeLimits(limits) {
1764
+ const byProvider = /* @__PURE__ */ new Map();
1765
+ for (const limit of limits) {
1766
+ const current = byProvider.get(limit.provider);
1767
+ if (!current || limitScore(limit) >= limitScore(current)) byProvider.set(limit.provider, limit);
1768
+ }
1769
+ return [...byProvider.values()];
1770
+ }
1771
+ function limitScore(limit) {
1772
+ const available = (limit.status ?? (limit.windows.length > 0 ? "available" : "unavailable")) === "available";
1773
+ const oauth = limit.source.endsWith("oauth_usage");
1774
+ return (available ? 100 : 0) + (oauth ? 10 : 0) + limit.windows.length;
1775
+ }
1776
+ function limitFingerprint(limit) {
1777
+ return JSON.stringify({
1778
+ provider: limit.provider,
1779
+ source: limit.source,
1780
+ status: limit.status,
1781
+ statusMessage: limit.statusMessage,
1782
+ planType: limit.planType,
1783
+ windows: limit.windows
1784
+ });
1785
+ }
1786
+ function resetDailyBudget(publication, now) {
1787
+ const parts = new Intl.DateTimeFormat("en-US", {
1788
+ timeZone: "America/Los_Angeles",
1789
+ year: "numeric",
1790
+ month: "2-digit",
1791
+ day: "2-digit"
1792
+ }).formatToParts(now);
1793
+ const value = (type) => parts.find((part) => part.type === type)?.value ?? "";
1794
+ const day = `${value("year")}-${value("month")}-${value("day")}`;
1795
+ if (publication.cloudWriteDay === day) return;
1796
+ publication.cloudWriteDay = day;
1797
+ publication.cloudWritesToday = 0;
1020
1798
  }
1021
1799
 
1022
1800
  // src/runner.ts
@@ -1025,6 +1803,7 @@ async function createRunner() {
1025
1803
  const session = await restoreSession(cfg);
1026
1804
  if (!session) throw new Error('Not signed in. Run "tokelytics login" first.');
1027
1805
  const connectors = buildConnectors();
1806
+ const limitCollectors = buildNativeLimitCollectors();
1028
1807
  const rest = new FirestoreRest({
1029
1808
  projectId: cfg.firebase.projectId,
1030
1809
  getToken: () => session.getToken(),
@@ -1036,34 +1815,55 @@ async function createRunner() {
1036
1815
  watchPaths() {
1037
1816
  return connectors.flatMap((c) => c.watchPaths());
1038
1817
  },
1039
- async syncOnce() {
1818
+ async syncOnce(options = {}) {
1040
1819
  const state = await loadState();
1041
1820
  const device = {
1042
1821
  deviceId: state.deviceId,
1043
- name: os4.hostname(),
1822
+ name: os5.hostname(),
1044
1823
  lastSyncAt: "",
1045
1824
  providers: connectors.map((c) => c.id)
1046
1825
  };
1047
- const res = await runSync(connectors, sink, state.scan, device);
1048
- await saveState({ deviceId: state.deviceId, scan: res.state });
1049
- return { newTurns: res.newTurns, byProvider: res.byProvider };
1826
+ const scan = state.dashboard ? state.scan : emptyScanState();
1827
+ const res = await runSync(connectors, sink, scan, device, limitCollectors, {
1828
+ ...options,
1829
+ publication: state.publication,
1830
+ dashboard: state.dashboard
1831
+ });
1832
+ const nextState = {
1833
+ deviceId: state.deviceId,
1834
+ scan: res.state,
1835
+ dashboard: res.dashboard,
1836
+ publication: res.publication
1837
+ };
1838
+ if (!sameState(state, nextState)) await saveState(nextState);
1839
+ if (res.publicationError) throw res.publicationError;
1840
+ return {
1841
+ processedTurns: res.processedTurns,
1842
+ processedLimits: res.processedLimits,
1843
+ byProvider: res.byProvider,
1844
+ published: res.published,
1845
+ cloudWritesToday: res.publication.cloudWritesToday ?? 0
1846
+ };
1050
1847
  }
1051
1848
  };
1052
1849
  }
1850
+ function sameState(left, right) {
1851
+ return JSON.stringify(left) === JSON.stringify(right);
1852
+ }
1053
1853
  async function registerDevice(cfg, session) {
1054
- const rest = new FirestoreRest({
1055
- projectId: cfg.firebase.projectId,
1056
- getToken: () => session.getToken(),
1057
- emulatorHost: cfg.firestoreEmulatorHost
1058
- });
1059
- const sink = new FirestoreSink(rest, session.uid);
1060
1854
  const state = await loadState();
1061
1855
  const connectors = buildConnectors();
1062
- await sink.touchDevice({
1856
+ const now = /* @__PURE__ */ new Date();
1857
+ const device = {
1063
1858
  deviceId: state.deviceId,
1064
- name: os4.hostname(),
1065
- lastSyncAt: "",
1859
+ name: os5.hostname(),
1860
+ lastSyncAt: now.toISOString(),
1066
1861
  providers: connectors.map((c) => c.id)
1862
+ };
1863
+ await saveState({
1864
+ ...state,
1865
+ dashboard: mergeDashboardSnapshot(state.dashboard, [], [], device, now),
1866
+ publication: { ...state.publication, dashboardDirty: true, lastDeviceAt: now.toISOString() }
1067
1867
  });
1068
1868
  }
1069
1869
 
@@ -1143,7 +1943,7 @@ var ReaddirpStream = class extends Readable {
1143
1943
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
1144
1944
  const statMethod = opts.lstat ? lstat : stat;
1145
1945
  if (wantBigintFsStats) {
1146
- this._stat = (path6) => statMethod(path6, { bigint: true });
1946
+ this._stat = (path8) => statMethod(path8, { bigint: true });
1147
1947
  } else {
1148
1948
  this._stat = statMethod;
1149
1949
  }
@@ -1168,8 +1968,8 @@ var ReaddirpStream = class extends Readable {
1168
1968
  const par = this.parent;
1169
1969
  const fil = par && par.files;
1170
1970
  if (fil && fil.length > 0) {
1171
- const { path: path6, depth } = par;
1172
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path6));
1971
+ const { path: path8, depth } = par;
1972
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path8));
1173
1973
  const awaited = await Promise.all(slice);
1174
1974
  for (const entry of awaited) {
1175
1975
  if (!entry)
@@ -1209,21 +2009,21 @@ var ReaddirpStream = class extends Readable {
1209
2009
  this.reading = false;
1210
2010
  }
1211
2011
  }
1212
- async _exploreDir(path6, depth) {
2012
+ async _exploreDir(path8, depth) {
1213
2013
  let files;
1214
2014
  try {
1215
- files = await readdir(path6, this._rdOptions);
2015
+ files = await readdir(path8, this._rdOptions);
1216
2016
  } catch (error) {
1217
2017
  this._onError(error);
1218
2018
  }
1219
- return { files, depth, path: path6 };
2019
+ return { files, depth, path: path8 };
1220
2020
  }
1221
- async _formatEntry(dirent, path6) {
2021
+ async _formatEntry(dirent, path8) {
1222
2022
  let entry;
1223
- const basename4 = this._isDirent ? dirent.name : dirent;
2023
+ const basename5 = this._isDirent ? dirent.name : dirent;
1224
2024
  try {
1225
- const fullPath = presolve(pjoin(path6, basename4));
1226
- entry = { path: prelative(this._root, fullPath), fullPath, basename: basename4 };
2025
+ const fullPath = presolve(pjoin(path8, basename5));
2026
+ entry = { path: prelative(this._root, fullPath), fullPath, basename: basename5 };
1227
2027
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
1228
2028
  } catch (err) {
1229
2029
  this._onError(err);
@@ -1622,16 +2422,16 @@ var delFromSet = (main2, prop, item) => {
1622
2422
  };
1623
2423
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
1624
2424
  var FsWatchInstances = /* @__PURE__ */ new Map();
1625
- function createFsWatchInstance(path6, options, listener, errHandler, emitRaw) {
2425
+ function createFsWatchInstance(path8, options, listener, errHandler, emitRaw) {
1626
2426
  const handleEvent = (rawEvent, evPath) => {
1627
- listener(path6);
1628
- emitRaw(rawEvent, evPath, { watchedPath: path6 });
1629
- if (evPath && path6 !== evPath) {
1630
- fsWatchBroadcast(sysPath.resolve(path6, evPath), KEY_LISTENERS, sysPath.join(path6, evPath));
2427
+ listener(path8);
2428
+ emitRaw(rawEvent, evPath, { watchedPath: path8 });
2429
+ if (evPath && path8 !== evPath) {
2430
+ fsWatchBroadcast(sysPath.resolve(path8, evPath), KEY_LISTENERS, sysPath.join(path8, evPath));
1631
2431
  }
1632
2432
  };
1633
2433
  try {
1634
- return fs_watch(path6, {
2434
+ return fs_watch(path8, {
1635
2435
  persistent: options.persistent
1636
2436
  }, handleEvent);
1637
2437
  } catch (error) {
@@ -1647,12 +2447,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
1647
2447
  listener(val1, val2, val3);
1648
2448
  });
1649
2449
  };
1650
- var setFsWatchListener = (path6, fullPath, options, handlers) => {
2450
+ var setFsWatchListener = (path8, fullPath, options, handlers) => {
1651
2451
  const { listener, errHandler, rawEmitter } = handlers;
1652
2452
  let cont = FsWatchInstances.get(fullPath);
1653
2453
  let watcher;
1654
2454
  if (!options.persistent) {
1655
- watcher = createFsWatchInstance(path6, options, listener, errHandler, rawEmitter);
2455
+ watcher = createFsWatchInstance(path8, options, listener, errHandler, rawEmitter);
1656
2456
  if (!watcher)
1657
2457
  return;
1658
2458
  return watcher.close.bind(watcher);
@@ -1663,7 +2463,7 @@ var setFsWatchListener = (path6, fullPath, options, handlers) => {
1663
2463
  addAndConvert(cont, KEY_RAW, rawEmitter);
1664
2464
  } else {
1665
2465
  watcher = createFsWatchInstance(
1666
- path6,
2466
+ path8,
1667
2467
  options,
1668
2468
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
1669
2469
  errHandler,
@@ -1678,7 +2478,7 @@ var setFsWatchListener = (path6, fullPath, options, handlers) => {
1678
2478
  cont.watcherUnusable = true;
1679
2479
  if (isWindows && error.code === "EPERM") {
1680
2480
  try {
1681
- const fd = await open(path6, "r");
2481
+ const fd = await open(path8, "r");
1682
2482
  await fd.close();
1683
2483
  broadcastErr(error);
1684
2484
  } catch (err) {
@@ -1709,7 +2509,7 @@ var setFsWatchListener = (path6, fullPath, options, handlers) => {
1709
2509
  };
1710
2510
  };
1711
2511
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
1712
- var setFsWatchFileListener = (path6, fullPath, options, handlers) => {
2512
+ var setFsWatchFileListener = (path8, fullPath, options, handlers) => {
1713
2513
  const { listener, rawEmitter } = handlers;
1714
2514
  let cont = FsWatchFileInstances.get(fullPath);
1715
2515
  const copts = cont && cont.options;
@@ -1731,7 +2531,7 @@ var setFsWatchFileListener = (path6, fullPath, options, handlers) => {
1731
2531
  });
1732
2532
  const currmtime = curr.mtimeMs;
1733
2533
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
1734
- foreach(cont.listeners, (listener2) => listener2(path6, curr));
2534
+ foreach(cont.listeners, (listener2) => listener2(path8, curr));
1735
2535
  }
1736
2536
  })
1737
2537
  };
@@ -1759,13 +2559,13 @@ var NodeFsHandler = class {
1759
2559
  * @param listener on fs change
1760
2560
  * @returns closer for the watcher instance
1761
2561
  */
1762
- _watchWithNodeFs(path6, listener) {
2562
+ _watchWithNodeFs(path8, listener) {
1763
2563
  const opts = this.fsw.options;
1764
- const directory = sysPath.dirname(path6);
1765
- const basename4 = sysPath.basename(path6);
2564
+ const directory = sysPath.dirname(path8);
2565
+ const basename5 = sysPath.basename(path8);
1766
2566
  const parent = this.fsw._getWatchedDir(directory);
1767
- parent.add(basename4);
1768
- const absolutePath = sysPath.resolve(path6);
2567
+ parent.add(basename5);
2568
+ const absolutePath = sysPath.resolve(path8);
1769
2569
  const options = {
1770
2570
  persistent: opts.persistent
1771
2571
  };
@@ -1774,13 +2574,13 @@ var NodeFsHandler = class {
1774
2574
  let closer;
1775
2575
  if (opts.usePolling) {
1776
2576
  const enableBin = opts.interval !== opts.binaryInterval;
1777
- options.interval = enableBin && isBinaryPath(basename4) ? opts.binaryInterval : opts.interval;
1778
- closer = setFsWatchFileListener(path6, absolutePath, options, {
2577
+ options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
2578
+ closer = setFsWatchFileListener(path8, absolutePath, options, {
1779
2579
  listener,
1780
2580
  rawEmitter: this.fsw._emitRaw
1781
2581
  });
1782
2582
  } else {
1783
- closer = setFsWatchListener(path6, absolutePath, options, {
2583
+ closer = setFsWatchListener(path8, absolutePath, options, {
1784
2584
  listener,
1785
2585
  errHandler: this._boundHandleError,
1786
2586
  rawEmitter: this.fsw._emitRaw
@@ -1797,12 +2597,12 @@ var NodeFsHandler = class {
1797
2597
  return;
1798
2598
  }
1799
2599
  const dirname3 = sysPath.dirname(file);
1800
- const basename4 = sysPath.basename(file);
2600
+ const basename5 = sysPath.basename(file);
1801
2601
  const parent = this.fsw._getWatchedDir(dirname3);
1802
2602
  let prevStats = stats;
1803
- if (parent.has(basename4))
2603
+ if (parent.has(basename5))
1804
2604
  return;
1805
- const listener = async (path6, newStats) => {
2605
+ const listener = async (path8, newStats) => {
1806
2606
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
1807
2607
  return;
1808
2608
  if (!newStats || newStats.mtimeMs === 0) {
@@ -1816,18 +2616,18 @@ var NodeFsHandler = class {
1816
2616
  this.fsw._emit(EV.CHANGE, file, newStats2);
1817
2617
  }
1818
2618
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
1819
- this.fsw._closeFile(path6);
2619
+ this.fsw._closeFile(path8);
1820
2620
  prevStats = newStats2;
1821
2621
  const closer2 = this._watchWithNodeFs(file, listener);
1822
2622
  if (closer2)
1823
- this.fsw._addPathCloser(path6, closer2);
2623
+ this.fsw._addPathCloser(path8, closer2);
1824
2624
  } else {
1825
2625
  prevStats = newStats2;
1826
2626
  }
1827
2627
  } catch (error) {
1828
- this.fsw._remove(dirname3, basename4);
2628
+ this.fsw._remove(dirname3, basename5);
1829
2629
  }
1830
- } else if (parent.has(basename4)) {
2630
+ } else if (parent.has(basename5)) {
1831
2631
  const at = newStats.atimeMs;
1832
2632
  const mt = newStats.mtimeMs;
1833
2633
  if (!at || at <= mt || mt !== prevStats.mtimeMs) {
@@ -1852,7 +2652,7 @@ var NodeFsHandler = class {
1852
2652
  * @param item basename of this item
1853
2653
  * @returns true if no more processing is needed for this entry.
1854
2654
  */
1855
- async _handleSymlink(entry, directory, path6, item) {
2655
+ async _handleSymlink(entry, directory, path8, item) {
1856
2656
  if (this.fsw.closed) {
1857
2657
  return;
1858
2658
  }
@@ -1862,7 +2662,7 @@ var NodeFsHandler = class {
1862
2662
  this.fsw._incrReadyCount();
1863
2663
  let linkPath;
1864
2664
  try {
1865
- linkPath = await fsrealpath(path6);
2665
+ linkPath = await fsrealpath(path8);
1866
2666
  } catch (e) {
1867
2667
  this.fsw._emitReady();
1868
2668
  return true;
@@ -1872,12 +2672,12 @@ var NodeFsHandler = class {
1872
2672
  if (dir.has(item)) {
1873
2673
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
1874
2674
  this.fsw._symlinkPaths.set(full, linkPath);
1875
- this.fsw._emit(EV.CHANGE, path6, entry.stats);
2675
+ this.fsw._emit(EV.CHANGE, path8, entry.stats);
1876
2676
  }
1877
2677
  } else {
1878
2678
  dir.add(item);
1879
2679
  this.fsw._symlinkPaths.set(full, linkPath);
1880
- this.fsw._emit(EV.ADD, path6, entry.stats);
2680
+ this.fsw._emit(EV.ADD, path8, entry.stats);
1881
2681
  }
1882
2682
  this.fsw._emitReady();
1883
2683
  return true;
@@ -1906,9 +2706,9 @@ var NodeFsHandler = class {
1906
2706
  return;
1907
2707
  }
1908
2708
  const item = entry.path;
1909
- let path6 = sysPath.join(directory, item);
2709
+ let path8 = sysPath.join(directory, item);
1910
2710
  current.add(item);
1911
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path6, item)) {
2711
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path8, item)) {
1912
2712
  return;
1913
2713
  }
1914
2714
  if (this.fsw.closed) {
@@ -1917,11 +2717,11 @@ var NodeFsHandler = class {
1917
2717
  }
1918
2718
  if (item === target || !target && !previous.has(item)) {
1919
2719
  this.fsw._incrReadyCount();
1920
- path6 = sysPath.join(dir, sysPath.relative(dir, path6));
1921
- this._addToNodeFs(path6, initialAdd, wh, depth + 1);
2720
+ path8 = sysPath.join(dir, sysPath.relative(dir, path8));
2721
+ this._addToNodeFs(path8, initialAdd, wh, depth + 1);
1922
2722
  }
1923
2723
  }).on(EV.ERROR, this._boundHandleError);
1924
- return new Promise((resolve3, reject) => {
2724
+ return new Promise((resolve4, reject) => {
1925
2725
  if (!stream)
1926
2726
  return reject();
1927
2727
  stream.once(STR_END, () => {
@@ -1930,7 +2730,7 @@ var NodeFsHandler = class {
1930
2730
  return;
1931
2731
  }
1932
2732
  const wasThrottled = throttler ? throttler.clear() : false;
1933
- resolve3(void 0);
2733
+ resolve4(void 0);
1934
2734
  previous.getChildren().filter((item) => {
1935
2735
  return item !== directory && !current.has(item);
1936
2736
  }).forEach((item) => {
@@ -1987,13 +2787,13 @@ var NodeFsHandler = class {
1987
2787
  * @param depth Child path actually targeted for watch
1988
2788
  * @param target Child path actually targeted for watch
1989
2789
  */
1990
- async _addToNodeFs(path6, initialAdd, priorWh, depth, target) {
2790
+ async _addToNodeFs(path8, initialAdd, priorWh, depth, target) {
1991
2791
  const ready = this.fsw._emitReady;
1992
- if (this.fsw._isIgnored(path6) || this.fsw.closed) {
2792
+ if (this.fsw._isIgnored(path8) || this.fsw.closed) {
1993
2793
  ready();
1994
2794
  return false;
1995
2795
  }
1996
- const wh = this.fsw._getWatchHelpers(path6);
2796
+ const wh = this.fsw._getWatchHelpers(path8);
1997
2797
  if (priorWh) {
1998
2798
  wh.filterPath = (entry) => priorWh.filterPath(entry);
1999
2799
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -2009,8 +2809,8 @@ var NodeFsHandler = class {
2009
2809
  const follow = this.fsw.options.followSymlinks;
2010
2810
  let closer;
2011
2811
  if (stats.isDirectory()) {
2012
- const absPath = sysPath.resolve(path6);
2013
- const targetPath = follow ? await fsrealpath(path6) : path6;
2812
+ const absPath = sysPath.resolve(path8);
2813
+ const targetPath = follow ? await fsrealpath(path8) : path8;
2014
2814
  if (this.fsw.closed)
2015
2815
  return;
2016
2816
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -2020,29 +2820,29 @@ var NodeFsHandler = class {
2020
2820
  this.fsw._symlinkPaths.set(absPath, targetPath);
2021
2821
  }
2022
2822
  } else if (stats.isSymbolicLink()) {
2023
- const targetPath = follow ? await fsrealpath(path6) : path6;
2823
+ const targetPath = follow ? await fsrealpath(path8) : path8;
2024
2824
  if (this.fsw.closed)
2025
2825
  return;
2026
2826
  const parent = sysPath.dirname(wh.watchPath);
2027
2827
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
2028
2828
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
2029
- closer = await this._handleDir(parent, stats, initialAdd, depth, path6, wh, targetPath);
2829
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path8, wh, targetPath);
2030
2830
  if (this.fsw.closed)
2031
2831
  return;
2032
2832
  if (targetPath !== void 0) {
2033
- this.fsw._symlinkPaths.set(sysPath.resolve(path6), targetPath);
2833
+ this.fsw._symlinkPaths.set(sysPath.resolve(path8), targetPath);
2034
2834
  }
2035
2835
  } else {
2036
2836
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
2037
2837
  }
2038
2838
  ready();
2039
2839
  if (closer)
2040
- this.fsw._addPathCloser(path6, closer);
2840
+ this.fsw._addPathCloser(path8, closer);
2041
2841
  return false;
2042
2842
  } catch (error) {
2043
2843
  if (this.fsw._handleError(error)) {
2044
2844
  ready();
2045
- return path6;
2845
+ return path8;
2046
2846
  }
2047
2847
  }
2048
2848
  }
@@ -2085,26 +2885,26 @@ function createPattern(matcher) {
2085
2885
  }
2086
2886
  return () => false;
2087
2887
  }
2088
- function normalizePath(path6) {
2089
- if (typeof path6 !== "string")
2888
+ function normalizePath(path8) {
2889
+ if (typeof path8 !== "string")
2090
2890
  throw new Error("string expected");
2091
- path6 = sysPath2.normalize(path6);
2092
- path6 = path6.replace(/\\/g, "/");
2891
+ path8 = sysPath2.normalize(path8);
2892
+ path8 = path8.replace(/\\/g, "/");
2093
2893
  let prepend = false;
2094
- if (path6.startsWith("//"))
2894
+ if (path8.startsWith("//"))
2095
2895
  prepend = true;
2096
2896
  const DOUBLE_SLASH_RE2 = /\/\//;
2097
- while (path6.match(DOUBLE_SLASH_RE2))
2098
- path6 = path6.replace(DOUBLE_SLASH_RE2, "/");
2897
+ while (path8.match(DOUBLE_SLASH_RE2))
2898
+ path8 = path8.replace(DOUBLE_SLASH_RE2, "/");
2099
2899
  if (prepend)
2100
- path6 = "/" + path6;
2101
- return path6;
2900
+ path8 = "/" + path8;
2901
+ return path8;
2102
2902
  }
2103
2903
  function matchPatterns(patterns, testString, stats) {
2104
- const path6 = normalizePath(testString);
2904
+ const path8 = normalizePath(testString);
2105
2905
  for (let index = 0; index < patterns.length; index++) {
2106
2906
  const pattern = patterns[index];
2107
- if (pattern(path6, stats)) {
2907
+ if (pattern(path8, stats)) {
2108
2908
  return true;
2109
2909
  }
2110
2910
  }
@@ -2144,19 +2944,19 @@ var toUnix = (string) => {
2144
2944
  }
2145
2945
  return str;
2146
2946
  };
2147
- var normalizePathToUnix = (path6) => toUnix(sysPath2.normalize(toUnix(path6)));
2148
- var normalizeIgnored = (cwd = "") => (path6) => {
2149
- if (typeof path6 === "string") {
2150
- return normalizePathToUnix(sysPath2.isAbsolute(path6) ? path6 : sysPath2.join(cwd, path6));
2947
+ var normalizePathToUnix = (path8) => toUnix(sysPath2.normalize(toUnix(path8)));
2948
+ var normalizeIgnored = (cwd = "") => (path8) => {
2949
+ if (typeof path8 === "string") {
2950
+ return normalizePathToUnix(sysPath2.isAbsolute(path8) ? path8 : sysPath2.join(cwd, path8));
2151
2951
  } else {
2152
- return path6;
2952
+ return path8;
2153
2953
  }
2154
2954
  };
2155
- var getAbsolutePath = (path6, cwd) => {
2156
- if (sysPath2.isAbsolute(path6)) {
2157
- return path6;
2955
+ var getAbsolutePath = (path8, cwd) => {
2956
+ if (sysPath2.isAbsolute(path8)) {
2957
+ return path8;
2158
2958
  }
2159
- return sysPath2.join(cwd, path6);
2959
+ return sysPath2.join(cwd, path8);
2160
2960
  };
2161
2961
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
2162
2962
  var DirEntry = class {
@@ -2211,10 +3011,10 @@ var DirEntry = class {
2211
3011
  var STAT_METHOD_F = "stat";
2212
3012
  var STAT_METHOD_L = "lstat";
2213
3013
  var WatchHelper = class {
2214
- constructor(path6, follow, fsw) {
3014
+ constructor(path8, follow, fsw) {
2215
3015
  this.fsw = fsw;
2216
- const watchPath = path6;
2217
- this.path = path6 = path6.replace(REPLACER_RE, "");
3016
+ const watchPath = path8;
3017
+ this.path = path8 = path8.replace(REPLACER_RE, "");
2218
3018
  this.watchPath = watchPath;
2219
3019
  this.fullWatchPath = sysPath2.resolve(watchPath);
2220
3020
  this.dirParts = [];
@@ -2336,20 +3136,20 @@ var FSWatcher = class extends EventEmitter {
2336
3136
  this._closePromise = void 0;
2337
3137
  let paths = unifyPaths(paths_);
2338
3138
  if (cwd) {
2339
- paths = paths.map((path6) => {
2340
- const absPath = getAbsolutePath(path6, cwd);
3139
+ paths = paths.map((path8) => {
3140
+ const absPath = getAbsolutePath(path8, cwd);
2341
3141
  return absPath;
2342
3142
  });
2343
3143
  }
2344
- paths.forEach((path6) => {
2345
- this._removeIgnoredPath(path6);
3144
+ paths.forEach((path8) => {
3145
+ this._removeIgnoredPath(path8);
2346
3146
  });
2347
3147
  this._userIgnored = void 0;
2348
3148
  if (!this._readyCount)
2349
3149
  this._readyCount = 0;
2350
3150
  this._readyCount += paths.length;
2351
- Promise.all(paths.map(async (path6) => {
2352
- const res = await this._nodeFsHandler._addToNodeFs(path6, !_internal, void 0, 0, _origAdd);
3151
+ Promise.all(paths.map(async (path8) => {
3152
+ const res = await this._nodeFsHandler._addToNodeFs(path8, !_internal, void 0, 0, _origAdd);
2353
3153
  if (res)
2354
3154
  this._emitReady();
2355
3155
  return res;
@@ -2371,17 +3171,17 @@ var FSWatcher = class extends EventEmitter {
2371
3171
  return this;
2372
3172
  const paths = unifyPaths(paths_);
2373
3173
  const { cwd } = this.options;
2374
- paths.forEach((path6) => {
2375
- if (!sysPath2.isAbsolute(path6) && !this._closers.has(path6)) {
3174
+ paths.forEach((path8) => {
3175
+ if (!sysPath2.isAbsolute(path8) && !this._closers.has(path8)) {
2376
3176
  if (cwd)
2377
- path6 = sysPath2.join(cwd, path6);
2378
- path6 = sysPath2.resolve(path6);
3177
+ path8 = sysPath2.join(cwd, path8);
3178
+ path8 = sysPath2.resolve(path8);
2379
3179
  }
2380
- this._closePath(path6);
2381
- this._addIgnoredPath(path6);
2382
- if (this._watched.has(path6)) {
3180
+ this._closePath(path8);
3181
+ this._addIgnoredPath(path8);
3182
+ if (this._watched.has(path8)) {
2383
3183
  this._addIgnoredPath({
2384
- path: path6,
3184
+ path: path8,
2385
3185
  recursive: true
2386
3186
  });
2387
3187
  }
@@ -2445,38 +3245,38 @@ var FSWatcher = class extends EventEmitter {
2445
3245
  * @param stats arguments to be passed with event
2446
3246
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
2447
3247
  */
2448
- async _emit(event, path6, stats) {
3248
+ async _emit(event, path8, stats) {
2449
3249
  if (this.closed)
2450
3250
  return;
2451
3251
  const opts = this.options;
2452
3252
  if (isWindows)
2453
- path6 = sysPath2.normalize(path6);
3253
+ path8 = sysPath2.normalize(path8);
2454
3254
  if (opts.cwd)
2455
- path6 = sysPath2.relative(opts.cwd, path6);
2456
- const args = [path6];
3255
+ path8 = sysPath2.relative(opts.cwd, path8);
3256
+ const args = [path8];
2457
3257
  if (stats != null)
2458
3258
  args.push(stats);
2459
3259
  const awf = opts.awaitWriteFinish;
2460
3260
  let pw;
2461
- if (awf && (pw = this._pendingWrites.get(path6))) {
3261
+ if (awf && (pw = this._pendingWrites.get(path8))) {
2462
3262
  pw.lastChange = /* @__PURE__ */ new Date();
2463
3263
  return this;
2464
3264
  }
2465
3265
  if (opts.atomic) {
2466
3266
  if (event === EVENTS.UNLINK) {
2467
- this._pendingUnlinks.set(path6, [event, ...args]);
3267
+ this._pendingUnlinks.set(path8, [event, ...args]);
2468
3268
  setTimeout(() => {
2469
- this._pendingUnlinks.forEach((entry, path7) => {
3269
+ this._pendingUnlinks.forEach((entry, path9) => {
2470
3270
  this.emit(...entry);
2471
3271
  this.emit(EVENTS.ALL, ...entry);
2472
- this._pendingUnlinks.delete(path7);
3272
+ this._pendingUnlinks.delete(path9);
2473
3273
  });
2474
3274
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
2475
3275
  return this;
2476
3276
  }
2477
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path6)) {
3277
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path8)) {
2478
3278
  event = EVENTS.CHANGE;
2479
- this._pendingUnlinks.delete(path6);
3279
+ this._pendingUnlinks.delete(path8);
2480
3280
  }
2481
3281
  }
2482
3282
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -2494,16 +3294,16 @@ var FSWatcher = class extends EventEmitter {
2494
3294
  this.emitWithAll(event, args);
2495
3295
  }
2496
3296
  };
2497
- this._awaitWriteFinish(path6, awf.stabilityThreshold, event, awfEmit);
3297
+ this._awaitWriteFinish(path8, awf.stabilityThreshold, event, awfEmit);
2498
3298
  return this;
2499
3299
  }
2500
3300
  if (event === EVENTS.CHANGE) {
2501
- const isThrottled = !this._throttle(EVENTS.CHANGE, path6, 50);
3301
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path8, 50);
2502
3302
  if (isThrottled)
2503
3303
  return this;
2504
3304
  }
2505
3305
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
2506
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path6) : path6;
3306
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path8) : path8;
2507
3307
  let stats2;
2508
3308
  try {
2509
3309
  stats2 = await stat3(fullPath);
@@ -2534,23 +3334,23 @@ var FSWatcher = class extends EventEmitter {
2534
3334
  * @param timeout duration of time to suppress duplicate actions
2535
3335
  * @returns tracking object or false if action should be suppressed
2536
3336
  */
2537
- _throttle(actionType, path6, timeout) {
3337
+ _throttle(actionType, path8, timeout) {
2538
3338
  if (!this._throttled.has(actionType)) {
2539
3339
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
2540
3340
  }
2541
3341
  const action = this._throttled.get(actionType);
2542
3342
  if (!action)
2543
3343
  throw new Error("invalid throttle");
2544
- const actionPath = action.get(path6);
3344
+ const actionPath = action.get(path8);
2545
3345
  if (actionPath) {
2546
3346
  actionPath.count++;
2547
3347
  return false;
2548
3348
  }
2549
3349
  let timeoutObject;
2550
3350
  const clear = () => {
2551
- const item = action.get(path6);
3351
+ const item = action.get(path8);
2552
3352
  const count = item ? item.count : 0;
2553
- action.delete(path6);
3353
+ action.delete(path8);
2554
3354
  clearTimeout(timeoutObject);
2555
3355
  if (item)
2556
3356
  clearTimeout(item.timeoutObject);
@@ -2558,7 +3358,7 @@ var FSWatcher = class extends EventEmitter {
2558
3358
  };
2559
3359
  timeoutObject = setTimeout(clear, timeout);
2560
3360
  const thr = { timeoutObject, clear, count: 0 };
2561
- action.set(path6, thr);
3361
+ action.set(path8, thr);
2562
3362
  return thr;
2563
3363
  }
2564
3364
  _incrReadyCount() {
@@ -2572,44 +3372,44 @@ var FSWatcher = class extends EventEmitter {
2572
3372
  * @param event
2573
3373
  * @param awfEmit Callback to be called when ready for event to be emitted.
2574
3374
  */
2575
- _awaitWriteFinish(path6, threshold, event, awfEmit) {
3375
+ _awaitWriteFinish(path8, threshold, event, awfEmit) {
2576
3376
  const awf = this.options.awaitWriteFinish;
2577
3377
  if (typeof awf !== "object")
2578
3378
  return;
2579
3379
  const pollInterval = awf.pollInterval;
2580
3380
  let timeoutHandler;
2581
- let fullPath = path6;
2582
- if (this.options.cwd && !sysPath2.isAbsolute(path6)) {
2583
- fullPath = sysPath2.join(this.options.cwd, path6);
3381
+ let fullPath = path8;
3382
+ if (this.options.cwd && !sysPath2.isAbsolute(path8)) {
3383
+ fullPath = sysPath2.join(this.options.cwd, path8);
2584
3384
  }
2585
3385
  const now = /* @__PURE__ */ new Date();
2586
3386
  const writes = this._pendingWrites;
2587
3387
  function awaitWriteFinishFn(prevStat) {
2588
3388
  statcb(fullPath, (err, curStat) => {
2589
- if (err || !writes.has(path6)) {
3389
+ if (err || !writes.has(path8)) {
2590
3390
  if (err && err.code !== "ENOENT")
2591
3391
  awfEmit(err);
2592
3392
  return;
2593
3393
  }
2594
3394
  const now2 = Number(/* @__PURE__ */ new Date());
2595
3395
  if (prevStat && curStat.size !== prevStat.size) {
2596
- writes.get(path6).lastChange = now2;
3396
+ writes.get(path8).lastChange = now2;
2597
3397
  }
2598
- const pw = writes.get(path6);
3398
+ const pw = writes.get(path8);
2599
3399
  const df = now2 - pw.lastChange;
2600
3400
  if (df >= threshold) {
2601
- writes.delete(path6);
3401
+ writes.delete(path8);
2602
3402
  awfEmit(void 0, curStat);
2603
3403
  } else {
2604
3404
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
2605
3405
  }
2606
3406
  });
2607
3407
  }
2608
- if (!writes.has(path6)) {
2609
- writes.set(path6, {
3408
+ if (!writes.has(path8)) {
3409
+ writes.set(path8, {
2610
3410
  lastChange: now,
2611
3411
  cancelWait: () => {
2612
- writes.delete(path6);
3412
+ writes.delete(path8);
2613
3413
  clearTimeout(timeoutHandler);
2614
3414
  return event;
2615
3415
  }
@@ -2620,8 +3420,8 @@ var FSWatcher = class extends EventEmitter {
2620
3420
  /**
2621
3421
  * Determines whether user has asked to ignore this path.
2622
3422
  */
2623
- _isIgnored(path6, stats) {
2624
- if (this.options.atomic && DOT_RE.test(path6))
3423
+ _isIgnored(path8, stats) {
3424
+ if (this.options.atomic && DOT_RE.test(path8))
2625
3425
  return true;
2626
3426
  if (!this._userIgnored) {
2627
3427
  const { cwd } = this.options;
@@ -2631,17 +3431,17 @@ var FSWatcher = class extends EventEmitter {
2631
3431
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
2632
3432
  this._userIgnored = anymatch(list, void 0);
2633
3433
  }
2634
- return this._userIgnored(path6, stats);
3434
+ return this._userIgnored(path8, stats);
2635
3435
  }
2636
- _isntIgnored(path6, stat4) {
2637
- return !this._isIgnored(path6, stat4);
3436
+ _isntIgnored(path8, stat4) {
3437
+ return !this._isIgnored(path8, stat4);
2638
3438
  }
2639
3439
  /**
2640
3440
  * Provides a set of common helpers and properties relating to symlink handling.
2641
3441
  * @param path file or directory pattern being watched
2642
3442
  */
2643
- _getWatchHelpers(path6) {
2644
- return new WatchHelper(path6, this.options.followSymlinks, this);
3443
+ _getWatchHelpers(path8) {
3444
+ return new WatchHelper(path8, this.options.followSymlinks, this);
2645
3445
  }
2646
3446
  // Directory helpers
2647
3447
  // -----------------
@@ -2673,63 +3473,63 @@ var FSWatcher = class extends EventEmitter {
2673
3473
  * @param item base path of item/directory
2674
3474
  */
2675
3475
  _remove(directory, item, isDirectory) {
2676
- const path6 = sysPath2.join(directory, item);
2677
- const fullPath = sysPath2.resolve(path6);
2678
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path6) || this._watched.has(fullPath);
2679
- if (!this._throttle("remove", path6, 100))
3476
+ const path8 = sysPath2.join(directory, item);
3477
+ const fullPath = sysPath2.resolve(path8);
3478
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path8) || this._watched.has(fullPath);
3479
+ if (!this._throttle("remove", path8, 100))
2680
3480
  return;
2681
3481
  if (!isDirectory && this._watched.size === 1) {
2682
3482
  this.add(directory, item, true);
2683
3483
  }
2684
- const wp = this._getWatchedDir(path6);
3484
+ const wp = this._getWatchedDir(path8);
2685
3485
  const nestedDirectoryChildren = wp.getChildren();
2686
- nestedDirectoryChildren.forEach((nested) => this._remove(path6, nested));
3486
+ nestedDirectoryChildren.forEach((nested) => this._remove(path8, nested));
2687
3487
  const parent = this._getWatchedDir(directory);
2688
3488
  const wasTracked = parent.has(item);
2689
3489
  parent.remove(item);
2690
3490
  if (this._symlinkPaths.has(fullPath)) {
2691
3491
  this._symlinkPaths.delete(fullPath);
2692
3492
  }
2693
- let relPath = path6;
3493
+ let relPath = path8;
2694
3494
  if (this.options.cwd)
2695
- relPath = sysPath2.relative(this.options.cwd, path6);
3495
+ relPath = sysPath2.relative(this.options.cwd, path8);
2696
3496
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
2697
3497
  const event = this._pendingWrites.get(relPath).cancelWait();
2698
3498
  if (event === EVENTS.ADD)
2699
3499
  return;
2700
3500
  }
2701
- this._watched.delete(path6);
3501
+ this._watched.delete(path8);
2702
3502
  this._watched.delete(fullPath);
2703
3503
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
2704
- if (wasTracked && !this._isIgnored(path6))
2705
- this._emit(eventName, path6);
2706
- this._closePath(path6);
3504
+ if (wasTracked && !this._isIgnored(path8))
3505
+ this._emit(eventName, path8);
3506
+ this._closePath(path8);
2707
3507
  }
2708
3508
  /**
2709
3509
  * Closes all watchers for a path
2710
3510
  */
2711
- _closePath(path6) {
2712
- this._closeFile(path6);
2713
- const dir = sysPath2.dirname(path6);
2714
- this._getWatchedDir(dir).remove(sysPath2.basename(path6));
3511
+ _closePath(path8) {
3512
+ this._closeFile(path8);
3513
+ const dir = sysPath2.dirname(path8);
3514
+ this._getWatchedDir(dir).remove(sysPath2.basename(path8));
2715
3515
  }
2716
3516
  /**
2717
3517
  * Closes only file-specific watchers
2718
3518
  */
2719
- _closeFile(path6) {
2720
- const closers = this._closers.get(path6);
3519
+ _closeFile(path8) {
3520
+ const closers = this._closers.get(path8);
2721
3521
  if (!closers)
2722
3522
  return;
2723
3523
  closers.forEach((closer) => closer());
2724
- this._closers.delete(path6);
3524
+ this._closers.delete(path8);
2725
3525
  }
2726
- _addPathCloser(path6, closer) {
3526
+ _addPathCloser(path8, closer) {
2727
3527
  if (!closer)
2728
3528
  return;
2729
- let list = this._closers.get(path6);
3529
+ let list = this._closers.get(path8);
2730
3530
  if (!list) {
2731
3531
  list = [];
2732
- this._closers.set(path6, list);
3532
+ this._closers.set(path8, list);
2733
3533
  }
2734
3534
  list.push(closer);
2735
3535
  }
@@ -2760,56 +3560,150 @@ var esm_default = { watch, FSWatcher };
2760
3560
 
2761
3561
  // src/watch.ts
2762
3562
  var DEBOUNCE_MS = 1200;
3563
+ var LIMIT_REFRESH_MS = 6e4;
3564
+ var FALLBACK_SCAN_MS = 5e3;
3565
+ var DEVICE_HEARTBEAT_MS = 60 * 6e4;
3566
+ var CLOUD_SYNC_INTERVAL_MS = 30 * 6e4;
3567
+ var MAX_CLOUD_WRITES_PER_DAY = 16;
3568
+ var QUOTA_RETRY_MS = 15 * 6e4;
3569
+ var QUOTA_RETRY_JITTER_MS = 5 * 6e4;
2763
3570
  async function watch2(runner) {
2764
- await safeSync(runner, "startup");
3571
+ const startupRetryAfter = await safeSync(runner, {
3572
+ reason: "startup",
3573
+ refreshLimits: true,
3574
+ forceDeviceHeartbeat: true,
3575
+ deviceHeartbeatMs: DEVICE_HEARTBEAT_MS,
3576
+ cloudSyncIntervalMs: CLOUD_SYNC_INTERVAL_MS,
3577
+ maxCloudWritesPerDay: MAX_CLOUD_WRITES_PER_DAY
3578
+ });
2765
3579
  const paths = runner.watchPaths();
2766
3580
  const watcher = esm_default.watch(paths, {
2767
3581
  ignoreInitial: true,
2768
3582
  awaitWriteFinish: { stabilityThreshold: 400, pollInterval: 100 },
2769
- ignored: (p) => !(p.endsWith(".jsonl") || !p.includes("."))
3583
+ // Never infer "file" from dots in the absolute path: the provider roots
3584
+ // themselves are named .claude and .codex on every supported platform.
3585
+ ignored: (p, stats) => Boolean(stats?.isFile() && !isUsageFile(p))
2770
3586
  });
2771
3587
  let timer;
2772
- let pending = false;
3588
+ let pending;
2773
3589
  let running = false;
2774
- const schedule = () => {
3590
+ let scheduledAt = 0;
3591
+ let blockedUntil = startupRetryAfter ? Date.now() + startupRetryAfter : 0;
3592
+ const schedule = (request, delay = DEBOUNCE_MS) => {
3593
+ pending = mergeRequests(pending, request);
3594
+ if (running) return;
3595
+ const target = Math.max(Date.now() + delay, blockedUntil);
3596
+ if (timer && scheduledAt <= target) return;
2775
3597
  if (timer) clearTimeout(timer);
3598
+ scheduledAt = target;
2776
3599
  timer = setTimeout(async () => {
2777
- if (running) {
2778
- pending = true;
2779
- return;
2780
- }
3600
+ timer = void 0;
3601
+ scheduledAt = 0;
3602
+ const next = pending;
3603
+ pending = void 0;
3604
+ if (!next) return;
2781
3605
  running = true;
2782
- await safeSync(runner, "change");
3606
+ const retryAfterMs = await safeSync(runner, next);
2783
3607
  running = false;
3608
+ if (retryAfterMs) {
3609
+ blockedUntil = Date.now() + retryAfterMs;
3610
+ pending = mergeRequests(pending, next);
3611
+ } else {
3612
+ blockedUntil = 0;
3613
+ }
2784
3614
  if (pending) {
2785
- pending = false;
2786
- schedule();
3615
+ const queued = pending;
3616
+ pending = void 0;
3617
+ schedule(queued, 0);
2787
3618
  }
2788
- }, DEBOUNCE_MS);
3619
+ }, Math.max(0, target - Date.now()));
2789
3620
  };
2790
- watcher.on("add", schedule).on("change", schedule);
3621
+ const turnRequest = (reason) => ({
3622
+ reason,
3623
+ refreshLimits: false,
3624
+ deviceHeartbeatMs: DEVICE_HEARTBEAT_MS,
3625
+ cloudSyncIntervalMs: CLOUD_SYNC_INTERVAL_MS,
3626
+ maxCloudWritesPerDay: MAX_CLOUD_WRITES_PER_DAY
3627
+ });
3628
+ watcher.on("add", () => schedule(turnRequest("change"))).on("change", () => schedule(turnRequest("change")));
3629
+ const fallbackTimer = setInterval(() => schedule(turnRequest("fallback"), 0), FALLBACK_SCAN_MS);
3630
+ const refreshTimer = setInterval(
3631
+ () => schedule(
3632
+ {
3633
+ reason: "limits",
3634
+ refreshLimits: true,
3635
+ deviceHeartbeatMs: DEVICE_HEARTBEAT_MS,
3636
+ cloudSyncIntervalMs: CLOUD_SYNC_INTERVAL_MS,
3637
+ maxCloudWritesPerDay: MAX_CLOUD_WRITES_PER_DAY
3638
+ },
3639
+ 0
3640
+ ),
3641
+ LIMIT_REFRESH_MS
3642
+ );
2791
3643
  console.log(`Watching for usage in:
2792
3644
  ${paths.join("\n ")}
2793
3645
  Press Ctrl+C to stop.`);
2794
- await new Promise((resolve3) => {
3646
+ await new Promise((resolve4) => {
2795
3647
  process.on("SIGINT", () => {
2796
- void watcher.close().then(resolve3);
3648
+ clearInterval(refreshTimer);
3649
+ clearInterval(fallbackTimer);
3650
+ if (timer) clearTimeout(timer);
3651
+ void watcher.close().then(resolve4);
2797
3652
  });
2798
3653
  });
2799
3654
  }
2800
- async function safeSync(runner, reason) {
3655
+ async function safeSync(runner, request) {
2801
3656
  try {
2802
- const { newTurns, byProvider } = await runner.syncOnce();
2803
- if (newTurns > 0) {
3657
+ const { reason, ...options } = request;
3658
+ const { processedTurns, byProvider, published, cloudWritesToday } = await runner.syncOnce(options);
3659
+ if (processedTurns > 0) {
2804
3660
  const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ");
2805
- console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] synced ${newTurns} new turn(s) (${detail})`);
3661
+ console.log(
3662
+ `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] processed ${processedTurns} turn(s) locally (${detail}); ` + (published ? `cloud snapshot published (${cloudWritesToday}/16 today)` : "cloud snapshot queued")
3663
+ );
2806
3664
  } else if (reason === "startup") {
2807
- console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] up to date`);
3665
+ console.log(
3666
+ `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] local snapshot ready; ` + (published ? `cloud snapshot published (${cloudWritesToday}/16 today)` : "cloud snapshot queued")
3667
+ );
2808
3668
  }
2809
3669
  } catch (err) {
3670
+ if (err instanceof FirestoreRestError && err.status === 429) {
3671
+ const retryAfter = Math.max(
3672
+ err.retryAfterMs ?? 0,
3673
+ QUOTA_RETRY_MS + Math.floor(Math.random() * QUOTA_RETRY_JITTER_MS)
3674
+ );
3675
+ console.error(
3676
+ `Cloud sync paused: Firestore quota is exhausted; the local snapshot is safe. Retrying in ${Math.ceil(retryAfter / 6e4)}m.`
3677
+ );
3678
+ return retryAfter;
3679
+ }
2810
3680
  console.error(`Sync error: ${err.message}`);
3681
+ return 15e3;
2811
3682
  }
2812
3683
  }
3684
+ function mergeRequests(current, incoming) {
3685
+ if (!current) return incoming;
3686
+ return {
3687
+ reason: incoming.reason === "change" || current.reason === "change" ? "change" : incoming.reason,
3688
+ refreshLimits: Boolean(current.refreshLimits || incoming.refreshLimits),
3689
+ forceDeviceHeartbeat: Boolean(current.forceDeviceHeartbeat || incoming.forceDeviceHeartbeat),
3690
+ deviceHeartbeatMs: Math.min(
3691
+ current.deviceHeartbeatMs ?? DEVICE_HEARTBEAT_MS,
3692
+ incoming.deviceHeartbeatMs ?? DEVICE_HEARTBEAT_MS
3693
+ ),
3694
+ cloudSyncIntervalMs: Math.min(
3695
+ current.cloudSyncIntervalMs ?? CLOUD_SYNC_INTERVAL_MS,
3696
+ incoming.cloudSyncIntervalMs ?? CLOUD_SYNC_INTERVAL_MS
3697
+ ),
3698
+ maxCloudWritesPerDay: Math.min(
3699
+ current.maxCloudWritesPerDay ?? MAX_CLOUD_WRITES_PER_DAY,
3700
+ incoming.maxCloudWritesPerDay ?? MAX_CLOUD_WRITES_PER_DAY
3701
+ )
3702
+ };
3703
+ }
3704
+ function isUsageFile(filePath) {
3705
+ return filePath.endsWith(".jsonl") || filePath.endsWith(".sqlite") || filePath.endsWith(".sqlite-wal");
3706
+ }
2813
3707
 
2814
3708
  // src/cli.ts
2815
3709
  var USAGE = `Tokelytics agent
@@ -2817,7 +3711,7 @@ var USAGE = `Tokelytics agent
2817
3711
  Usage:
2818
3712
  tokelytics login Sign in by approving in your browser
2819
3713
  tokelytics sync Run one incremental sync to your dashboard
2820
- tokelytics watch Stream usage in realtime (filesystem watcher)
3714
+ tokelytics watch Watch usage and refresh the cloud snapshot
2821
3715
  tokelytics status Show current sign-in and device
2822
3716
  tokelytics logout Forget stored credentials
2823
3717
  `;
@@ -2838,9 +3732,9 @@ async function main(argv) {
2838
3732
  }
2839
3733
  case "sync": {
2840
3734
  const runner = await createRunner();
2841
- const { newTurns, byProvider } = await runner.syncOnce();
3735
+ const { processedTurns, processedLimits, byProvider } = await runner.syncOnce();
2842
3736
  const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ") || "none";
2843
- console.log(`Synced ${newTurns} new turn(s) (${detail}).`);
3737
+ console.log(`Processed ${processedTurns} turn(s) (${detail}); refreshed ${processedLimits} usage provider(s).`);
2844
3738
  return 0;
2845
3739
  }
2846
3740
  case "watch": {