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