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