tokelytics 0.2.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 +1255 -438
  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,124 +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
- var TIMELINE_BUCKET_MS = 5 * 60 * 1e3;
151
- var TIMELINE_RETENTION_MS = 8 * 24 * 60 * 60 * 1e3;
152
- function bucketStartMs(ts) {
153
- const ms = Date.parse(ts);
154
- if (!Number.isFinite(ms))
155
- return null;
156
- return Math.floor(ms / TIMELINE_BUCKET_MS) * TIMELINE_BUCKET_MS;
157
- }
158
- function dayOfMs(t) {
159
- return new Date(t).toISOString().slice(0, 10);
160
- }
161
- function buildTimelineBuckets(turns) {
162
- const map = /* @__PURE__ */ new Map();
163
- for (const t of turns) {
164
- const start = bucketStartMs(t.ts);
165
- if (start === null)
166
- continue;
167
- const bucket = map.get(start) ?? { t: start, models: {} };
168
- const key = t.model || "unknown";
169
- const m = bucket.models[key] ??= { i: 0, o: 0, cr: 0, cc: 0, ci: 0, rs: 0 };
170
- m.i += t.inputTokens;
171
- m.o += t.outputTokens;
172
- m.cr += t.cacheReadTokens;
173
- m.cc += t.cacheCreationTokens;
174
- m.ci += t.cachedInputTokens;
175
- m.rs += t.reasoningTokens;
176
- map.set(start, bucket);
177
- }
178
- return [...map.values()].sort((a, b) => a.t - b.t);
179
- }
180
- function mergeTimeline(existing, recomputed, affectedDays, nowMs) {
181
- const cutoff = nowMs - TIMELINE_RETENTION_MS;
182
- const kept = existing.filter((b) => b.t >= cutoff && !affectedDays.has(dayOfMs(b.t)));
183
- const fresh = recomputed.filter((b) => b.t >= cutoff);
184
- return [...kept, ...fresh].sort((a, b) => a.t - b.t);
185
- }
186
- function affectedKeys(turns) {
187
- const sessionIds = /* @__PURE__ */ new Set();
188
- const dayBuckets = /* @__PURE__ */ new Map();
189
- for (const t of turns) {
190
- sessionIds.add(t.sessionId);
191
- const day = dayOf(t.ts);
192
- if (day)
193
- dayBuckets.set(rollupId(t.provider, day), { provider: t.provider, day });
194
- }
195
- 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()];
196
196
  }
197
197
 
198
198
  // ../packages/core/dist/providers/claude.js
199
199
  import * as os from "node:os";
200
- import * as path2 from "node:path";
200
+ import * as path3 from "node:path";
201
201
 
202
202
  // ../packages/core/dist/fs-scan.js
203
203
  import { promises as fs } from "node:fs";
@@ -261,13 +261,144 @@ async function readJsonlFrom(filePath, fromLine) {
261
261
  return { totalLines: lines.length, entries };
262
262
  }
263
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
+
264
395
  // ../packages/core/dist/providers/claude.js
265
396
  var MTIME_EPSILON_MS = 1;
266
397
  function defaultClaudeDirs(home = os.homedir()) {
267
398
  return [
268
- path2.join(home, ".claude", "projects"),
399
+ path3.join(home, ".claude", "projects"),
269
400
  // Xcode coding-assistant integration (macOS); silently skipped if absent.
270
- path2.join(home, "Library", "Developer", "Xcode", "CodingAssistant", "ClaudeAgentConfig", "projects")
401
+ path3.join(home, "Library", "Developer", "Xcode", "CodingAssistant", "ClaudeAgentConfig", "projects")
271
402
  ];
272
403
  }
273
404
  function parseClaudeEntries(entries) {
@@ -292,14 +423,20 @@ function parseClaudeEntries(entries) {
292
423
  if (inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens === 0)
293
424
  continue;
294
425
  let toolName = null;
426
+ const fileReads = [];
427
+ const cwd = rec.cwd ?? "";
295
428
  for (const item of msg.content ?? []) {
296
429
  if (item && typeof item === "object" && item.type === "tool_use") {
297
- toolName = item.name ?? null;
298
- 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
+ }
299
437
  }
300
438
  }
301
439
  const messageId = msg.id ?? "";
302
- const cwd = rec.cwd ?? "";
303
440
  const turn = {
304
441
  provider: "claude",
305
442
  turnId: messageId ? `claude:${messageId}` : `claude:noid:${sessionId}:${lineNo}`,
@@ -313,6 +450,7 @@ function parseClaudeEntries(entries) {
313
450
  cachedInputTokens: 0,
314
451
  reasoningTokens: 0,
315
452
  toolName,
453
+ fileReads,
316
454
  project: projectNameFromCwd(cwd),
317
455
  cwd,
318
456
  gitBranch: rec.gitBranch ?? ""
@@ -324,6 +462,63 @@ function parseClaudeEntries(entries) {
324
462
  }
325
463
  return [...noId, ...seen.values()];
326
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
+ }
327
522
  var ClaudeConnector = class {
328
523
  id = "claude";
329
524
  dirs;
@@ -336,6 +531,7 @@ var ClaudeConnector = class {
336
531
  async collect(state) {
337
532
  const nextFiles = { ...state.files };
338
533
  const turns = [];
534
+ const scannedEntries = [];
339
535
  for (const dir of this.dirs) {
340
536
  const files = await walkFiles(dir, (name) => name.endsWith(".jsonl"));
341
537
  for (const filePath of files) {
@@ -348,22 +544,30 @@ var ClaudeConnector = class {
348
544
  }
349
545
  const fromLine = cursor ? cursor.lines : 0;
350
546
  const { totalLines, entries } = await readJsonlFrom(filePath, fromLine);
547
+ scannedEntries.push(...entries);
351
548
  if (entries.length > 0) {
352
549
  turns.push(...parseClaudeEntries(entries));
353
550
  }
354
551
  nextFiles[filePath] = { mtimeMs, lines: totalLines };
355
552
  }
356
553
  }
357
- return { turns, state: { files: nextFiles } };
554
+ return { turns, limits: parseClaudeLimitSnapshots(scannedEntries), state: { files: nextFiles } };
358
555
  }
359
556
  };
360
557
 
361
558
  // ../packages/core/dist/providers/codex.js
559
+ import { promises as fs2 } from "node:fs";
362
560
  import * as os2 from "node:os";
363
- import * as path3 from "node:path";
561
+ import * as path4 from "node:path";
364
562
  var MTIME_EPSILON_MS2 = 1;
563
+ var RATE_LIMIT_REFRESH_MS = 6e4;
564
+ var MAX_RATE_LIMIT_LOG_BYTES = 16 * 1024 * 1024;
365
565
  function defaultCodexDirs(home = os2.homedir()) {
366
- 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")];
367
571
  }
368
572
  function usageFromObj(obj) {
369
573
  if (!obj || typeof obj !== "object")
@@ -452,8 +656,29 @@ function parseCodexEntries(fileId, entries) {
452
656
  let prevCumulative = null;
453
657
  let seq = 0;
454
658
  let runningModel = meta.model;
659
+ let pendingReads = [];
660
+ let pendingToolName = null;
661
+ const readsByCall = /* @__PURE__ */ new Map();
455
662
  for (const { record } of entries) {
456
- 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);
457
682
  if (recModel)
458
683
  runningModel = recModel;
459
684
  const ev = extractUsageEvent(record);
@@ -489,26 +714,148 @@ function parseCodexEntries(fileId, entries) {
489
714
  cacheCreationTokens: 0,
490
715
  cachedInputTokens: delta.cachedInput,
491
716
  reasoningTokens: delta.reasoning,
492
- toolName: null,
717
+ toolName: pendingToolName,
718
+ fileReads: pendingReads,
493
719
  project: meta.project,
494
720
  cwd: meta.cwd,
495
721
  gitBranch: meta.gitBranch
496
722
  });
723
+ pendingReads = [];
724
+ pendingToolName = null;
725
+ readsByCall.clear();
497
726
  seq++;
498
727
  }
499
728
  return turns;
500
729
  }
501
730
  function codexFileId(filePath) {
502
- 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] : [];
503
846
  }
504
847
  var CodexConnector = class {
505
848
  id = "codex";
506
849
  dirs;
507
- constructor(dirs = defaultCodexDirs()) {
850
+ rateLimitFiles;
851
+ cachedLimits = [];
852
+ nextLimitReadAt = 0;
853
+ constructor(dirs = defaultCodexDirs(), rateLimitFiles = defaultCodexRateLimitFiles()) {
508
854
  this.dirs = dirs;
855
+ this.rateLimitFiles = rateLimitFiles;
509
856
  }
510
857
  watchPaths() {
511
- return this.dirs;
858
+ return [...this.dirs, ...this.rateLimitFiles];
512
859
  }
513
860
  async collect(state) {
514
861
  const nextFiles = { ...state.files };
@@ -525,39 +872,51 @@ var CodexConnector = class {
525
872
  }
526
873
  const { totalLines, entries } = await readJsonlFrom(filePath, 0);
527
874
  if (entries.length > 0) {
528
- 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
+ }
529
882
  }
530
883
  nextFiles[filePath] = { mtimeMs, lines: totalLines };
531
884
  }
532
885
  }
533
- 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 } };
534
893
  }
535
894
  };
536
895
 
537
896
  // src/paths.ts
538
897
  function configDir() {
539
- return process.env.TOKELYTICS_HOME ?? path4.join(os3.homedir(), ".tokelytics");
898
+ return process.env.TOKELYTICS_HOME ?? path5.join(os3.homedir(), ".tokelytics");
540
899
  }
541
- var statePath = () => path4.join(configDir(), "state.json");
542
- var credsPath = () => path4.join(configDir(), "credentials.json");
900
+ var statePath = () => path5.join(configDir(), "state.json");
901
+ var credsPath = () => path5.join(configDir(), "credentials.json");
543
902
  async function ensureDir() {
544
- await fs2.mkdir(configDir(), { recursive: true });
903
+ await fs3.mkdir(configDir(), { recursive: true });
545
904
  }
546
905
  async function readJson(file) {
547
906
  try {
548
- return JSON.parse(await fs2.readFile(file, "utf-8"));
907
+ return JSON.parse(await fs3.readFile(file, "utf-8"));
549
908
  } catch {
550
909
  return null;
551
910
  }
552
911
  }
553
912
  async function writeJson(file, value) {
554
913
  await ensureDir();
555
- await fs2.writeFile(file, JSON.stringify(value, null, 2), "utf-8");
914
+ await fs3.writeFile(file, JSON.stringify(value, null, 2), "utf-8");
556
915
  }
557
916
  async function loadState() {
558
917
  const s = await readJson(statePath());
559
918
  if (s && s.deviceId && s.scan) return s;
560
- const fresh = { deviceId: randomUUID(), scan: emptyScanState() };
919
+ const fresh = { deviceId: randomUUID(), scan: emptyScanState(), publication: {} };
561
920
  await writeJson(statePath(), fresh);
562
921
  return fresh;
563
922
  }
@@ -570,13 +929,13 @@ async function loadCredentials() {
570
929
  async function saveCredentials(creds) {
571
930
  await writeJson(credsPath(), creds);
572
931
  try {
573
- await fs2.chmod(credsPath(), 384);
932
+ await fs3.chmod(credsPath(), 384);
574
933
  } catch {
575
934
  }
576
935
  }
577
936
  async function clearCredentials() {
578
937
  try {
579
- await fs2.rm(credsPath(), { force: true });
938
+ await fs3.rm(credsPath(), { force: true });
580
939
  } catch {
581
940
  }
582
941
  }
@@ -613,7 +972,7 @@ function fromEnv() {
613
972
  }
614
973
  async function fromFile() {
615
974
  try {
616
- 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");
617
976
  return JSON.parse(raw);
618
977
  } catch {
619
978
  return {};
@@ -656,7 +1015,7 @@ function openBrowser(url) {
656
1015
  }
657
1016
  }
658
1017
  function captureOAuthCode(buildUrl) {
659
- return new Promise((resolve3, reject) => {
1018
+ return new Promise((resolve4, reject) => {
660
1019
  const server = http.createServer((req, res) => {
661
1020
  const u = new URL(req.url ?? "/", "http://localhost");
662
1021
  const code = u.searchParams.get("code");
@@ -666,7 +1025,7 @@ function captureOAuthCode(buildUrl) {
666
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>`
667
1026
  );
668
1027
  server.close();
669
- if (code) resolve3({ code });
1028
+ if (code) resolve4({ code });
670
1029
  else reject(new Error(`OAuth failed: ${error ?? "no code returned"}`));
671
1030
  });
672
1031
  server.listen(0, "127.0.0.1", () => {
@@ -761,13 +1120,13 @@ var CORS_HEADERS = {
761
1120
  "Access-Control-Allow-Headers": "Content-Type"
762
1121
  };
763
1122
  function readBody(req) {
764
- return new Promise((resolve3, reject) => {
1123
+ return new Promise((resolve4, reject) => {
765
1124
  let data = "";
766
1125
  req.on("data", (chunk) => {
767
1126
  data += chunk;
768
1127
  if (data.length > 1e6) req.destroy();
769
1128
  });
770
- req.on("end", () => resolve3(data));
1129
+ req.on("end", () => resolve4(data));
771
1130
  req.on("error", reject);
772
1131
  });
773
1132
  }
@@ -784,10 +1143,9 @@ async function exchangeRefreshToken(cfg, refreshToken) {
784
1143
  async function browserHandoffLogin(cfg) {
785
1144
  const nonce = randomBytes(5).toString("hex").toUpperCase();
786
1145
  const TIMEOUT_MS = 5 * 6e4;
787
- const payload = await new Promise((resolve3, reject) => {
788
- let timer;
1146
+ const payload = await new Promise((resolve4, reject) => {
789
1147
  function finish(act) {
790
- if (timer) clearTimeout(timer);
1148
+ clearTimeout(timer);
791
1149
  server.close();
792
1150
  act();
793
1151
  }
@@ -817,10 +1175,10 @@ async function browserHandoffLogin(cfg) {
817
1175
  }
818
1176
  res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
819
1177
  res.end(JSON.stringify({ ok: true }));
820
- finish(() => resolve3(body));
1178
+ finish(() => resolve4(body));
821
1179
  }).catch((err) => finish(() => reject(err)));
822
1180
  });
823
- timer = setTimeout(
1181
+ const timer = setTimeout(
824
1182
  () => finish(() => reject(new Error('Timed out waiting for browser sign-in. Run "tokelytics login" again.'))),
825
1183
  TIMEOUT_MS
826
1184
  );
@@ -876,13 +1234,300 @@ async function restoreSession(cfg) {
876
1234
  }
877
1235
 
878
1236
  // src/runner.ts
879
- import * as os4 from "node:os";
1237
+ import * as os5 from "node:os";
880
1238
 
881
1239
  // src/connectors.ts
882
1240
  function buildConnectors() {
883
1241
  return [new ClaudeConnector(), new CodexConnector()];
884
1242
  }
885
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
+
886
1531
  // src/firestore-rest.ts
887
1532
  function encodeValue(v) {
888
1533
  if (v === null || v === void 0) return { nullValue: null };
@@ -918,6 +1563,14 @@ function decodeFields(fields) {
918
1563
  for (const [k, val] of Object.entries(fields)) out[k] = decodeValue(val);
919
1564
  return out;
920
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
+ };
921
1574
  var FirestoreRest = class {
922
1575
  projectId;
923
1576
  getToken;
@@ -947,15 +1600,20 @@ var FirestoreRest = class {
947
1600
  const res = await fetch(this.url(`${this.docsRoot()}:commit`), {
948
1601
  method: "POST",
949
1602
  headers: await this.headers(),
950
- body: JSON.stringify({ writes })
1603
+ body: JSON.stringify({ writes }),
1604
+ signal: AbortSignal.timeout(15e3)
951
1605
  });
952
- if (!res.ok) throw new Error(`Firestore commit failed (${res.status}): ${await res.text()}`);
1606
+ if (!res.ok) throw await firestoreError("commit", res);
953
1607
  }
954
1608
  /** Fetch a single document's decoded fields, or null if it doesn't exist. */
955
1609
  async getDoc(name) {
956
- const res = await fetch(this.url(name), { method: "GET", headers: await this.headers() });
1610
+ const res = await fetch(this.url(name), {
1611
+ method: "GET",
1612
+ headers: await this.headers(),
1613
+ signal: AbortSignal.timeout(15e3)
1614
+ });
957
1615
  if (res.status === 404) return null;
958
- if (!res.ok) throw new Error(`Firestore get failed (${res.status}): ${await res.text()}`);
1616
+ if (!res.ok) throw await firestoreError("get", res);
959
1617
  const doc = await res.json();
960
1618
  return doc.fields ? decodeFields(doc.fields) : {};
961
1619
  }
@@ -973,127 +1631,170 @@ var FirestoreRest = class {
973
1631
  const res = await fetch(this.url(`${parent}:runQuery`), {
974
1632
  method: "POST",
975
1633
  headers: await this.headers(),
976
- body: JSON.stringify({ structuredQuery: { from: [{ collectionId }], where } })
1634
+ body: JSON.stringify({ structuredQuery: { from: [{ collectionId }], where } }),
1635
+ signal: AbortSignal.timeout(15e3)
977
1636
  });
978
- if (!res.ok) throw new Error(`Firestore query failed (${res.status}): ${await res.text()}`);
1637
+ if (!res.ok) throw await firestoreError("query", res);
979
1638
  const rows = await res.json();
980
1639
  return rows.filter((r) => r.document?.fields).map((r) => decodeFields(r.document.fields));
981
1640
  }
982
1641
  };
983
-
984
- // src/sink.ts
985
- function timelineDaysByProvider(buckets, nowMs) {
986
- const cutoffDay = new Date(nowMs - TIMELINE_RETENTION_MS).toISOString().slice(0, 10);
987
- const out = /* @__PURE__ */ new Map();
988
- for (const { provider, day } of buckets) {
989
- if (day < cutoffDay) continue;
990
- let set = out.get(provider);
991
- if (!set) {
992
- set = /* @__PURE__ */ new Set();
993
- out.set(provider, set);
994
- }
995
- set.add(day);
996
- }
997
- return out;
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
+ );
998
1651
  }
999
1652
 
1000
1653
  // src/firestore-sink.ts
1001
- var BATCH = 400;
1002
1654
  var FirestoreSink = class {
1003
- constructor(fs4, uid) {
1004
- this.fs = fs4;
1655
+ constructor(fs6, uid) {
1656
+ this.fs = fs6;
1005
1657
  this.uid = uid;
1006
1658
  }
1007
- async writeTurns(turns) {
1008
- for (let i = 0; i < turns.length; i += BATCH) {
1009
- const slice = turns.slice(i, i + BATCH);
1010
- await this.fs.upsert(
1011
- slice.map((t) => ({
1012
- name: this.fs.docName("users", this.uid, "turns", t.turnId),
1013
- fields: { ...t, day: dayOf(t.ts) }
1014
- }))
1015
- );
1016
- }
1017
- }
1018
- async turnsForSession(sessionId) {
1019
- const rows = await this.fs.queryEqual(["users", this.uid], "turns", [
1020
- { field: "sessionId", op: "EQUAL", value: sessionId }
1021
- ]);
1022
- return rows;
1023
- }
1024
- async turnsForBucket(provider, day) {
1025
- const rows = await this.fs.queryEqual(["users", this.uid], "turns", [
1026
- { field: "provider", op: "EQUAL", value: provider },
1027
- { field: "day", op: "EQUAL", value: day }
1028
- ]);
1029
- return rows;
1030
- }
1031
- async recomputeSessions(sessionIds) {
1032
- for (const sid of sessionIds) {
1033
- const turns = await this.turnsForSession(sid);
1034
- if (!turns.length) continue;
1035
- const agg = aggregateSession(sid, turns);
1036
- await this.fs.upsert([{ name: this.fs.docName("users", this.uid, "sessions", sid), fields: { ...agg } }]);
1037
- }
1038
- }
1039
- async recomputeRollups(buckets) {
1040
- for (const { provider, day } of buckets) {
1041
- const turns = await this.turnsForBucket(provider, day);
1042
- if (!turns.length) continue;
1043
- const rollup = buildRollup(provider, day, turns);
1044
- await this.fs.upsert([
1045
- { name: this.fs.docName("users", this.uid, "rollups", rollupId(provider, day)), fields: { ...rollup } }
1046
- ]);
1047
- }
1048
- }
1049
- async recomputeTimelines(buckets) {
1050
- const now = Date.now();
1051
- for (const [provider, days] of timelineDaysByProvider(buckets, now)) {
1052
- const recomputed = [];
1053
- for (const day of days) {
1054
- recomputed.push(...buildTimelineBuckets(await this.turnsForBucket(provider, day)));
1055
- }
1056
- const name = this.fs.docName("users", this.uid, "timelines", provider);
1057
- const existingDoc = await this.fs.getDoc(name);
1058
- const existing = existingDoc?.["buckets"] ?? [];
1059
- const merged = mergeTimeline(existing, recomputed, days, now);
1060
- await this.fs.upsert([
1061
- { name, fields: { provider, buckets: merged, updatedAt: new Date(now).toISOString() } }
1062
- ]);
1063
- }
1064
- }
1065
- async touchDevice(meta) {
1659
+ async writeDashboardSnapshot(snapshot2) {
1660
+ if (!snapshot2.device?.deviceId) throw new Error("Dashboard snapshot is missing its device id.");
1066
1661
  await this.fs.upsert([
1067
- { 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
+ }
1068
1666
  ]);
1069
1667
  }
1070
1668
  };
1071
1669
 
1072
1670
  // src/sync.ts
1073
- 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;
1074
1680
  let st = state;
1075
1681
  const collected = [];
1682
+ const limits = [];
1076
1683
  const byProvider = {};
1077
1684
  for (const c of connectors) {
1078
- const { turns: turns2, state: next } = await c.collect(st);
1685
+ const { turns: turns2, limits: nativeLimits2 = [], state: next } = await c.collect(st);
1079
1686
  st = next;
1687
+ if (refreshLimits) limits.push(...nativeLimits2);
1080
1688
  for (const t of turns2) {
1081
1689
  collected.push(t);
1082
1690
  byProvider[t.provider] = (byProvider[t.provider] ?? 0) + 1;
1083
1691
  }
1084
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
+ }
1085
1705
  const unique = /* @__PURE__ */ new Map();
1086
1706
  for (const t of collected) unique.set(t.turnId, t);
1087
1707
  const turns = [...unique.values()];
1088
- if (turns.length > 0) {
1089
- await sink.writeTurns(turns);
1090
- const { sessionIds, dayBuckets } = affectedKeys(turns);
1091
- await sink.recomputeSessions(sessionIds);
1092
- await sink.recomputeRollups(dayBuckets);
1093
- await sink.recomputeTimelines(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
+ }
1094
1751
  }
1095
- await sink.touchDevice({ ...device, lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
1096
- 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;
1097
1798
  }
1098
1799
 
1099
1800
  // src/runner.ts
@@ -1102,6 +1803,7 @@ async function createRunner() {
1102
1803
  const session = await restoreSession(cfg);
1103
1804
  if (!session) throw new Error('Not signed in. Run "tokelytics login" first.');
1104
1805
  const connectors = buildConnectors();
1806
+ const limitCollectors = buildNativeLimitCollectors();
1105
1807
  const rest = new FirestoreRest({
1106
1808
  projectId: cfg.firebase.projectId,
1107
1809
  getToken: () => session.getToken(),
@@ -1113,34 +1815,55 @@ async function createRunner() {
1113
1815
  watchPaths() {
1114
1816
  return connectors.flatMap((c) => c.watchPaths());
1115
1817
  },
1116
- async syncOnce() {
1818
+ async syncOnce(options = {}) {
1117
1819
  const state = await loadState();
1118
1820
  const device = {
1119
1821
  deviceId: state.deviceId,
1120
- name: os4.hostname(),
1822
+ name: os5.hostname(),
1121
1823
  lastSyncAt: "",
1122
1824
  providers: connectors.map((c) => c.id)
1123
1825
  };
1124
- const res = await runSync(connectors, sink, state.scan, device);
1125
- await saveState({ deviceId: state.deviceId, scan: res.state });
1126
- 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
+ };
1127
1847
  }
1128
1848
  };
1129
1849
  }
1850
+ function sameState(left, right) {
1851
+ return JSON.stringify(left) === JSON.stringify(right);
1852
+ }
1130
1853
  async function registerDevice(cfg, session) {
1131
- const rest = new FirestoreRest({
1132
- projectId: cfg.firebase.projectId,
1133
- getToken: () => session.getToken(),
1134
- emulatorHost: cfg.firestoreEmulatorHost
1135
- });
1136
- const sink = new FirestoreSink(rest, session.uid);
1137
1854
  const state = await loadState();
1138
1855
  const connectors = buildConnectors();
1139
- await sink.touchDevice({
1856
+ const now = /* @__PURE__ */ new Date();
1857
+ const device = {
1140
1858
  deviceId: state.deviceId,
1141
- name: os4.hostname(),
1142
- lastSyncAt: "",
1859
+ name: os5.hostname(),
1860
+ lastSyncAt: now.toISOString(),
1143
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() }
1144
1867
  });
1145
1868
  }
1146
1869
 
@@ -1220,7 +1943,7 @@ var ReaddirpStream = class extends Readable {
1220
1943
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
1221
1944
  const statMethod = opts.lstat ? lstat : stat;
1222
1945
  if (wantBigintFsStats) {
1223
- this._stat = (path6) => statMethod(path6, { bigint: true });
1946
+ this._stat = (path8) => statMethod(path8, { bigint: true });
1224
1947
  } else {
1225
1948
  this._stat = statMethod;
1226
1949
  }
@@ -1245,8 +1968,8 @@ var ReaddirpStream = class extends Readable {
1245
1968
  const par = this.parent;
1246
1969
  const fil = par && par.files;
1247
1970
  if (fil && fil.length > 0) {
1248
- const { path: path6, depth } = par;
1249
- 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));
1250
1973
  const awaited = await Promise.all(slice);
1251
1974
  for (const entry of awaited) {
1252
1975
  if (!entry)
@@ -1286,21 +2009,21 @@ var ReaddirpStream = class extends Readable {
1286
2009
  this.reading = false;
1287
2010
  }
1288
2011
  }
1289
- async _exploreDir(path6, depth) {
2012
+ async _exploreDir(path8, depth) {
1290
2013
  let files;
1291
2014
  try {
1292
- files = await readdir(path6, this._rdOptions);
2015
+ files = await readdir(path8, this._rdOptions);
1293
2016
  } catch (error) {
1294
2017
  this._onError(error);
1295
2018
  }
1296
- return { files, depth, path: path6 };
2019
+ return { files, depth, path: path8 };
1297
2020
  }
1298
- async _formatEntry(dirent, path6) {
2021
+ async _formatEntry(dirent, path8) {
1299
2022
  let entry;
1300
- const basename4 = this._isDirent ? dirent.name : dirent;
2023
+ const basename5 = this._isDirent ? dirent.name : dirent;
1301
2024
  try {
1302
- const fullPath = presolve(pjoin(path6, basename4));
1303
- 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 };
1304
2027
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
1305
2028
  } catch (err) {
1306
2029
  this._onError(err);
@@ -1699,16 +2422,16 @@ var delFromSet = (main2, prop, item) => {
1699
2422
  };
1700
2423
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
1701
2424
  var FsWatchInstances = /* @__PURE__ */ new Map();
1702
- function createFsWatchInstance(path6, options, listener, errHandler, emitRaw) {
2425
+ function createFsWatchInstance(path8, options, listener, errHandler, emitRaw) {
1703
2426
  const handleEvent = (rawEvent, evPath) => {
1704
- listener(path6);
1705
- emitRaw(rawEvent, evPath, { watchedPath: path6 });
1706
- if (evPath && path6 !== evPath) {
1707
- 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));
1708
2431
  }
1709
2432
  };
1710
2433
  try {
1711
- return fs_watch(path6, {
2434
+ return fs_watch(path8, {
1712
2435
  persistent: options.persistent
1713
2436
  }, handleEvent);
1714
2437
  } catch (error) {
@@ -1724,12 +2447,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
1724
2447
  listener(val1, val2, val3);
1725
2448
  });
1726
2449
  };
1727
- var setFsWatchListener = (path6, fullPath, options, handlers) => {
2450
+ var setFsWatchListener = (path8, fullPath, options, handlers) => {
1728
2451
  const { listener, errHandler, rawEmitter } = handlers;
1729
2452
  let cont = FsWatchInstances.get(fullPath);
1730
2453
  let watcher;
1731
2454
  if (!options.persistent) {
1732
- watcher = createFsWatchInstance(path6, options, listener, errHandler, rawEmitter);
2455
+ watcher = createFsWatchInstance(path8, options, listener, errHandler, rawEmitter);
1733
2456
  if (!watcher)
1734
2457
  return;
1735
2458
  return watcher.close.bind(watcher);
@@ -1740,7 +2463,7 @@ var setFsWatchListener = (path6, fullPath, options, handlers) => {
1740
2463
  addAndConvert(cont, KEY_RAW, rawEmitter);
1741
2464
  } else {
1742
2465
  watcher = createFsWatchInstance(
1743
- path6,
2466
+ path8,
1744
2467
  options,
1745
2468
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
1746
2469
  errHandler,
@@ -1755,7 +2478,7 @@ var setFsWatchListener = (path6, fullPath, options, handlers) => {
1755
2478
  cont.watcherUnusable = true;
1756
2479
  if (isWindows && error.code === "EPERM") {
1757
2480
  try {
1758
- const fd = await open(path6, "r");
2481
+ const fd = await open(path8, "r");
1759
2482
  await fd.close();
1760
2483
  broadcastErr(error);
1761
2484
  } catch (err) {
@@ -1786,7 +2509,7 @@ var setFsWatchListener = (path6, fullPath, options, handlers) => {
1786
2509
  };
1787
2510
  };
1788
2511
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
1789
- var setFsWatchFileListener = (path6, fullPath, options, handlers) => {
2512
+ var setFsWatchFileListener = (path8, fullPath, options, handlers) => {
1790
2513
  const { listener, rawEmitter } = handlers;
1791
2514
  let cont = FsWatchFileInstances.get(fullPath);
1792
2515
  const copts = cont && cont.options;
@@ -1808,7 +2531,7 @@ var setFsWatchFileListener = (path6, fullPath, options, handlers) => {
1808
2531
  });
1809
2532
  const currmtime = curr.mtimeMs;
1810
2533
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
1811
- foreach(cont.listeners, (listener2) => listener2(path6, curr));
2534
+ foreach(cont.listeners, (listener2) => listener2(path8, curr));
1812
2535
  }
1813
2536
  })
1814
2537
  };
@@ -1836,13 +2559,13 @@ var NodeFsHandler = class {
1836
2559
  * @param listener on fs change
1837
2560
  * @returns closer for the watcher instance
1838
2561
  */
1839
- _watchWithNodeFs(path6, listener) {
2562
+ _watchWithNodeFs(path8, listener) {
1840
2563
  const opts = this.fsw.options;
1841
- const directory = sysPath.dirname(path6);
1842
- const basename4 = sysPath.basename(path6);
2564
+ const directory = sysPath.dirname(path8);
2565
+ const basename5 = sysPath.basename(path8);
1843
2566
  const parent = this.fsw._getWatchedDir(directory);
1844
- parent.add(basename4);
1845
- const absolutePath = sysPath.resolve(path6);
2567
+ parent.add(basename5);
2568
+ const absolutePath = sysPath.resolve(path8);
1846
2569
  const options = {
1847
2570
  persistent: opts.persistent
1848
2571
  };
@@ -1851,13 +2574,13 @@ var NodeFsHandler = class {
1851
2574
  let closer;
1852
2575
  if (opts.usePolling) {
1853
2576
  const enableBin = opts.interval !== opts.binaryInterval;
1854
- options.interval = enableBin && isBinaryPath(basename4) ? opts.binaryInterval : opts.interval;
1855
- closer = setFsWatchFileListener(path6, absolutePath, options, {
2577
+ options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
2578
+ closer = setFsWatchFileListener(path8, absolutePath, options, {
1856
2579
  listener,
1857
2580
  rawEmitter: this.fsw._emitRaw
1858
2581
  });
1859
2582
  } else {
1860
- closer = setFsWatchListener(path6, absolutePath, options, {
2583
+ closer = setFsWatchListener(path8, absolutePath, options, {
1861
2584
  listener,
1862
2585
  errHandler: this._boundHandleError,
1863
2586
  rawEmitter: this.fsw._emitRaw
@@ -1874,12 +2597,12 @@ var NodeFsHandler = class {
1874
2597
  return;
1875
2598
  }
1876
2599
  const dirname3 = sysPath.dirname(file);
1877
- const basename4 = sysPath.basename(file);
2600
+ const basename5 = sysPath.basename(file);
1878
2601
  const parent = this.fsw._getWatchedDir(dirname3);
1879
2602
  let prevStats = stats;
1880
- if (parent.has(basename4))
2603
+ if (parent.has(basename5))
1881
2604
  return;
1882
- const listener = async (path6, newStats) => {
2605
+ const listener = async (path8, newStats) => {
1883
2606
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
1884
2607
  return;
1885
2608
  if (!newStats || newStats.mtimeMs === 0) {
@@ -1893,18 +2616,18 @@ var NodeFsHandler = class {
1893
2616
  this.fsw._emit(EV.CHANGE, file, newStats2);
1894
2617
  }
1895
2618
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
1896
- this.fsw._closeFile(path6);
2619
+ this.fsw._closeFile(path8);
1897
2620
  prevStats = newStats2;
1898
2621
  const closer2 = this._watchWithNodeFs(file, listener);
1899
2622
  if (closer2)
1900
- this.fsw._addPathCloser(path6, closer2);
2623
+ this.fsw._addPathCloser(path8, closer2);
1901
2624
  } else {
1902
2625
  prevStats = newStats2;
1903
2626
  }
1904
2627
  } catch (error) {
1905
- this.fsw._remove(dirname3, basename4);
2628
+ this.fsw._remove(dirname3, basename5);
1906
2629
  }
1907
- } else if (parent.has(basename4)) {
2630
+ } else if (parent.has(basename5)) {
1908
2631
  const at = newStats.atimeMs;
1909
2632
  const mt = newStats.mtimeMs;
1910
2633
  if (!at || at <= mt || mt !== prevStats.mtimeMs) {
@@ -1929,7 +2652,7 @@ var NodeFsHandler = class {
1929
2652
  * @param item basename of this item
1930
2653
  * @returns true if no more processing is needed for this entry.
1931
2654
  */
1932
- async _handleSymlink(entry, directory, path6, item) {
2655
+ async _handleSymlink(entry, directory, path8, item) {
1933
2656
  if (this.fsw.closed) {
1934
2657
  return;
1935
2658
  }
@@ -1939,7 +2662,7 @@ var NodeFsHandler = class {
1939
2662
  this.fsw._incrReadyCount();
1940
2663
  let linkPath;
1941
2664
  try {
1942
- linkPath = await fsrealpath(path6);
2665
+ linkPath = await fsrealpath(path8);
1943
2666
  } catch (e) {
1944
2667
  this.fsw._emitReady();
1945
2668
  return true;
@@ -1949,12 +2672,12 @@ var NodeFsHandler = class {
1949
2672
  if (dir.has(item)) {
1950
2673
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
1951
2674
  this.fsw._symlinkPaths.set(full, linkPath);
1952
- this.fsw._emit(EV.CHANGE, path6, entry.stats);
2675
+ this.fsw._emit(EV.CHANGE, path8, entry.stats);
1953
2676
  }
1954
2677
  } else {
1955
2678
  dir.add(item);
1956
2679
  this.fsw._symlinkPaths.set(full, linkPath);
1957
- this.fsw._emit(EV.ADD, path6, entry.stats);
2680
+ this.fsw._emit(EV.ADD, path8, entry.stats);
1958
2681
  }
1959
2682
  this.fsw._emitReady();
1960
2683
  return true;
@@ -1983,9 +2706,9 @@ var NodeFsHandler = class {
1983
2706
  return;
1984
2707
  }
1985
2708
  const item = entry.path;
1986
- let path6 = sysPath.join(directory, item);
2709
+ let path8 = sysPath.join(directory, item);
1987
2710
  current.add(item);
1988
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path6, item)) {
2711
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path8, item)) {
1989
2712
  return;
1990
2713
  }
1991
2714
  if (this.fsw.closed) {
@@ -1994,11 +2717,11 @@ var NodeFsHandler = class {
1994
2717
  }
1995
2718
  if (item === target || !target && !previous.has(item)) {
1996
2719
  this.fsw._incrReadyCount();
1997
- path6 = sysPath.join(dir, sysPath.relative(dir, path6));
1998
- this._addToNodeFs(path6, initialAdd, wh, depth + 1);
2720
+ path8 = sysPath.join(dir, sysPath.relative(dir, path8));
2721
+ this._addToNodeFs(path8, initialAdd, wh, depth + 1);
1999
2722
  }
2000
2723
  }).on(EV.ERROR, this._boundHandleError);
2001
- return new Promise((resolve3, reject) => {
2724
+ return new Promise((resolve4, reject) => {
2002
2725
  if (!stream)
2003
2726
  return reject();
2004
2727
  stream.once(STR_END, () => {
@@ -2007,7 +2730,7 @@ var NodeFsHandler = class {
2007
2730
  return;
2008
2731
  }
2009
2732
  const wasThrottled = throttler ? throttler.clear() : false;
2010
- resolve3(void 0);
2733
+ resolve4(void 0);
2011
2734
  previous.getChildren().filter((item) => {
2012
2735
  return item !== directory && !current.has(item);
2013
2736
  }).forEach((item) => {
@@ -2064,13 +2787,13 @@ var NodeFsHandler = class {
2064
2787
  * @param depth Child path actually targeted for watch
2065
2788
  * @param target Child path actually targeted for watch
2066
2789
  */
2067
- async _addToNodeFs(path6, initialAdd, priorWh, depth, target) {
2790
+ async _addToNodeFs(path8, initialAdd, priorWh, depth, target) {
2068
2791
  const ready = this.fsw._emitReady;
2069
- if (this.fsw._isIgnored(path6) || this.fsw.closed) {
2792
+ if (this.fsw._isIgnored(path8) || this.fsw.closed) {
2070
2793
  ready();
2071
2794
  return false;
2072
2795
  }
2073
- const wh = this.fsw._getWatchHelpers(path6);
2796
+ const wh = this.fsw._getWatchHelpers(path8);
2074
2797
  if (priorWh) {
2075
2798
  wh.filterPath = (entry) => priorWh.filterPath(entry);
2076
2799
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -2086,8 +2809,8 @@ var NodeFsHandler = class {
2086
2809
  const follow = this.fsw.options.followSymlinks;
2087
2810
  let closer;
2088
2811
  if (stats.isDirectory()) {
2089
- const absPath = sysPath.resolve(path6);
2090
- const targetPath = follow ? await fsrealpath(path6) : path6;
2812
+ const absPath = sysPath.resolve(path8);
2813
+ const targetPath = follow ? await fsrealpath(path8) : path8;
2091
2814
  if (this.fsw.closed)
2092
2815
  return;
2093
2816
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -2097,29 +2820,29 @@ var NodeFsHandler = class {
2097
2820
  this.fsw._symlinkPaths.set(absPath, targetPath);
2098
2821
  }
2099
2822
  } else if (stats.isSymbolicLink()) {
2100
- const targetPath = follow ? await fsrealpath(path6) : path6;
2823
+ const targetPath = follow ? await fsrealpath(path8) : path8;
2101
2824
  if (this.fsw.closed)
2102
2825
  return;
2103
2826
  const parent = sysPath.dirname(wh.watchPath);
2104
2827
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
2105
2828
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
2106
- closer = await this._handleDir(parent, stats, initialAdd, depth, path6, wh, targetPath);
2829
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path8, wh, targetPath);
2107
2830
  if (this.fsw.closed)
2108
2831
  return;
2109
2832
  if (targetPath !== void 0) {
2110
- this.fsw._symlinkPaths.set(sysPath.resolve(path6), targetPath);
2833
+ this.fsw._symlinkPaths.set(sysPath.resolve(path8), targetPath);
2111
2834
  }
2112
2835
  } else {
2113
2836
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
2114
2837
  }
2115
2838
  ready();
2116
2839
  if (closer)
2117
- this.fsw._addPathCloser(path6, closer);
2840
+ this.fsw._addPathCloser(path8, closer);
2118
2841
  return false;
2119
2842
  } catch (error) {
2120
2843
  if (this.fsw._handleError(error)) {
2121
2844
  ready();
2122
- return path6;
2845
+ return path8;
2123
2846
  }
2124
2847
  }
2125
2848
  }
@@ -2162,26 +2885,26 @@ function createPattern(matcher) {
2162
2885
  }
2163
2886
  return () => false;
2164
2887
  }
2165
- function normalizePath(path6) {
2166
- if (typeof path6 !== "string")
2888
+ function normalizePath(path8) {
2889
+ if (typeof path8 !== "string")
2167
2890
  throw new Error("string expected");
2168
- path6 = sysPath2.normalize(path6);
2169
- path6 = path6.replace(/\\/g, "/");
2891
+ path8 = sysPath2.normalize(path8);
2892
+ path8 = path8.replace(/\\/g, "/");
2170
2893
  let prepend = false;
2171
- if (path6.startsWith("//"))
2894
+ if (path8.startsWith("//"))
2172
2895
  prepend = true;
2173
2896
  const DOUBLE_SLASH_RE2 = /\/\//;
2174
- while (path6.match(DOUBLE_SLASH_RE2))
2175
- path6 = path6.replace(DOUBLE_SLASH_RE2, "/");
2897
+ while (path8.match(DOUBLE_SLASH_RE2))
2898
+ path8 = path8.replace(DOUBLE_SLASH_RE2, "/");
2176
2899
  if (prepend)
2177
- path6 = "/" + path6;
2178
- return path6;
2900
+ path8 = "/" + path8;
2901
+ return path8;
2179
2902
  }
2180
2903
  function matchPatterns(patterns, testString, stats) {
2181
- const path6 = normalizePath(testString);
2904
+ const path8 = normalizePath(testString);
2182
2905
  for (let index = 0; index < patterns.length; index++) {
2183
2906
  const pattern = patterns[index];
2184
- if (pattern(path6, stats)) {
2907
+ if (pattern(path8, stats)) {
2185
2908
  return true;
2186
2909
  }
2187
2910
  }
@@ -2221,19 +2944,19 @@ var toUnix = (string) => {
2221
2944
  }
2222
2945
  return str;
2223
2946
  };
2224
- var normalizePathToUnix = (path6) => toUnix(sysPath2.normalize(toUnix(path6)));
2225
- var normalizeIgnored = (cwd = "") => (path6) => {
2226
- if (typeof path6 === "string") {
2227
- 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));
2228
2951
  } else {
2229
- return path6;
2952
+ return path8;
2230
2953
  }
2231
2954
  };
2232
- var getAbsolutePath = (path6, cwd) => {
2233
- if (sysPath2.isAbsolute(path6)) {
2234
- return path6;
2955
+ var getAbsolutePath = (path8, cwd) => {
2956
+ if (sysPath2.isAbsolute(path8)) {
2957
+ return path8;
2235
2958
  }
2236
- return sysPath2.join(cwd, path6);
2959
+ return sysPath2.join(cwd, path8);
2237
2960
  };
2238
2961
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
2239
2962
  var DirEntry = class {
@@ -2288,10 +3011,10 @@ var DirEntry = class {
2288
3011
  var STAT_METHOD_F = "stat";
2289
3012
  var STAT_METHOD_L = "lstat";
2290
3013
  var WatchHelper = class {
2291
- constructor(path6, follow, fsw) {
3014
+ constructor(path8, follow, fsw) {
2292
3015
  this.fsw = fsw;
2293
- const watchPath = path6;
2294
- this.path = path6 = path6.replace(REPLACER_RE, "");
3016
+ const watchPath = path8;
3017
+ this.path = path8 = path8.replace(REPLACER_RE, "");
2295
3018
  this.watchPath = watchPath;
2296
3019
  this.fullWatchPath = sysPath2.resolve(watchPath);
2297
3020
  this.dirParts = [];
@@ -2413,20 +3136,20 @@ var FSWatcher = class extends EventEmitter {
2413
3136
  this._closePromise = void 0;
2414
3137
  let paths = unifyPaths(paths_);
2415
3138
  if (cwd) {
2416
- paths = paths.map((path6) => {
2417
- const absPath = getAbsolutePath(path6, cwd);
3139
+ paths = paths.map((path8) => {
3140
+ const absPath = getAbsolutePath(path8, cwd);
2418
3141
  return absPath;
2419
3142
  });
2420
3143
  }
2421
- paths.forEach((path6) => {
2422
- this._removeIgnoredPath(path6);
3144
+ paths.forEach((path8) => {
3145
+ this._removeIgnoredPath(path8);
2423
3146
  });
2424
3147
  this._userIgnored = void 0;
2425
3148
  if (!this._readyCount)
2426
3149
  this._readyCount = 0;
2427
3150
  this._readyCount += paths.length;
2428
- Promise.all(paths.map(async (path6) => {
2429
- 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);
2430
3153
  if (res)
2431
3154
  this._emitReady();
2432
3155
  return res;
@@ -2448,17 +3171,17 @@ var FSWatcher = class extends EventEmitter {
2448
3171
  return this;
2449
3172
  const paths = unifyPaths(paths_);
2450
3173
  const { cwd } = this.options;
2451
- paths.forEach((path6) => {
2452
- if (!sysPath2.isAbsolute(path6) && !this._closers.has(path6)) {
3174
+ paths.forEach((path8) => {
3175
+ if (!sysPath2.isAbsolute(path8) && !this._closers.has(path8)) {
2453
3176
  if (cwd)
2454
- path6 = sysPath2.join(cwd, path6);
2455
- path6 = sysPath2.resolve(path6);
3177
+ path8 = sysPath2.join(cwd, path8);
3178
+ path8 = sysPath2.resolve(path8);
2456
3179
  }
2457
- this._closePath(path6);
2458
- this._addIgnoredPath(path6);
2459
- if (this._watched.has(path6)) {
3180
+ this._closePath(path8);
3181
+ this._addIgnoredPath(path8);
3182
+ if (this._watched.has(path8)) {
2460
3183
  this._addIgnoredPath({
2461
- path: path6,
3184
+ path: path8,
2462
3185
  recursive: true
2463
3186
  });
2464
3187
  }
@@ -2522,38 +3245,38 @@ var FSWatcher = class extends EventEmitter {
2522
3245
  * @param stats arguments to be passed with event
2523
3246
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
2524
3247
  */
2525
- async _emit(event, path6, stats) {
3248
+ async _emit(event, path8, stats) {
2526
3249
  if (this.closed)
2527
3250
  return;
2528
3251
  const opts = this.options;
2529
3252
  if (isWindows)
2530
- path6 = sysPath2.normalize(path6);
3253
+ path8 = sysPath2.normalize(path8);
2531
3254
  if (opts.cwd)
2532
- path6 = sysPath2.relative(opts.cwd, path6);
2533
- const args = [path6];
3255
+ path8 = sysPath2.relative(opts.cwd, path8);
3256
+ const args = [path8];
2534
3257
  if (stats != null)
2535
3258
  args.push(stats);
2536
3259
  const awf = opts.awaitWriteFinish;
2537
3260
  let pw;
2538
- if (awf && (pw = this._pendingWrites.get(path6))) {
3261
+ if (awf && (pw = this._pendingWrites.get(path8))) {
2539
3262
  pw.lastChange = /* @__PURE__ */ new Date();
2540
3263
  return this;
2541
3264
  }
2542
3265
  if (opts.atomic) {
2543
3266
  if (event === EVENTS.UNLINK) {
2544
- this._pendingUnlinks.set(path6, [event, ...args]);
3267
+ this._pendingUnlinks.set(path8, [event, ...args]);
2545
3268
  setTimeout(() => {
2546
- this._pendingUnlinks.forEach((entry, path7) => {
3269
+ this._pendingUnlinks.forEach((entry, path9) => {
2547
3270
  this.emit(...entry);
2548
3271
  this.emit(EVENTS.ALL, ...entry);
2549
- this._pendingUnlinks.delete(path7);
3272
+ this._pendingUnlinks.delete(path9);
2550
3273
  });
2551
3274
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
2552
3275
  return this;
2553
3276
  }
2554
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path6)) {
3277
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path8)) {
2555
3278
  event = EVENTS.CHANGE;
2556
- this._pendingUnlinks.delete(path6);
3279
+ this._pendingUnlinks.delete(path8);
2557
3280
  }
2558
3281
  }
2559
3282
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -2571,16 +3294,16 @@ var FSWatcher = class extends EventEmitter {
2571
3294
  this.emitWithAll(event, args);
2572
3295
  }
2573
3296
  };
2574
- this._awaitWriteFinish(path6, awf.stabilityThreshold, event, awfEmit);
3297
+ this._awaitWriteFinish(path8, awf.stabilityThreshold, event, awfEmit);
2575
3298
  return this;
2576
3299
  }
2577
3300
  if (event === EVENTS.CHANGE) {
2578
- const isThrottled = !this._throttle(EVENTS.CHANGE, path6, 50);
3301
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path8, 50);
2579
3302
  if (isThrottled)
2580
3303
  return this;
2581
3304
  }
2582
3305
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
2583
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path6) : path6;
3306
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path8) : path8;
2584
3307
  let stats2;
2585
3308
  try {
2586
3309
  stats2 = await stat3(fullPath);
@@ -2611,23 +3334,23 @@ var FSWatcher = class extends EventEmitter {
2611
3334
  * @param timeout duration of time to suppress duplicate actions
2612
3335
  * @returns tracking object or false if action should be suppressed
2613
3336
  */
2614
- _throttle(actionType, path6, timeout) {
3337
+ _throttle(actionType, path8, timeout) {
2615
3338
  if (!this._throttled.has(actionType)) {
2616
3339
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
2617
3340
  }
2618
3341
  const action = this._throttled.get(actionType);
2619
3342
  if (!action)
2620
3343
  throw new Error("invalid throttle");
2621
- const actionPath = action.get(path6);
3344
+ const actionPath = action.get(path8);
2622
3345
  if (actionPath) {
2623
3346
  actionPath.count++;
2624
3347
  return false;
2625
3348
  }
2626
3349
  let timeoutObject;
2627
3350
  const clear = () => {
2628
- const item = action.get(path6);
3351
+ const item = action.get(path8);
2629
3352
  const count = item ? item.count : 0;
2630
- action.delete(path6);
3353
+ action.delete(path8);
2631
3354
  clearTimeout(timeoutObject);
2632
3355
  if (item)
2633
3356
  clearTimeout(item.timeoutObject);
@@ -2635,7 +3358,7 @@ var FSWatcher = class extends EventEmitter {
2635
3358
  };
2636
3359
  timeoutObject = setTimeout(clear, timeout);
2637
3360
  const thr = { timeoutObject, clear, count: 0 };
2638
- action.set(path6, thr);
3361
+ action.set(path8, thr);
2639
3362
  return thr;
2640
3363
  }
2641
3364
  _incrReadyCount() {
@@ -2649,44 +3372,44 @@ var FSWatcher = class extends EventEmitter {
2649
3372
  * @param event
2650
3373
  * @param awfEmit Callback to be called when ready for event to be emitted.
2651
3374
  */
2652
- _awaitWriteFinish(path6, threshold, event, awfEmit) {
3375
+ _awaitWriteFinish(path8, threshold, event, awfEmit) {
2653
3376
  const awf = this.options.awaitWriteFinish;
2654
3377
  if (typeof awf !== "object")
2655
3378
  return;
2656
3379
  const pollInterval = awf.pollInterval;
2657
3380
  let timeoutHandler;
2658
- let fullPath = path6;
2659
- if (this.options.cwd && !sysPath2.isAbsolute(path6)) {
2660
- 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);
2661
3384
  }
2662
3385
  const now = /* @__PURE__ */ new Date();
2663
3386
  const writes = this._pendingWrites;
2664
3387
  function awaitWriteFinishFn(prevStat) {
2665
3388
  statcb(fullPath, (err, curStat) => {
2666
- if (err || !writes.has(path6)) {
3389
+ if (err || !writes.has(path8)) {
2667
3390
  if (err && err.code !== "ENOENT")
2668
3391
  awfEmit(err);
2669
3392
  return;
2670
3393
  }
2671
3394
  const now2 = Number(/* @__PURE__ */ new Date());
2672
3395
  if (prevStat && curStat.size !== prevStat.size) {
2673
- writes.get(path6).lastChange = now2;
3396
+ writes.get(path8).lastChange = now2;
2674
3397
  }
2675
- const pw = writes.get(path6);
3398
+ const pw = writes.get(path8);
2676
3399
  const df = now2 - pw.lastChange;
2677
3400
  if (df >= threshold) {
2678
- writes.delete(path6);
3401
+ writes.delete(path8);
2679
3402
  awfEmit(void 0, curStat);
2680
3403
  } else {
2681
3404
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
2682
3405
  }
2683
3406
  });
2684
3407
  }
2685
- if (!writes.has(path6)) {
2686
- writes.set(path6, {
3408
+ if (!writes.has(path8)) {
3409
+ writes.set(path8, {
2687
3410
  lastChange: now,
2688
3411
  cancelWait: () => {
2689
- writes.delete(path6);
3412
+ writes.delete(path8);
2690
3413
  clearTimeout(timeoutHandler);
2691
3414
  return event;
2692
3415
  }
@@ -2697,8 +3420,8 @@ var FSWatcher = class extends EventEmitter {
2697
3420
  /**
2698
3421
  * Determines whether user has asked to ignore this path.
2699
3422
  */
2700
- _isIgnored(path6, stats) {
2701
- if (this.options.atomic && DOT_RE.test(path6))
3423
+ _isIgnored(path8, stats) {
3424
+ if (this.options.atomic && DOT_RE.test(path8))
2702
3425
  return true;
2703
3426
  if (!this._userIgnored) {
2704
3427
  const { cwd } = this.options;
@@ -2708,17 +3431,17 @@ var FSWatcher = class extends EventEmitter {
2708
3431
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
2709
3432
  this._userIgnored = anymatch(list, void 0);
2710
3433
  }
2711
- return this._userIgnored(path6, stats);
3434
+ return this._userIgnored(path8, stats);
2712
3435
  }
2713
- _isntIgnored(path6, stat4) {
2714
- return !this._isIgnored(path6, stat4);
3436
+ _isntIgnored(path8, stat4) {
3437
+ return !this._isIgnored(path8, stat4);
2715
3438
  }
2716
3439
  /**
2717
3440
  * Provides a set of common helpers and properties relating to symlink handling.
2718
3441
  * @param path file or directory pattern being watched
2719
3442
  */
2720
- _getWatchHelpers(path6) {
2721
- return new WatchHelper(path6, this.options.followSymlinks, this);
3443
+ _getWatchHelpers(path8) {
3444
+ return new WatchHelper(path8, this.options.followSymlinks, this);
2722
3445
  }
2723
3446
  // Directory helpers
2724
3447
  // -----------------
@@ -2750,63 +3473,63 @@ var FSWatcher = class extends EventEmitter {
2750
3473
  * @param item base path of item/directory
2751
3474
  */
2752
3475
  _remove(directory, item, isDirectory) {
2753
- const path6 = sysPath2.join(directory, item);
2754
- const fullPath = sysPath2.resolve(path6);
2755
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path6) || this._watched.has(fullPath);
2756
- 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))
2757
3480
  return;
2758
3481
  if (!isDirectory && this._watched.size === 1) {
2759
3482
  this.add(directory, item, true);
2760
3483
  }
2761
- const wp = this._getWatchedDir(path6);
3484
+ const wp = this._getWatchedDir(path8);
2762
3485
  const nestedDirectoryChildren = wp.getChildren();
2763
- nestedDirectoryChildren.forEach((nested) => this._remove(path6, nested));
3486
+ nestedDirectoryChildren.forEach((nested) => this._remove(path8, nested));
2764
3487
  const parent = this._getWatchedDir(directory);
2765
3488
  const wasTracked = parent.has(item);
2766
3489
  parent.remove(item);
2767
3490
  if (this._symlinkPaths.has(fullPath)) {
2768
3491
  this._symlinkPaths.delete(fullPath);
2769
3492
  }
2770
- let relPath = path6;
3493
+ let relPath = path8;
2771
3494
  if (this.options.cwd)
2772
- relPath = sysPath2.relative(this.options.cwd, path6);
3495
+ relPath = sysPath2.relative(this.options.cwd, path8);
2773
3496
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
2774
3497
  const event = this._pendingWrites.get(relPath).cancelWait();
2775
3498
  if (event === EVENTS.ADD)
2776
3499
  return;
2777
3500
  }
2778
- this._watched.delete(path6);
3501
+ this._watched.delete(path8);
2779
3502
  this._watched.delete(fullPath);
2780
3503
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
2781
- if (wasTracked && !this._isIgnored(path6))
2782
- this._emit(eventName, path6);
2783
- this._closePath(path6);
3504
+ if (wasTracked && !this._isIgnored(path8))
3505
+ this._emit(eventName, path8);
3506
+ this._closePath(path8);
2784
3507
  }
2785
3508
  /**
2786
3509
  * Closes all watchers for a path
2787
3510
  */
2788
- _closePath(path6) {
2789
- this._closeFile(path6);
2790
- const dir = sysPath2.dirname(path6);
2791
- 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));
2792
3515
  }
2793
3516
  /**
2794
3517
  * Closes only file-specific watchers
2795
3518
  */
2796
- _closeFile(path6) {
2797
- const closers = this._closers.get(path6);
3519
+ _closeFile(path8) {
3520
+ const closers = this._closers.get(path8);
2798
3521
  if (!closers)
2799
3522
  return;
2800
3523
  closers.forEach((closer) => closer());
2801
- this._closers.delete(path6);
3524
+ this._closers.delete(path8);
2802
3525
  }
2803
- _addPathCloser(path6, closer) {
3526
+ _addPathCloser(path8, closer) {
2804
3527
  if (!closer)
2805
3528
  return;
2806
- let list = this._closers.get(path6);
3529
+ let list = this._closers.get(path8);
2807
3530
  if (!list) {
2808
3531
  list = [];
2809
- this._closers.set(path6, list);
3532
+ this._closers.set(path8, list);
2810
3533
  }
2811
3534
  list.push(closer);
2812
3535
  }
@@ -2837,56 +3560,150 @@ var esm_default = { watch, FSWatcher };
2837
3560
 
2838
3561
  // src/watch.ts
2839
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;
2840
3570
  async function watch2(runner) {
2841
- 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
+ });
2842
3579
  const paths = runner.watchPaths();
2843
3580
  const watcher = esm_default.watch(paths, {
2844
3581
  ignoreInitial: true,
2845
3582
  awaitWriteFinish: { stabilityThreshold: 400, pollInterval: 100 },
2846
- 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))
2847
3586
  });
2848
3587
  let timer;
2849
- let pending = false;
3588
+ let pending;
2850
3589
  let running = false;
2851
- 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;
2852
3597
  if (timer) clearTimeout(timer);
3598
+ scheduledAt = target;
2853
3599
  timer = setTimeout(async () => {
2854
- if (running) {
2855
- pending = true;
2856
- return;
2857
- }
3600
+ timer = void 0;
3601
+ scheduledAt = 0;
3602
+ const next = pending;
3603
+ pending = void 0;
3604
+ if (!next) return;
2858
3605
  running = true;
2859
- await safeSync(runner, "change");
3606
+ const retryAfterMs = await safeSync(runner, next);
2860
3607
  running = false;
3608
+ if (retryAfterMs) {
3609
+ blockedUntil = Date.now() + retryAfterMs;
3610
+ pending = mergeRequests(pending, next);
3611
+ } else {
3612
+ blockedUntil = 0;
3613
+ }
2861
3614
  if (pending) {
2862
- pending = false;
2863
- schedule();
3615
+ const queued = pending;
3616
+ pending = void 0;
3617
+ schedule(queued, 0);
2864
3618
  }
2865
- }, DEBOUNCE_MS);
3619
+ }, Math.max(0, target - Date.now()));
2866
3620
  };
2867
- 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
+ );
2868
3643
  console.log(`Watching for usage in:
2869
3644
  ${paths.join("\n ")}
2870
3645
  Press Ctrl+C to stop.`);
2871
- await new Promise((resolve3) => {
3646
+ await new Promise((resolve4) => {
2872
3647
  process.on("SIGINT", () => {
2873
- void watcher.close().then(resolve3);
3648
+ clearInterval(refreshTimer);
3649
+ clearInterval(fallbackTimer);
3650
+ if (timer) clearTimeout(timer);
3651
+ void watcher.close().then(resolve4);
2874
3652
  });
2875
3653
  });
2876
3654
  }
2877
- async function safeSync(runner, reason) {
3655
+ async function safeSync(runner, request) {
2878
3656
  try {
2879
- const { newTurns, byProvider } = await runner.syncOnce();
2880
- if (newTurns > 0) {
3657
+ const { reason, ...options } = request;
3658
+ const { processedTurns, byProvider, published, cloudWritesToday } = await runner.syncOnce(options);
3659
+ if (processedTurns > 0) {
2881
3660
  const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ");
2882
- 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
+ );
2883
3664
  } else if (reason === "startup") {
2884
- 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
+ );
2885
3668
  }
2886
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
+ }
2887
3680
  console.error(`Sync error: ${err.message}`);
3681
+ return 15e3;
2888
3682
  }
2889
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
+ }
2890
3707
 
2891
3708
  // src/cli.ts
2892
3709
  var USAGE = `Tokelytics agent
@@ -2894,7 +3711,7 @@ var USAGE = `Tokelytics agent
2894
3711
  Usage:
2895
3712
  tokelytics login Sign in by approving in your browser
2896
3713
  tokelytics sync Run one incremental sync to your dashboard
2897
- tokelytics watch Stream usage in realtime (filesystem watcher)
3714
+ tokelytics watch Watch usage and refresh the cloud snapshot
2898
3715
  tokelytics status Show current sign-in and device
2899
3716
  tokelytics logout Forget stored credentials
2900
3717
  `;
@@ -2915,9 +3732,9 @@ async function main(argv) {
2915
3732
  }
2916
3733
  case "sync": {
2917
3734
  const runner = await createRunner();
2918
- const { newTurns, byProvider } = await runner.syncOnce();
3735
+ const { processedTurns, processedLimits, byProvider } = await runner.syncOnce();
2919
3736
  const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ") || "none";
2920
- console.log(`Synced ${newTurns} new turn(s) (${detail}).`);
3737
+ console.log(`Processed ${processedTurns} turn(s) (${detail}); refreshed ${processedLimits} usage provider(s).`);
2921
3738
  return 0;
2922
3739
  }
2923
3740
  case "watch": {