tokelytics 0.2.0 → 0.3.1
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 +12 -3
- package/bin/tokelytics.mjs +1446 -446
- package/package.json +18 -6
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,134 @@ 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
|
+
var SNAPSHOT_TARGET_BYTES = 7e5;
|
|
74
|
+
function emptyDashboardSnapshot(now = /* @__PURE__ */ new Date()) {
|
|
75
|
+
return {
|
|
76
|
+
version: 2,
|
|
77
|
+
updatedAt: now.toISOString(),
|
|
78
|
+
recentTurns: [],
|
|
79
|
+
sessions: [],
|
|
80
|
+
rollups: [],
|
|
81
|
+
limits: []
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function mergeDashboardSnapshot(previous, turns, limits, device, now = /* @__PURE__ */ new Date()) {
|
|
85
|
+
const next = structuredClone(previous ?? emptyDashboardSnapshot(now));
|
|
86
|
+
next.version = 2;
|
|
87
|
+
if (turns.length > 0) {
|
|
88
|
+
next.recentTurns = mergeRecentTurns(next.recentTurns, turns);
|
|
89
|
+
next.sessions = mergeSessions(next.sessions, turns);
|
|
90
|
+
next.rollups = mergeRollups(next.rollups, turns, now);
|
|
91
|
+
}
|
|
92
|
+
if (limits.length > 0)
|
|
93
|
+
next.limits = mergeLimits(next.limits, limits);
|
|
94
|
+
if (device)
|
|
95
|
+
next.device = device;
|
|
96
|
+
if (turns.length > 0 || limits.length > 0 || device)
|
|
97
|
+
next.updatedAt = now.toISOString();
|
|
98
|
+
return next;
|
|
99
|
+
}
|
|
100
|
+
function prepareDashboardSnapshotForCloud(snapshot2, targetBytes = SNAPSHOT_TARGET_BYTES) {
|
|
101
|
+
const next = structuredClone(snapshot2);
|
|
102
|
+
next.recentTurns = next.recentTurns.map(sanitizeTurn);
|
|
103
|
+
next.sessions = next.sessions.map((session) => ({
|
|
104
|
+
...session,
|
|
105
|
+
project: safeLabel(session.project),
|
|
106
|
+
gitBranch: safeLabel(session.gitBranch, 120)
|
|
107
|
+
}));
|
|
108
|
+
if (next.device)
|
|
109
|
+
next.device.name = safeLabel(next.device.name, 120);
|
|
110
|
+
while (snapshotBytes(next) > targetBytes && next.sessions.length > 20)
|
|
111
|
+
next.sessions.pop();
|
|
112
|
+
while (snapshotBytes(next) > targetBytes && next.rollups.length > 30)
|
|
113
|
+
next.rollups.shift();
|
|
114
|
+
while (snapshotBytes(next) > targetBytes && next.recentTurns.length > 8)
|
|
115
|
+
next.recentTurns.shift();
|
|
116
|
+
if (snapshotBytes(next) > targetBytes) {
|
|
117
|
+
next.recentTurns = next.recentTurns.map((turn) => ({ ...turn, fileReads: void 0 }));
|
|
118
|
+
}
|
|
119
|
+
if (snapshotBytes(next) > targetBytes) {
|
|
120
|
+
throw new Error(`Dashboard snapshot exceeds the ${targetBytes} byte safety limit after pruning.`);
|
|
121
|
+
}
|
|
122
|
+
return next;
|
|
123
|
+
}
|
|
124
|
+
function snapshotBytes(snapshot2) {
|
|
125
|
+
return new TextEncoder().encode(JSON.stringify(snapshot2)).byteLength;
|
|
126
|
+
}
|
|
127
|
+
function mergeRecentTurns(existing, incoming) {
|
|
128
|
+
const byId = new Map(existing.map((turn) => [turn.turnId, turn]));
|
|
129
|
+
for (const turn of incoming)
|
|
130
|
+
byId.set(turn.turnId, turn);
|
|
131
|
+
return [...byId.values()].sort((a, b) => a.ts.localeCompare(b.ts)).slice(-SNAPSHOT_RECENT_TURNS);
|
|
132
|
+
}
|
|
133
|
+
function sanitizeTurn(turn) {
|
|
134
|
+
return {
|
|
135
|
+
...turn,
|
|
136
|
+
cwd: void 0,
|
|
137
|
+
project: safeLabel(turn.project),
|
|
138
|
+
gitBranch: safeLabel(turn.gitBranch, 120),
|
|
139
|
+
toolName: safeLabel(turn.toolName ?? void 0, 80) || void 0,
|
|
140
|
+
fileReads: turn.fileReads?.slice(0, 20).map((read) => ({
|
|
141
|
+
...read,
|
|
142
|
+
path: safePath(read.path)
|
|
143
|
+
}))
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function safeLabel(value, max = 160) {
|
|
147
|
+
if (!value)
|
|
148
|
+
return "";
|
|
149
|
+
return [...value].filter((character) => {
|
|
150
|
+
const code = character.charCodeAt(0);
|
|
151
|
+
return code > 31 && code !== 127;
|
|
152
|
+
}).join("").trim().slice(0, max);
|
|
153
|
+
}
|
|
154
|
+
function safePath(value) {
|
|
155
|
+
const normalized = value.replaceAll("\\", "/");
|
|
156
|
+
const parts = normalized.split("/").filter((part) => part && part !== "." && part !== ".." && !/^[A-Za-z]:$/.test(part));
|
|
157
|
+
return parts.slice(-3).join("/").slice(-200);
|
|
158
|
+
}
|
|
159
|
+
function mergeSessions(existing, turns) {
|
|
160
|
+
const byId = new Map(existing.map((session) => [session.sessionId, { ...session }]));
|
|
161
|
+
for (const turn of turns) {
|
|
162
|
+
const session = byId.get(turn.sessionId) ?? emptySession(turn);
|
|
163
|
+
session.turnCount++;
|
|
164
|
+
session.inputTokens += turn.inputTokens;
|
|
165
|
+
session.outputTokens += turn.outputTokens;
|
|
166
|
+
session.cacheReadTokens += turn.cacheReadTokens;
|
|
167
|
+
session.cacheCreationTokens += turn.cacheCreationTokens;
|
|
168
|
+
session.cachedInputTokens += turn.cachedInputTokens;
|
|
169
|
+
session.reasoningTokens += turn.reasoningTokens;
|
|
170
|
+
if (!session.firstTs || turn.ts < session.firstTs)
|
|
171
|
+
session.firstTs = turn.ts;
|
|
172
|
+
if (!session.lastTs || turn.ts > session.lastTs)
|
|
173
|
+
session.lastTs = turn.ts;
|
|
174
|
+
if (turn.project && session.project === "unknown")
|
|
175
|
+
session.project = turn.project;
|
|
176
|
+
if (turn.gitBranch && !session.gitBranch)
|
|
177
|
+
session.gitBranch = turn.gitBranch;
|
|
178
|
+
if (turn.model && (!session.model || modelPriority(turn.model) > modelPriority(session.model))) {
|
|
179
|
+
session.model = turn.model;
|
|
180
|
+
}
|
|
181
|
+
byId.set(turn.sessionId, session);
|
|
182
|
+
}
|
|
183
|
+
return [...byId.values()].sort((a, b) => b.lastTs.localeCompare(a.lastTs)).slice(0, SNAPSHOT_SESSIONS);
|
|
184
|
+
}
|
|
185
|
+
function emptySession(turn) {
|
|
186
|
+
return {
|
|
187
|
+
sessionId: turn.sessionId,
|
|
188
|
+
provider: turn.provider,
|
|
189
|
+
project: turn.project || "unknown",
|
|
190
|
+
gitBranch: turn.gitBranch || "",
|
|
72
191
|
firstTs: "",
|
|
73
192
|
lastTs: "",
|
|
74
|
-
model: "",
|
|
193
|
+
model: turn.model || "",
|
|
75
194
|
turnCount: 0,
|
|
76
195
|
inputTokens: 0,
|
|
77
196
|
outputTokens: 0,
|
|
@@ -80,124 +199,59 @@ function aggregateSession(sessionId, turns) {
|
|
|
80
199
|
cachedInputTokens: 0,
|
|
81
200
|
reasoningTokens: 0
|
|
82
201
|
};
|
|
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
202
|
}
|
|
121
|
-
function
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
203
|
+
function mergeRollups(existing, turns, now) {
|
|
204
|
+
const byId = new Map(existing.map((rollup) => [rollup.id, structuredClone(rollup)]));
|
|
205
|
+
for (const turn of turns) {
|
|
206
|
+
const day = dayOf(turn.ts);
|
|
207
|
+
if (!day)
|
|
208
|
+
continue;
|
|
209
|
+
const id = rollupId(turn.provider, day);
|
|
210
|
+
const rollup = byId.get(id) ?? { id, provider: turn.provider, day, models: {}, hourly: [] };
|
|
211
|
+
const model = turn.model || "unknown";
|
|
212
|
+
const totals = rollup.models[model] ??= {
|
|
213
|
+
model,
|
|
214
|
+
turns: 0,
|
|
215
|
+
inputTokens: 0,
|
|
216
|
+
outputTokens: 0,
|
|
217
|
+
cacheReadTokens: 0,
|
|
218
|
+
cacheCreationTokens: 0,
|
|
219
|
+
cachedInputTokens: 0,
|
|
220
|
+
reasoningTokens: 0
|
|
221
|
+
};
|
|
222
|
+
totals.turns++;
|
|
223
|
+
totals.inputTokens += turn.inputTokens;
|
|
224
|
+
totals.outputTokens += turn.outputTokens;
|
|
225
|
+
totals.cacheReadTokens += turn.cacheReadTokens;
|
|
226
|
+
totals.cacheCreationTokens += turn.cacheCreationTokens;
|
|
227
|
+
totals.cachedInputTokens += turn.cachedInputTokens;
|
|
228
|
+
totals.reasoningTokens += turn.reasoningTokens;
|
|
229
|
+
const hour = hourOf(turn.ts);
|
|
230
|
+
if (hour !== null) {
|
|
231
|
+
const bucket = rollup.hourly.find((item) => item.hour === hour) ?? { hour, output: 0, turns: 0 };
|
|
232
|
+
bucket.output += turn.outputTokens;
|
|
138
233
|
bucket.turns++;
|
|
139
|
-
|
|
234
|
+
if (!rollup.hourly.some((item) => item.hour === hour))
|
|
235
|
+
rollup.hourly.push(bucket);
|
|
236
|
+
rollup.hourly.sort((a, b) => a.hour - b.hour);
|
|
140
237
|
}
|
|
238
|
+
byId.set(id, rollup);
|
|
141
239
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
models,
|
|
147
|
-
hourly: [...hours.values()].sort((a, b) => a.hour - b.hour)
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
var TIMELINE_BUCKET_MS = 5 * 60 * 1e3;
|
|
151
|
-
var TIMELINE_RETENTION_MS = 8 * 24 * 60 * 60 * 1e3;
|
|
152
|
-
function bucketStartMs(ts) {
|
|
153
|
-
const ms = Date.parse(ts);
|
|
154
|
-
if (!Number.isFinite(ms))
|
|
155
|
-
return null;
|
|
156
|
-
return Math.floor(ms / TIMELINE_BUCKET_MS) * TIMELINE_BUCKET_MS;
|
|
240
|
+
const cutoff = new Date(now);
|
|
241
|
+
cutoff.setUTCDate(cutoff.getUTCDate() - (SNAPSHOT_RETENTION_DAYS - 1));
|
|
242
|
+
const cutoffDay = cutoff.toISOString().slice(0, 10);
|
|
243
|
+
return [...byId.values()].filter((rollup) => rollup.day >= cutoffDay).sort((a, b) => a.day.localeCompare(b.day) || a.provider.localeCompare(b.provider));
|
|
157
244
|
}
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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()] };
|
|
245
|
+
function mergeLimits(existing, incoming) {
|
|
246
|
+
const byProvider = new Map(existing.map((limit) => [limit.provider, limit]));
|
|
247
|
+
for (const limit of incoming)
|
|
248
|
+
byProvider.set(limit.provider, limit);
|
|
249
|
+
return [...byProvider.values()];
|
|
196
250
|
}
|
|
197
251
|
|
|
198
252
|
// ../packages/core/dist/providers/claude.js
|
|
199
253
|
import * as os from "node:os";
|
|
200
|
-
import * as
|
|
254
|
+
import * as path3 from "node:path";
|
|
201
255
|
|
|
202
256
|
// ../packages/core/dist/fs-scan.js
|
|
203
257
|
import { promises as fs } from "node:fs";
|
|
@@ -261,13 +315,144 @@ async function readJsonlFrom(filePath, fromLine) {
|
|
|
261
315
|
return { totalLines: lines.length, entries };
|
|
262
316
|
}
|
|
263
317
|
|
|
318
|
+
// ../packages/core/dist/providers/file-read.js
|
|
319
|
+
import { statSync } from "node:fs";
|
|
320
|
+
import * as path2 from "node:path";
|
|
321
|
+
var BYTES_PER_TOKEN = 4;
|
|
322
|
+
var APPROX_BYTES_PER_LINE = 120;
|
|
323
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
324
|
+
".7z",
|
|
325
|
+
".avif",
|
|
326
|
+
".bmp",
|
|
327
|
+
".class",
|
|
328
|
+
".dll",
|
|
329
|
+
".dylib",
|
|
330
|
+
".exe",
|
|
331
|
+
".gif",
|
|
332
|
+
".gz",
|
|
333
|
+
".ico",
|
|
334
|
+
".jar",
|
|
335
|
+
".jpeg",
|
|
336
|
+
".jpg",
|
|
337
|
+
".mov",
|
|
338
|
+
".mp3",
|
|
339
|
+
".mp4",
|
|
340
|
+
".pdf",
|
|
341
|
+
".png",
|
|
342
|
+
".so",
|
|
343
|
+
".tar",
|
|
344
|
+
".wav",
|
|
345
|
+
".webm",
|
|
346
|
+
".webp",
|
|
347
|
+
".zip"
|
|
348
|
+
]);
|
|
349
|
+
function isGeneratedPath(value) {
|
|
350
|
+
const normalized = value.replace(/\\/g, "/").toLowerCase();
|
|
351
|
+
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);
|
|
352
|
+
}
|
|
353
|
+
function isTextTokenCandidatePath(value) {
|
|
354
|
+
return !BINARY_EXTENSIONS.has(path2.extname(value).toLowerCase());
|
|
355
|
+
}
|
|
356
|
+
function displayPath(absolutePath, cwd) {
|
|
357
|
+
const normalized = absolutePath.replace(/\\/g, "/");
|
|
358
|
+
const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
359
|
+
if (normalizedCwd && normalized.toLowerCase().startsWith(`${normalizedCwd.toLowerCase()}/`)) {
|
|
360
|
+
return normalized.slice(normalizedCwd.length + 1);
|
|
361
|
+
}
|
|
362
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
363
|
+
return parts.slice(-3).join("/") || path2.basename(absolutePath);
|
|
364
|
+
}
|
|
365
|
+
function fileReadFromPath(rawPath, cwd, limitLines) {
|
|
366
|
+
if (!rawPath.trim())
|
|
367
|
+
return null;
|
|
368
|
+
if (!isTextTokenCandidatePath(rawPath))
|
|
369
|
+
return null;
|
|
370
|
+
const rawIsAbsolute = path2.isAbsolute(rawPath);
|
|
371
|
+
const absolutePath = rawIsAbsolute ? path2.normalize(rawPath) : path2.resolve(cwd || process.cwd(), rawPath);
|
|
372
|
+
let bytes = 0;
|
|
373
|
+
try {
|
|
374
|
+
const stat4 = statSync(absolutePath);
|
|
375
|
+
if (!stat4.isFile())
|
|
376
|
+
return null;
|
|
377
|
+
bytes = stat4.size;
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
if (limitLines && limitLines > 0 && bytes > 0) {
|
|
381
|
+
bytes = Math.min(bytes, limitLines * APPROX_BYTES_PER_LINE);
|
|
382
|
+
}
|
|
383
|
+
const normalizedRaw = rawPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
384
|
+
const safePath2 = !rawIsAbsolute && !normalizedRaw.startsWith("../") ? normalizedRaw : displayPath(absolutePath, cwd);
|
|
385
|
+
return {
|
|
386
|
+
path: safePath2,
|
|
387
|
+
bytes,
|
|
388
|
+
estimatedTokens: bytes > 0 ? Math.ceil(bytes / BYTES_PER_TOKEN) : 0,
|
|
389
|
+
generated: isGeneratedPath(safePath2)
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function parseJsonObject(value) {
|
|
393
|
+
if (typeof value !== "string")
|
|
394
|
+
return {};
|
|
395
|
+
try {
|
|
396
|
+
const parsed = JSON.parse(value);
|
|
397
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
398
|
+
} catch {
|
|
399
|
+
return {};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function candidateAfterCommand(command, tool) {
|
|
403
|
+
const escaped = tool.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
404
|
+
const rx = new RegExp(`(?:^|[;|&]\\s*)${escaped}\\s+(?:(?:-[\\w-]+)(?:\\s+\\d+)?\\s+)*(?:"([^"]+)"|'([^']+)'|([^\\s;|&]+))`, "gim");
|
|
405
|
+
const out = [];
|
|
406
|
+
for (const match of command.matchAll(rx)) {
|
|
407
|
+
const value = match[1] ?? match[2] ?? match[3];
|
|
408
|
+
if (value && !value.startsWith("-") && !value.startsWith("$") && !value.startsWith("(") && !value.startsWith("%")) {
|
|
409
|
+
out.push(value);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
function fileReadsFromCodexCall(name, argsValue, fallbackCwd) {
|
|
415
|
+
const args = parseJsonObject(argsValue);
|
|
416
|
+
const cwd = typeof args["workdir"] === "string" ? args["workdir"] : fallbackCwd;
|
|
417
|
+
if (name === "view_image")
|
|
418
|
+
return [];
|
|
419
|
+
if (name !== "shell_command")
|
|
420
|
+
return [];
|
|
421
|
+
const command = typeof args["command"] === "string" ? args["command"] : "";
|
|
422
|
+
const candidates = [
|
|
423
|
+
...candidateAfterCommand(command, "Get-Content"),
|
|
424
|
+
...candidateAfterCommand(command, "gc"),
|
|
425
|
+
...candidateAfterCommand(command, "cat"),
|
|
426
|
+
...candidateAfterCommand(command, "type"),
|
|
427
|
+
...candidateAfterCommand(command, "more"),
|
|
428
|
+
...candidateAfterCommand(command, "head"),
|
|
429
|
+
...candidateAfterCommand(command, "tail")
|
|
430
|
+
];
|
|
431
|
+
return [...new Set(candidates)].flatMap((candidate) => {
|
|
432
|
+
const read = fileReadFromPath(candidate, cwd);
|
|
433
|
+
return read ? [read] : [];
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function fillMissingReadEstimate(reads, output) {
|
|
437
|
+
if (typeof output !== "string" || output.length === 0)
|
|
438
|
+
return;
|
|
439
|
+
const missing = reads.filter((read) => read.estimatedTokens === 0);
|
|
440
|
+
if (missing.length === 0)
|
|
441
|
+
return;
|
|
442
|
+
const each = Math.max(1, Math.ceil(output.length / BYTES_PER_TOKEN / missing.length));
|
|
443
|
+
for (const read of missing) {
|
|
444
|
+
read.estimatedTokens = each;
|
|
445
|
+
read.bytes = each * BYTES_PER_TOKEN;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
264
449
|
// ../packages/core/dist/providers/claude.js
|
|
265
450
|
var MTIME_EPSILON_MS = 1;
|
|
266
451
|
function defaultClaudeDirs(home = os.homedir()) {
|
|
267
452
|
return [
|
|
268
|
-
|
|
453
|
+
path3.join(home, ".claude", "projects"),
|
|
269
454
|
// Xcode coding-assistant integration (macOS); silently skipped if absent.
|
|
270
|
-
|
|
455
|
+
path3.join(home, "Library", "Developer", "Xcode", "CodingAssistant", "ClaudeAgentConfig", "projects")
|
|
271
456
|
];
|
|
272
457
|
}
|
|
273
458
|
function parseClaudeEntries(entries) {
|
|
@@ -292,14 +477,20 @@ function parseClaudeEntries(entries) {
|
|
|
292
477
|
if (inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens === 0)
|
|
293
478
|
continue;
|
|
294
479
|
let toolName = null;
|
|
480
|
+
const fileReads = [];
|
|
481
|
+
const cwd = rec.cwd ?? "";
|
|
295
482
|
for (const item of msg.content ?? []) {
|
|
296
483
|
if (item && typeof item === "object" && item.type === "tool_use") {
|
|
297
|
-
toolName
|
|
298
|
-
|
|
484
|
+
toolName ??= item.name ?? null;
|
|
485
|
+
if (item.name === "Read" && typeof item.input?.file_path === "string") {
|
|
486
|
+
const limit = typeof item.input.limit === "number" ? item.input.limit : void 0;
|
|
487
|
+
const read = fileReadFromPath(item.input.file_path, cwd, limit);
|
|
488
|
+
if (read)
|
|
489
|
+
fileReads.push(read);
|
|
490
|
+
}
|
|
299
491
|
}
|
|
300
492
|
}
|
|
301
493
|
const messageId = msg.id ?? "";
|
|
302
|
-
const cwd = rec.cwd ?? "";
|
|
303
494
|
const turn = {
|
|
304
495
|
provider: "claude",
|
|
305
496
|
turnId: messageId ? `claude:${messageId}` : `claude:noid:${sessionId}:${lineNo}`,
|
|
@@ -313,6 +504,7 @@ function parseClaudeEntries(entries) {
|
|
|
313
504
|
cachedInputTokens: 0,
|
|
314
505
|
reasoningTokens: 0,
|
|
315
506
|
toolName,
|
|
507
|
+
fileReads,
|
|
316
508
|
project: projectNameFromCwd(cwd),
|
|
317
509
|
cwd,
|
|
318
510
|
gitBranch: rec.gitBranch ?? ""
|
|
@@ -324,6 +516,63 @@ function parseClaudeEntries(entries) {
|
|
|
324
516
|
}
|
|
325
517
|
return [...noId, ...seen.values()];
|
|
326
518
|
}
|
|
519
|
+
function asNumber(v) {
|
|
520
|
+
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
|
|
521
|
+
return Number.isFinite(n) ? n : void 0;
|
|
522
|
+
}
|
|
523
|
+
function labelClaudeWindow(id) {
|
|
524
|
+
const k = id.toLowerCase();
|
|
525
|
+
if (k.includes("5") || k.includes("session"))
|
|
526
|
+
return "Session (5hr)";
|
|
527
|
+
if (k.includes("7") || k.includes("week"))
|
|
528
|
+
return "Weekly (7 day)";
|
|
529
|
+
return "Usage";
|
|
530
|
+
}
|
|
531
|
+
function collectClaudeWindows(value, prefix = "") {
|
|
532
|
+
if (!value || typeof value !== "object")
|
|
533
|
+
return [];
|
|
534
|
+
const obj = value;
|
|
535
|
+
const usedPercent = asNumber(obj["used_percentage"] ?? obj["usedPercent"] ?? obj["used_percent"]);
|
|
536
|
+
const resetsRaw = obj["resets_at"] ?? obj["resetsAt"] ?? obj["reset_at"];
|
|
537
|
+
const resetsAt = typeof resetsRaw === "string" ? resetsRaw : asNumber(resetsRaw) !== void 0 ? new Date(asNumber(resetsRaw) * 1e3).toISOString() : void 0;
|
|
538
|
+
const resetAfterSeconds = asNumber(obj["resets_in_seconds"] ?? obj["reset_after_seconds"] ?? obj["resetAfterSeconds"]);
|
|
539
|
+
const current = usedPercent === void 0 ? [] : [
|
|
540
|
+
{
|
|
541
|
+
id: prefix || "usage",
|
|
542
|
+
label: labelClaudeWindow(prefix || "usage"),
|
|
543
|
+
usedPercent,
|
|
544
|
+
resetsAt,
|
|
545
|
+
resetAfterSeconds
|
|
546
|
+
}
|
|
547
|
+
];
|
|
548
|
+
for (const [key, child] of Object.entries(obj)) {
|
|
549
|
+
if (!child || typeof child !== "object")
|
|
550
|
+
continue;
|
|
551
|
+
current.push(...collectClaudeWindows(child, prefix ? `${prefix}.${key}` : key));
|
|
552
|
+
}
|
|
553
|
+
return current;
|
|
554
|
+
}
|
|
555
|
+
function parseClaudeLimitSnapshots(entries, syncedAt = /* @__PURE__ */ new Date()) {
|
|
556
|
+
let latest = null;
|
|
557
|
+
for (const { record } of entries) {
|
|
558
|
+
if (!record || typeof record !== "object")
|
|
559
|
+
continue;
|
|
560
|
+
const rec = record;
|
|
561
|
+
const rateLimits = rec.rateLimits ?? rec["rate_limits"];
|
|
562
|
+
if (!rateLimits)
|
|
563
|
+
continue;
|
|
564
|
+
const windows = collectClaudeWindows(rateLimits).filter((w, idx, arr) => arr.findIndex((x) => x.id === w.id) === idx);
|
|
565
|
+
if (windows.length === 0)
|
|
566
|
+
continue;
|
|
567
|
+
latest = {
|
|
568
|
+
provider: "claude",
|
|
569
|
+
source: "claude.rateLimits",
|
|
570
|
+
updatedAt: syncedAt.toISOString(),
|
|
571
|
+
windows
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return latest ? [latest] : [];
|
|
575
|
+
}
|
|
327
576
|
var ClaudeConnector = class {
|
|
328
577
|
id = "claude";
|
|
329
578
|
dirs;
|
|
@@ -336,6 +585,7 @@ var ClaudeConnector = class {
|
|
|
336
585
|
async collect(state) {
|
|
337
586
|
const nextFiles = { ...state.files };
|
|
338
587
|
const turns = [];
|
|
588
|
+
const scannedEntries = [];
|
|
339
589
|
for (const dir of this.dirs) {
|
|
340
590
|
const files = await walkFiles(dir, (name) => name.endsWith(".jsonl"));
|
|
341
591
|
for (const filePath of files) {
|
|
@@ -348,22 +598,30 @@ var ClaudeConnector = class {
|
|
|
348
598
|
}
|
|
349
599
|
const fromLine = cursor ? cursor.lines : 0;
|
|
350
600
|
const { totalLines, entries } = await readJsonlFrom(filePath, fromLine);
|
|
601
|
+
scannedEntries.push(...entries);
|
|
351
602
|
if (entries.length > 0) {
|
|
352
603
|
turns.push(...parseClaudeEntries(entries));
|
|
353
604
|
}
|
|
354
605
|
nextFiles[filePath] = { mtimeMs, lines: totalLines };
|
|
355
606
|
}
|
|
356
607
|
}
|
|
357
|
-
return { turns, state: { files: nextFiles } };
|
|
608
|
+
return { turns, limits: parseClaudeLimitSnapshots(scannedEntries), state: { files: nextFiles } };
|
|
358
609
|
}
|
|
359
610
|
};
|
|
360
611
|
|
|
361
612
|
// ../packages/core/dist/providers/codex.js
|
|
613
|
+
import { promises as fs2 } from "node:fs";
|
|
362
614
|
import * as os2 from "node:os";
|
|
363
|
-
import * as
|
|
615
|
+
import * as path4 from "node:path";
|
|
364
616
|
var MTIME_EPSILON_MS2 = 1;
|
|
617
|
+
var RATE_LIMIT_REFRESH_MS = 6e4;
|
|
618
|
+
var MAX_RATE_LIMIT_LOG_BYTES = 16 * 1024 * 1024;
|
|
365
619
|
function defaultCodexDirs(home = os2.homedir()) {
|
|
366
|
-
return [
|
|
620
|
+
return [path4.join(home, ".codex", "sessions")];
|
|
621
|
+
}
|
|
622
|
+
function defaultCodexRateLimitFiles(home = os2.homedir()) {
|
|
623
|
+
const base = path4.join(home, ".codex");
|
|
624
|
+
return [path4.join(base, "logs_2.sqlite"), path4.join(base, "logs_2.sqlite-wal")];
|
|
367
625
|
}
|
|
368
626
|
function usageFromObj(obj) {
|
|
369
627
|
if (!obj || typeof obj !== "object")
|
|
@@ -452,8 +710,29 @@ function parseCodexEntries(fileId, entries) {
|
|
|
452
710
|
let prevCumulative = null;
|
|
453
711
|
let seq = 0;
|
|
454
712
|
let runningModel = meta.model;
|
|
713
|
+
let pendingReads = [];
|
|
714
|
+
let pendingToolName = null;
|
|
715
|
+
const readsByCall = /* @__PURE__ */ new Map();
|
|
455
716
|
for (const { record } of entries) {
|
|
456
|
-
const
|
|
717
|
+
const rec = record;
|
|
718
|
+
const payload = rec["payload"];
|
|
719
|
+
if (payload?.["type"] === "function_call") {
|
|
720
|
+
const name = typeof payload["name"] === "string" ? payload["name"] : "";
|
|
721
|
+
pendingToolName = name || pendingToolName;
|
|
722
|
+
const reads = fileReadsFromCodexCall(name, payload["arguments"], meta.cwd);
|
|
723
|
+
if (reads.length > 0) {
|
|
724
|
+
pendingReads = [...pendingReads, ...reads];
|
|
725
|
+
const callId = typeof payload["call_id"] === "string" ? payload["call_id"] : "";
|
|
726
|
+
if (callId)
|
|
727
|
+
readsByCall.set(callId, reads);
|
|
728
|
+
}
|
|
729
|
+
} else if (payload?.["type"] === "function_call_output") {
|
|
730
|
+
const callId = typeof payload["call_id"] === "string" ? payload["call_id"] : "";
|
|
731
|
+
const reads = readsByCall.get(callId);
|
|
732
|
+
if (reads)
|
|
733
|
+
fillMissingReadEstimate(reads, payload["output"]);
|
|
734
|
+
}
|
|
735
|
+
const recModel = pickModel(payload) ?? pickModel(record);
|
|
457
736
|
if (recModel)
|
|
458
737
|
runningModel = recModel;
|
|
459
738
|
const ev = extractUsageEvent(record);
|
|
@@ -489,26 +768,148 @@ function parseCodexEntries(fileId, entries) {
|
|
|
489
768
|
cacheCreationTokens: 0,
|
|
490
769
|
cachedInputTokens: delta.cachedInput,
|
|
491
770
|
reasoningTokens: delta.reasoning,
|
|
492
|
-
toolName:
|
|
771
|
+
toolName: pendingToolName,
|
|
772
|
+
fileReads: pendingReads,
|
|
493
773
|
project: meta.project,
|
|
494
774
|
cwd: meta.cwd,
|
|
495
775
|
gitBranch: meta.gitBranch
|
|
496
776
|
});
|
|
777
|
+
pendingReads = [];
|
|
778
|
+
pendingToolName = null;
|
|
779
|
+
readsByCall.clear();
|
|
497
780
|
seq++;
|
|
498
781
|
}
|
|
499
782
|
return turns;
|
|
500
783
|
}
|
|
501
784
|
function codexFileId(filePath) {
|
|
502
|
-
return
|
|
785
|
+
return path4.basename(filePath).replace(/\.jsonl$/i, "");
|
|
786
|
+
}
|
|
787
|
+
function asNumber2(v) {
|
|
788
|
+
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
|
|
789
|
+
return Number.isFinite(n) ? n : void 0;
|
|
790
|
+
}
|
|
791
|
+
function codexWindowLabel(id, minutes) {
|
|
792
|
+
if (minutes === 300)
|
|
793
|
+
return "5 hour usage limit";
|
|
794
|
+
if (minutes === 10080)
|
|
795
|
+
return "Weekly usage limit";
|
|
796
|
+
return id === "primary" ? "Primary usage limit" : id === "secondary" ? "Secondary usage limit" : "Usage limit";
|
|
797
|
+
}
|
|
798
|
+
function toNativeWindow(id, raw) {
|
|
799
|
+
if (!raw)
|
|
800
|
+
return null;
|
|
801
|
+
const usedPercent = asNumber2(raw.used_percent);
|
|
802
|
+
if (usedPercent === void 0)
|
|
803
|
+
return null;
|
|
804
|
+
const windowMinutes = asNumber2(raw.window_minutes);
|
|
805
|
+
const resetAfterSeconds = asNumber2(raw.reset_after_seconds);
|
|
806
|
+
const resetAtSeconds = asNumber2(raw.reset_at);
|
|
807
|
+
return {
|
|
808
|
+
id,
|
|
809
|
+
label: codexWindowLabel(id, windowMinutes),
|
|
810
|
+
usedPercent,
|
|
811
|
+
windowMinutes,
|
|
812
|
+
resetsAt: resetAtSeconds ? new Date(resetAtSeconds * 1e3).toISOString() : void 0,
|
|
813
|
+
resetAfterSeconds
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function extractJsonObject(text, start) {
|
|
817
|
+
let depth = 0;
|
|
818
|
+
let inString = false;
|
|
819
|
+
let escaped = false;
|
|
820
|
+
for (let i = start; i < text.length; i++) {
|
|
821
|
+
const ch = text[i];
|
|
822
|
+
if (inString) {
|
|
823
|
+
if (escaped) {
|
|
824
|
+
escaped = false;
|
|
825
|
+
} else if (ch === "\\") {
|
|
826
|
+
escaped = true;
|
|
827
|
+
} else if (ch === '"') {
|
|
828
|
+
inString = false;
|
|
829
|
+
}
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (ch === '"') {
|
|
833
|
+
inString = true;
|
|
834
|
+
} else if (ch === "{") {
|
|
835
|
+
depth++;
|
|
836
|
+
} else if (ch === "}") {
|
|
837
|
+
depth--;
|
|
838
|
+
if (depth === 0)
|
|
839
|
+
return text.slice(start, i + 1);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
function parseCodexRateLimitSnapshot(text, syncedAt = /* @__PURE__ */ new Date()) {
|
|
845
|
+
const needle = '"type":"codex.rate_limits"';
|
|
846
|
+
let pos = -1;
|
|
847
|
+
let latest = null;
|
|
848
|
+
while ((pos = text.indexOf(needle, pos + 1)) !== -1) {
|
|
849
|
+
const start = text.lastIndexOf("{", pos);
|
|
850
|
+
if (start < 0)
|
|
851
|
+
continue;
|
|
852
|
+
const json = extractJsonObject(text, start);
|
|
853
|
+
if (!json)
|
|
854
|
+
continue;
|
|
855
|
+
let payload;
|
|
856
|
+
try {
|
|
857
|
+
payload = JSON.parse(json);
|
|
858
|
+
} catch {
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (payload.type !== "codex.rate_limits")
|
|
862
|
+
continue;
|
|
863
|
+
const windows = [
|
|
864
|
+
toNativeWindow("primary", payload.rate_limits?.primary),
|
|
865
|
+
toNativeWindow("secondary", payload.rate_limits?.secondary)
|
|
866
|
+
].filter((w) => Boolean(w));
|
|
867
|
+
if (windows.length === 0)
|
|
868
|
+
continue;
|
|
869
|
+
latest = {
|
|
870
|
+
provider: "codex",
|
|
871
|
+
source: "codex.rate_limits",
|
|
872
|
+
planType: typeof payload.plan_type === "string" ? payload.plan_type : void 0,
|
|
873
|
+
updatedAt: syncedAt.toISOString(),
|
|
874
|
+
windows
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
return latest;
|
|
878
|
+
}
|
|
879
|
+
async function readCodexRateLimits(files) {
|
|
880
|
+
const chunks = [];
|
|
881
|
+
for (const file of files) {
|
|
882
|
+
try {
|
|
883
|
+
const stat4 = await fs2.stat(file);
|
|
884
|
+
const length = Math.min(stat4.size, MAX_RATE_LIMIT_LOG_BYTES);
|
|
885
|
+
const handle = await fs2.open(file, "r");
|
|
886
|
+
try {
|
|
887
|
+
const buffer = Buffer.alloc(length);
|
|
888
|
+
await handle.read(buffer, 0, length, Math.max(0, stat4.size - length));
|
|
889
|
+
chunks.push(buffer.toString("latin1"));
|
|
890
|
+
} finally {
|
|
891
|
+
await handle.close();
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (chunks.length === 0)
|
|
897
|
+
return [];
|
|
898
|
+
const snapshot2 = parseCodexRateLimitSnapshot(chunks.join("\n"), /* @__PURE__ */ new Date());
|
|
899
|
+
return snapshot2 ? [snapshot2] : [];
|
|
503
900
|
}
|
|
504
901
|
var CodexConnector = class {
|
|
505
902
|
id = "codex";
|
|
506
903
|
dirs;
|
|
507
|
-
|
|
904
|
+
rateLimitFiles;
|
|
905
|
+
cachedLimits = [];
|
|
906
|
+
nextLimitReadAt = 0;
|
|
907
|
+
constructor(dirs = defaultCodexDirs(), rateLimitFiles = defaultCodexRateLimitFiles()) {
|
|
508
908
|
this.dirs = dirs;
|
|
909
|
+
this.rateLimitFiles = rateLimitFiles;
|
|
509
910
|
}
|
|
510
911
|
watchPaths() {
|
|
511
|
-
return this.dirs;
|
|
912
|
+
return [...this.dirs, ...this.rateLimitFiles];
|
|
512
913
|
}
|
|
513
914
|
async collect(state) {
|
|
514
915
|
const nextFiles = { ...state.files };
|
|
@@ -525,44 +926,71 @@ var CodexConnector = class {
|
|
|
525
926
|
}
|
|
526
927
|
const { totalLines, entries } = await readJsonlFrom(filePath, 0);
|
|
527
928
|
if (entries.length > 0) {
|
|
528
|
-
|
|
929
|
+
const parsed = parseCodexEntries(codexFileId(filePath), entries);
|
|
930
|
+
if (cursor && totalLines >= cursor.lines) {
|
|
931
|
+
const previousCount = parseCodexEntries(codexFileId(filePath), entries.filter((entry) => entry.lineNo <= cursor.lines)).length;
|
|
932
|
+
turns.push(...parsed.slice(previousCount));
|
|
933
|
+
} else {
|
|
934
|
+
turns.push(...parsed);
|
|
935
|
+
}
|
|
529
936
|
}
|
|
530
937
|
nextFiles[filePath] = { mtimeMs, lines: totalLines };
|
|
531
938
|
}
|
|
532
939
|
}
|
|
533
|
-
|
|
940
|
+
if (Date.now() >= this.nextLimitReadAt) {
|
|
941
|
+
const latest = await readCodexRateLimits(this.rateLimitFiles);
|
|
942
|
+
if (latest.length > 0)
|
|
943
|
+
this.cachedLimits = latest;
|
|
944
|
+
this.nextLimitReadAt = Date.now() + RATE_LIMIT_REFRESH_MS;
|
|
945
|
+
}
|
|
946
|
+
return { turns, limits: this.cachedLimits, state: { files: nextFiles } };
|
|
534
947
|
}
|
|
535
948
|
};
|
|
536
949
|
|
|
537
950
|
// src/paths.ts
|
|
538
951
|
function configDir() {
|
|
539
|
-
return process.env.TOKELYTICS_HOME ??
|
|
952
|
+
return process.env.TOKELYTICS_HOME ?? path5.join(os3.homedir(), ".tokelytics");
|
|
540
953
|
}
|
|
541
|
-
var statePath = () =>
|
|
542
|
-
var
|
|
954
|
+
var statePath = () => path5.join(configDir(), "state.json");
|
|
955
|
+
var stateBackupPath = () => path5.join(configDir(), "state.backup.json");
|
|
956
|
+
var credsPath = () => path5.join(configDir(), "credentials.json");
|
|
957
|
+
var updatePath = () => path5.join(configDir(), "update.json");
|
|
958
|
+
var watchLockPath = () => path5.join(configDir(), "watch.lock");
|
|
543
959
|
async function ensureDir() {
|
|
544
|
-
await
|
|
960
|
+
await fs3.mkdir(configDir(), { recursive: true });
|
|
545
961
|
}
|
|
546
962
|
async function readJson(file) {
|
|
547
963
|
try {
|
|
548
|
-
return JSON.parse(await
|
|
964
|
+
return JSON.parse(await fs3.readFile(file, "utf-8"));
|
|
549
965
|
} catch {
|
|
550
966
|
return null;
|
|
551
967
|
}
|
|
552
968
|
}
|
|
553
|
-
async function writeJson(file, value) {
|
|
969
|
+
async function writeJson(file, value, backup) {
|
|
554
970
|
await ensureDir();
|
|
555
|
-
|
|
971
|
+
const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
972
|
+
await fs3.writeFile(temp, JSON.stringify(value, null, 2), "utf-8");
|
|
973
|
+
try {
|
|
974
|
+
if (backup) {
|
|
975
|
+
try {
|
|
976
|
+
await fs3.copyFile(file, backup);
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
await fs3.rename(temp, file);
|
|
981
|
+
} finally {
|
|
982
|
+
await fs3.rm(temp, { force: true }).catch(() => void 0);
|
|
983
|
+
}
|
|
556
984
|
}
|
|
557
985
|
async function loadState() {
|
|
558
|
-
const s = await readJson(statePath());
|
|
986
|
+
const s = await readJson(statePath()) ?? await readJson(stateBackupPath());
|
|
559
987
|
if (s && s.deviceId && s.scan) return s;
|
|
560
|
-
const fresh = { deviceId: randomUUID(), scan: emptyScanState() };
|
|
988
|
+
const fresh = { deviceId: randomUUID(), scan: emptyScanState(), publication: {} };
|
|
561
989
|
await writeJson(statePath(), fresh);
|
|
562
990
|
return fresh;
|
|
563
991
|
}
|
|
564
992
|
async function saveState(state) {
|
|
565
|
-
await writeJson(statePath(), state);
|
|
993
|
+
await writeJson(statePath(), state, stateBackupPath());
|
|
566
994
|
}
|
|
567
995
|
async function loadCredentials() {
|
|
568
996
|
return readJson(credsPath());
|
|
@@ -570,16 +998,59 @@ async function loadCredentials() {
|
|
|
570
998
|
async function saveCredentials(creds) {
|
|
571
999
|
await writeJson(credsPath(), creds);
|
|
572
1000
|
try {
|
|
573
|
-
await
|
|
1001
|
+
await fs3.chmod(credsPath(), 384);
|
|
574
1002
|
} catch {
|
|
575
1003
|
}
|
|
576
1004
|
}
|
|
577
1005
|
async function clearCredentials() {
|
|
578
1006
|
try {
|
|
579
|
-
await
|
|
1007
|
+
await fs3.rm(credsPath(), { force: true });
|
|
580
1008
|
} catch {
|
|
581
1009
|
}
|
|
582
1010
|
}
|
|
1011
|
+
async function loadUpdateState() {
|
|
1012
|
+
return await readJson(updatePath()) ?? {};
|
|
1013
|
+
}
|
|
1014
|
+
async function saveUpdateState(state) {
|
|
1015
|
+
await writeJson(updatePath(), state);
|
|
1016
|
+
}
|
|
1017
|
+
async function acquireWatchLock() {
|
|
1018
|
+
await ensureDir();
|
|
1019
|
+
const file = watchLockPath();
|
|
1020
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1021
|
+
try {
|
|
1022
|
+
const handle = await fs3.open(file, "wx");
|
|
1023
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
|
|
1024
|
+
await handle.close();
|
|
1025
|
+
let released = false;
|
|
1026
|
+
return {
|
|
1027
|
+
async release() {
|
|
1028
|
+
if (released) return;
|
|
1029
|
+
released = true;
|
|
1030
|
+
const lock = await readJson(file);
|
|
1031
|
+
if (lock?.pid === process.pid) await fs3.rm(file, { force: true });
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
const code = error.code;
|
|
1036
|
+
if (code !== "EEXIST") throw error;
|
|
1037
|
+
const lock = await readJson(file);
|
|
1038
|
+
if (lock?.pid && processIsAlive(lock.pid)) {
|
|
1039
|
+
throw new Error(`Tokelytics is already running (PID ${lock.pid}).`, { cause: error });
|
|
1040
|
+
}
|
|
1041
|
+
await fs3.rm(file, { force: true });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
throw new Error("Could not acquire the Tokelytics watcher lock.");
|
|
1045
|
+
}
|
|
1046
|
+
function processIsAlive(pid) {
|
|
1047
|
+
try {
|
|
1048
|
+
process.kill(pid, 0);
|
|
1049
|
+
return true;
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
return error.code === "EPERM";
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
583
1054
|
|
|
584
1055
|
// src/config.ts
|
|
585
1056
|
var DEFAULT_FIREBASE = {
|
|
@@ -613,7 +1084,7 @@ function fromEnv() {
|
|
|
613
1084
|
}
|
|
614
1085
|
async function fromFile() {
|
|
615
1086
|
try {
|
|
616
|
-
const raw = await
|
|
1087
|
+
const raw = await fs4.readFile(path6.join(configDir(), "config.json"), "utf-8");
|
|
617
1088
|
return JSON.parse(raw);
|
|
618
1089
|
} catch {
|
|
619
1090
|
return {};
|
|
@@ -656,7 +1127,7 @@ function openBrowser(url) {
|
|
|
656
1127
|
}
|
|
657
1128
|
}
|
|
658
1129
|
function captureOAuthCode(buildUrl) {
|
|
659
|
-
return new Promise((
|
|
1130
|
+
return new Promise((resolve4, reject) => {
|
|
660
1131
|
const server = http.createServer((req, res) => {
|
|
661
1132
|
const u = new URL(req.url ?? "/", "http://localhost");
|
|
662
1133
|
const code = u.searchParams.get("code");
|
|
@@ -666,7 +1137,7 @@ function captureOAuthCode(buildUrl) {
|
|
|
666
1137
|
`<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
1138
|
);
|
|
668
1139
|
server.close();
|
|
669
|
-
if (code)
|
|
1140
|
+
if (code) resolve4({ code });
|
|
670
1141
|
else reject(new Error(`OAuth failed: ${error ?? "no code returned"}`));
|
|
671
1142
|
});
|
|
672
1143
|
server.listen(0, "127.0.0.1", () => {
|
|
@@ -761,13 +1232,13 @@ var CORS_HEADERS = {
|
|
|
761
1232
|
"Access-Control-Allow-Headers": "Content-Type"
|
|
762
1233
|
};
|
|
763
1234
|
function readBody(req) {
|
|
764
|
-
return new Promise((
|
|
1235
|
+
return new Promise((resolve4, reject) => {
|
|
765
1236
|
let data = "";
|
|
766
1237
|
req.on("data", (chunk) => {
|
|
767
1238
|
data += chunk;
|
|
768
1239
|
if (data.length > 1e6) req.destroy();
|
|
769
1240
|
});
|
|
770
|
-
req.on("end", () =>
|
|
1241
|
+
req.on("end", () => resolve4(data));
|
|
771
1242
|
req.on("error", reject);
|
|
772
1243
|
});
|
|
773
1244
|
}
|
|
@@ -784,10 +1255,9 @@ async function exchangeRefreshToken(cfg, refreshToken) {
|
|
|
784
1255
|
async function browserHandoffLogin(cfg) {
|
|
785
1256
|
const nonce = randomBytes(5).toString("hex").toUpperCase();
|
|
786
1257
|
const TIMEOUT_MS = 5 * 6e4;
|
|
787
|
-
const payload = await new Promise((
|
|
788
|
-
let timer;
|
|
1258
|
+
const payload = await new Promise((resolve4, reject) => {
|
|
789
1259
|
function finish(act) {
|
|
790
|
-
|
|
1260
|
+
clearTimeout(timer);
|
|
791
1261
|
server.close();
|
|
792
1262
|
act();
|
|
793
1263
|
}
|
|
@@ -817,10 +1287,10 @@ async function browserHandoffLogin(cfg) {
|
|
|
817
1287
|
}
|
|
818
1288
|
res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
819
1289
|
res.end(JSON.stringify({ ok: true }));
|
|
820
|
-
finish(() =>
|
|
1290
|
+
finish(() => resolve4(body));
|
|
821
1291
|
}).catch((err) => finish(() => reject(err)));
|
|
822
1292
|
});
|
|
823
|
-
timer = setTimeout(
|
|
1293
|
+
const timer = setTimeout(
|
|
824
1294
|
() => finish(() => reject(new Error('Timed out waiting for browser sign-in. Run "tokelytics login" again.'))),
|
|
825
1295
|
TIMEOUT_MS
|
|
826
1296
|
);
|
|
@@ -876,13 +1346,300 @@ async function restoreSession(cfg) {
|
|
|
876
1346
|
}
|
|
877
1347
|
|
|
878
1348
|
// src/runner.ts
|
|
879
|
-
import * as
|
|
1349
|
+
import * as os5 from "node:os";
|
|
880
1350
|
|
|
881
1351
|
// src/connectors.ts
|
|
882
1352
|
function buildConnectors() {
|
|
883
1353
|
return [new ClaudeConnector(), new CodexConnector()];
|
|
884
1354
|
}
|
|
885
1355
|
|
|
1356
|
+
// src/provider-limits.ts
|
|
1357
|
+
import { promises as fs5 } from "node:fs";
|
|
1358
|
+
import * as os4 from "node:os";
|
|
1359
|
+
import * as path7 from "node:path";
|
|
1360
|
+
var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
1361
|
+
var CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
1362
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
1363
|
+
var REFRESH_INTERVAL_MS = 6e4;
|
|
1364
|
+
function asNumber3(value) {
|
|
1365
|
+
const number = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
1366
|
+
return Number.isFinite(number) ? number : void 0;
|
|
1367
|
+
}
|
|
1368
|
+
function asIso(value) {
|
|
1369
|
+
if (typeof value === "string") {
|
|
1370
|
+
const date = new Date(value);
|
|
1371
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : void 0;
|
|
1372
|
+
}
|
|
1373
|
+
const seconds = asNumber3(value);
|
|
1374
|
+
if (seconds === void 0 || seconds <= 0) return void 0;
|
|
1375
|
+
return new Date(seconds * 1e3).toISOString();
|
|
1376
|
+
}
|
|
1377
|
+
function snapshot(provider, source, status, message, updatedAt, windows = [], planType) {
|
|
1378
|
+
return {
|
|
1379
|
+
provider,
|
|
1380
|
+
source,
|
|
1381
|
+
status,
|
|
1382
|
+
statusMessage: message,
|
|
1383
|
+
planType,
|
|
1384
|
+
updatedAt: updatedAt.toISOString(),
|
|
1385
|
+
windows
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function parseClaudeUsageResponse(value, updatedAt = /* @__PURE__ */ new Date(), planType) {
|
|
1389
|
+
const response = value ?? {};
|
|
1390
|
+
const windows = [];
|
|
1391
|
+
const add = (id, label, minutes, bucket) => {
|
|
1392
|
+
const usedPercent = asNumber3(bucket?.utilization);
|
|
1393
|
+
if (usedPercent === void 0) return;
|
|
1394
|
+
windows.push({
|
|
1395
|
+
id,
|
|
1396
|
+
label,
|
|
1397
|
+
usedPercent,
|
|
1398
|
+
windowMinutes: minutes,
|
|
1399
|
+
resetsAt: asIso(bucket?.resets_at)
|
|
1400
|
+
});
|
|
1401
|
+
};
|
|
1402
|
+
add("session", "5-hour", 300, response.five_hour);
|
|
1403
|
+
add("weekly", "Weekly", 10080, response.seven_day);
|
|
1404
|
+
return snapshot(
|
|
1405
|
+
"claude",
|
|
1406
|
+
"claude.oauth_usage",
|
|
1407
|
+
windows.length > 0 ? "available" : "unavailable",
|
|
1408
|
+
windows.length > 0 ? void 0 : "Claude did not return allowance windows. Retrying automatically.",
|
|
1409
|
+
updatedAt,
|
|
1410
|
+
windows,
|
|
1411
|
+
planType
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
function parseCodexUsageResponse(value, updatedAt = /* @__PURE__ */ new Date()) {
|
|
1415
|
+
const response = value ?? {};
|
|
1416
|
+
const details = response.rate_limit;
|
|
1417
|
+
const windows = [];
|
|
1418
|
+
const add = (id, label, fallbackMinutes, window) => {
|
|
1419
|
+
const usedPercent = asNumber3(window?.used_percent);
|
|
1420
|
+
if (usedPercent === void 0) return;
|
|
1421
|
+
const windowSeconds = asNumber3(window?.limit_window_seconds);
|
|
1422
|
+
windows.push({
|
|
1423
|
+
id,
|
|
1424
|
+
label,
|
|
1425
|
+
usedPercent,
|
|
1426
|
+
windowMinutes: windowSeconds ? Math.round(windowSeconds / 60) : fallbackMinutes,
|
|
1427
|
+
resetsAt: asIso(window?.reset_at)
|
|
1428
|
+
});
|
|
1429
|
+
};
|
|
1430
|
+
add("primary", "5-hour", 300, details?.primary_window);
|
|
1431
|
+
add("secondary", "Weekly", 10080, details?.secondary_window);
|
|
1432
|
+
return snapshot(
|
|
1433
|
+
"codex",
|
|
1434
|
+
"codex.oauth_usage",
|
|
1435
|
+
windows.length > 0 ? "available" : "unavailable",
|
|
1436
|
+
windows.length > 0 ? void 0 : "Codex did not return allowance windows. Retrying automatically.",
|
|
1437
|
+
updatedAt,
|
|
1438
|
+
windows,
|
|
1439
|
+
typeof response.plan_type === "string" ? response.plan_type : void 0
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
async function readJson2(file) {
|
|
1443
|
+
try {
|
|
1444
|
+
return JSON.parse(await fs5.readFile(file, "utf-8"));
|
|
1445
|
+
} catch {
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
async function readClaudeCredentials(home, env) {
|
|
1450
|
+
const configDir2 = env.CLAUDE_CONFIG_DIR || path7.join(home, ".claude");
|
|
1451
|
+
const root = await readJson2(path7.join(configDir2, ".credentials.json"));
|
|
1452
|
+
const oauth = root?.["claudeAiOauth"];
|
|
1453
|
+
if (!oauth || typeof oauth !== "object") return null;
|
|
1454
|
+
const data = oauth;
|
|
1455
|
+
const accessToken = data["accessToken"];
|
|
1456
|
+
if (typeof accessToken !== "string" || !accessToken) return null;
|
|
1457
|
+
return {
|
|
1458
|
+
accessToken,
|
|
1459
|
+
expiresAt: asNumber3(data["expiresAt"]),
|
|
1460
|
+
planType: typeof data["subscriptionType"] === "string" ? data["subscriptionType"] : void 0
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
async function readCodexCredentials(home, env) {
|
|
1464
|
+
const configDir2 = env.CODEX_HOME || path7.join(home, ".codex");
|
|
1465
|
+
const root = await readJson2(path7.join(configDir2, "auth.json"));
|
|
1466
|
+
const tokens = root?.["tokens"];
|
|
1467
|
+
if (!tokens || typeof tokens !== "object") return null;
|
|
1468
|
+
const data = tokens;
|
|
1469
|
+
const accessToken = data["access_token"];
|
|
1470
|
+
if (typeof accessToken !== "string" || !accessToken) return null;
|
|
1471
|
+
return {
|
|
1472
|
+
accessToken,
|
|
1473
|
+
accountId: typeof data["account_id"] === "string" ? data["account_id"] : void 0
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
async function requestJson(fetcher, url, headers) {
|
|
1477
|
+
const response = await fetcher(url, {
|
|
1478
|
+
headers,
|
|
1479
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1480
|
+
});
|
|
1481
|
+
if (!response.ok) return { status: response.status };
|
|
1482
|
+
try {
|
|
1483
|
+
return { status: response.status, body: await response.json() };
|
|
1484
|
+
} catch {
|
|
1485
|
+
return { status: response.status };
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function authMessage(provider) {
|
|
1489
|
+
return `${provider} sign-in needs refresh. Open ${provider} Code once; Tokelytics will retry automatically.`;
|
|
1490
|
+
}
|
|
1491
|
+
var ClaudeLimitCollector = class {
|
|
1492
|
+
constructor(options) {
|
|
1493
|
+
this.options = options;
|
|
1494
|
+
}
|
|
1495
|
+
provider = "claude";
|
|
1496
|
+
cached = null;
|
|
1497
|
+
nextRefreshAt = 0;
|
|
1498
|
+
async collect() {
|
|
1499
|
+
const updatedAt = this.options.now();
|
|
1500
|
+
if (this.cached && updatedAt.getTime() < this.nextRefreshAt) return this.cached;
|
|
1501
|
+
this.nextRefreshAt = updatedAt.getTime() + REFRESH_INTERVAL_MS;
|
|
1502
|
+
const fresh = await this.collectFresh(updatedAt);
|
|
1503
|
+
if (fresh.status === "error" && this.cached?.status === "available") {
|
|
1504
|
+
return {
|
|
1505
|
+
...this.cached,
|
|
1506
|
+
statusMessage: "Latest refresh was delayed; showing the last provider-reported values."
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
this.cached = fresh;
|
|
1510
|
+
return fresh;
|
|
1511
|
+
}
|
|
1512
|
+
async collectFresh(updatedAt) {
|
|
1513
|
+
try {
|
|
1514
|
+
const credentials = await readClaudeCredentials(this.options.home, this.options.env);
|
|
1515
|
+
if (!credentials) {
|
|
1516
|
+
return snapshot(
|
|
1517
|
+
"claude",
|
|
1518
|
+
"claude.oauth_usage",
|
|
1519
|
+
"unavailable",
|
|
1520
|
+
"Claude Code is not signed in on this machine.",
|
|
1521
|
+
updatedAt
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
const result = await requestJson(this.options.fetcher, CLAUDE_USAGE_URL, {
|
|
1525
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
1526
|
+
"anthropic-beta": "oauth-2025-04-20"
|
|
1527
|
+
});
|
|
1528
|
+
if (result.status === 401 || result.status === 403) {
|
|
1529
|
+
return snapshot("claude", "claude.oauth_usage", "error", authMessage("Claude"), updatedAt);
|
|
1530
|
+
}
|
|
1531
|
+
if (result.status === 429) {
|
|
1532
|
+
return snapshot(
|
|
1533
|
+
"claude",
|
|
1534
|
+
"claude.oauth_usage",
|
|
1535
|
+
"error",
|
|
1536
|
+
"Claude temporarily limited usage checks. Retrying automatically.",
|
|
1537
|
+
updatedAt
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
if (!result.body) {
|
|
1541
|
+
return snapshot(
|
|
1542
|
+
"claude",
|
|
1543
|
+
"claude.oauth_usage",
|
|
1544
|
+
"error",
|
|
1545
|
+
"Claude usage service could not be reached. Retrying automatically.",
|
|
1546
|
+
updatedAt
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
return parseClaudeUsageResponse(result.body, updatedAt, credentials.planType);
|
|
1550
|
+
} catch {
|
|
1551
|
+
return snapshot(
|
|
1552
|
+
"claude",
|
|
1553
|
+
"claude.oauth_usage",
|
|
1554
|
+
"error",
|
|
1555
|
+
"Claude usage service could not be reached. Retrying automatically.",
|
|
1556
|
+
updatedAt
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
var CodexLimitCollector = class {
|
|
1562
|
+
constructor(options) {
|
|
1563
|
+
this.options = options;
|
|
1564
|
+
}
|
|
1565
|
+
provider = "codex";
|
|
1566
|
+
cached = null;
|
|
1567
|
+
nextRefreshAt = 0;
|
|
1568
|
+
async collect() {
|
|
1569
|
+
const updatedAt = this.options.now();
|
|
1570
|
+
if (this.cached && updatedAt.getTime() < this.nextRefreshAt) return this.cached;
|
|
1571
|
+
this.nextRefreshAt = updatedAt.getTime() + REFRESH_INTERVAL_MS;
|
|
1572
|
+
const fresh = await this.collectFresh(updatedAt);
|
|
1573
|
+
if (fresh.status === "error" && this.cached?.status === "available") {
|
|
1574
|
+
return {
|
|
1575
|
+
...this.cached,
|
|
1576
|
+
statusMessage: "Latest refresh was delayed; showing the last provider-reported values."
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
this.cached = fresh;
|
|
1580
|
+
return fresh;
|
|
1581
|
+
}
|
|
1582
|
+
async collectFresh(updatedAt) {
|
|
1583
|
+
try {
|
|
1584
|
+
const credentials = await readCodexCredentials(this.options.home, this.options.env);
|
|
1585
|
+
if (!credentials) {
|
|
1586
|
+
return snapshot(
|
|
1587
|
+
"codex",
|
|
1588
|
+
"codex.oauth_usage",
|
|
1589
|
+
"unavailable",
|
|
1590
|
+
"Codex is not signed in on this machine.",
|
|
1591
|
+
updatedAt
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
const headers = {
|
|
1595
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
1596
|
+
"User-Agent": "codex-cli"
|
|
1597
|
+
};
|
|
1598
|
+
if (credentials.accountId) headers["ChatGPT-Account-Id"] = credentials.accountId;
|
|
1599
|
+
const result = await requestJson(this.options.fetcher, CODEX_USAGE_URL, headers);
|
|
1600
|
+
if (result.status === 401 || result.status === 403) {
|
|
1601
|
+
return snapshot("codex", "codex.oauth_usage", "error", authMessage("Codex"), updatedAt);
|
|
1602
|
+
}
|
|
1603
|
+
if (result.status === 429) {
|
|
1604
|
+
return snapshot(
|
|
1605
|
+
"codex",
|
|
1606
|
+
"codex.oauth_usage",
|
|
1607
|
+
"error",
|
|
1608
|
+
"Codex temporarily limited usage checks. Retrying automatically.",
|
|
1609
|
+
updatedAt
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
if (!result.body) {
|
|
1613
|
+
return snapshot(
|
|
1614
|
+
"codex",
|
|
1615
|
+
"codex.oauth_usage",
|
|
1616
|
+
"error",
|
|
1617
|
+
"Codex usage service could not be reached. Retrying automatically.",
|
|
1618
|
+
updatedAt
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
return parseCodexUsageResponse(result.body, updatedAt);
|
|
1622
|
+
} catch {
|
|
1623
|
+
return snapshot(
|
|
1624
|
+
"codex",
|
|
1625
|
+
"codex.oauth_usage",
|
|
1626
|
+
"error",
|
|
1627
|
+
"Codex usage service could not be reached. Retrying automatically.",
|
|
1628
|
+
updatedAt
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
function buildNativeLimitCollectors(options = {}) {
|
|
1634
|
+
const resolved = {
|
|
1635
|
+
home: options.home ?? os4.homedir(),
|
|
1636
|
+
env: options.env ?? process.env,
|
|
1637
|
+
fetcher: options.fetcher ?? fetch,
|
|
1638
|
+
now: options.now ?? (() => /* @__PURE__ */ new Date())
|
|
1639
|
+
};
|
|
1640
|
+
return [new ClaudeLimitCollector(resolved), new CodexLimitCollector(resolved)];
|
|
1641
|
+
}
|
|
1642
|
+
|
|
886
1643
|
// src/firestore-rest.ts
|
|
887
1644
|
function encodeValue(v) {
|
|
888
1645
|
if (v === null || v === void 0) return { nullValue: null };
|
|
@@ -918,6 +1675,14 @@ function decodeFields(fields) {
|
|
|
918
1675
|
for (const [k, val] of Object.entries(fields)) out[k] = decodeValue(val);
|
|
919
1676
|
return out;
|
|
920
1677
|
}
|
|
1678
|
+
var FirestoreRestError = class extends Error {
|
|
1679
|
+
constructor(message, status, retryAfterMs) {
|
|
1680
|
+
super(message);
|
|
1681
|
+
this.status = status;
|
|
1682
|
+
this.retryAfterMs = retryAfterMs;
|
|
1683
|
+
this.name = "FirestoreRestError";
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
921
1686
|
var FirestoreRest = class {
|
|
922
1687
|
projectId;
|
|
923
1688
|
getToken;
|
|
@@ -947,15 +1712,20 @@ var FirestoreRest = class {
|
|
|
947
1712
|
const res = await fetch(this.url(`${this.docsRoot()}:commit`), {
|
|
948
1713
|
method: "POST",
|
|
949
1714
|
headers: await this.headers(),
|
|
950
|
-
body: JSON.stringify({ writes })
|
|
1715
|
+
body: JSON.stringify({ writes }),
|
|
1716
|
+
signal: AbortSignal.timeout(15e3)
|
|
951
1717
|
});
|
|
952
|
-
if (!res.ok) throw
|
|
1718
|
+
if (!res.ok) throw await firestoreError("commit", res);
|
|
953
1719
|
}
|
|
954
1720
|
/** Fetch a single document's decoded fields, or null if it doesn't exist. */
|
|
955
1721
|
async getDoc(name) {
|
|
956
|
-
const res = await fetch(this.url(name), {
|
|
1722
|
+
const res = await fetch(this.url(name), {
|
|
1723
|
+
method: "GET",
|
|
1724
|
+
headers: await this.headers(),
|
|
1725
|
+
signal: AbortSignal.timeout(15e3)
|
|
1726
|
+
});
|
|
957
1727
|
if (res.status === 404) return null;
|
|
958
|
-
if (!res.ok) throw
|
|
1728
|
+
if (!res.ok) throw await firestoreError("get", res);
|
|
959
1729
|
const doc = await res.json();
|
|
960
1730
|
return doc.fields ? decodeFields(doc.fields) : {};
|
|
961
1731
|
}
|
|
@@ -973,127 +1743,190 @@ var FirestoreRest = class {
|
|
|
973
1743
|
const res = await fetch(this.url(`${parent}:runQuery`), {
|
|
974
1744
|
method: "POST",
|
|
975
1745
|
headers: await this.headers(),
|
|
976
|
-
body: JSON.stringify({ structuredQuery: { from: [{ collectionId }], where } })
|
|
1746
|
+
body: JSON.stringify({ structuredQuery: { from: [{ collectionId }], where } }),
|
|
1747
|
+
signal: AbortSignal.timeout(15e3)
|
|
977
1748
|
});
|
|
978
|
-
if (!res.ok) throw
|
|
1749
|
+
if (!res.ok) throw await firestoreError("query", res);
|
|
979
1750
|
const rows = await res.json();
|
|
980
1751
|
return rows.filter((r) => r.document?.fields).map((r) => decodeFields(r.document.fields));
|
|
981
1752
|
}
|
|
982
1753
|
};
|
|
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;
|
|
1754
|
+
async function firestoreError(operation, response) {
|
|
1755
|
+
const body = (await response.text()).slice(0, 800);
|
|
1756
|
+
const retryAfter = response.headers.get("retry-after");
|
|
1757
|
+
const seconds = retryAfter ? Number(retryAfter) : Number.NaN;
|
|
1758
|
+
return new FirestoreRestError(
|
|
1759
|
+
`Firestore ${operation} failed (${response.status}): ${body}`,
|
|
1760
|
+
response.status,
|
|
1761
|
+
Number.isFinite(seconds) ? seconds * 1e3 : void 0
|
|
1762
|
+
);
|
|
998
1763
|
}
|
|
999
1764
|
|
|
1000
1765
|
// src/firestore-sink.ts
|
|
1001
|
-
var BATCH = 400;
|
|
1002
1766
|
var FirestoreSink = class {
|
|
1003
|
-
constructor(
|
|
1004
|
-
this.fs =
|
|
1767
|
+
constructor(fs6, uid) {
|
|
1768
|
+
this.fs = fs6;
|
|
1005
1769
|
this.uid = uid;
|
|
1006
1770
|
}
|
|
1007
|
-
async
|
|
1008
|
-
|
|
1009
|
-
|
|
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) {
|
|
1771
|
+
async writeDashboardSnapshot(snapshot2) {
|
|
1772
|
+
if (!snapshot2.device?.deviceId) throw new Error("Dashboard snapshot is missing its device id.");
|
|
1773
|
+
const cloudSnapshot = prepareDashboardSnapshotForCloud(snapshot2);
|
|
1066
1774
|
await this.fs.upsert([
|
|
1067
|
-
{
|
|
1775
|
+
{
|
|
1776
|
+
name: this.fs.docName("users", this.uid, "machines", snapshot2.device.deviceId),
|
|
1777
|
+
fields: { ...cloudSnapshot }
|
|
1778
|
+
}
|
|
1068
1779
|
]);
|
|
1069
1780
|
}
|
|
1070
1781
|
};
|
|
1071
1782
|
|
|
1783
|
+
// src/version.ts
|
|
1784
|
+
var AGENT_VERSION = "0.3.1";
|
|
1785
|
+
|
|
1072
1786
|
// src/sync.ts
|
|
1073
|
-
async function runSync(connectors, sink, state, device) {
|
|
1787
|
+
async function runSync(connectors, sink, state, device, limitCollectors = [], options = {}) {
|
|
1788
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1789
|
+
const refreshLimits = options.refreshLimits ?? true;
|
|
1790
|
+
const publication = {
|
|
1791
|
+
...options.publication,
|
|
1792
|
+
limitFingerprints: { ...options.publication?.limitFingerprints }
|
|
1793
|
+
};
|
|
1794
|
+
const cloudSyncIntervalMs = options.cloudSyncIntervalMs ?? 0;
|
|
1795
|
+
const maxCloudWritesPerDay = options.maxCloudWritesPerDay ?? 16;
|
|
1074
1796
|
let st = state;
|
|
1075
1797
|
const collected = [];
|
|
1798
|
+
const limits = [];
|
|
1076
1799
|
const byProvider = {};
|
|
1077
1800
|
for (const c of connectors) {
|
|
1078
|
-
const { turns: turns2, state: next } = await c.collect(st);
|
|
1801
|
+
const { turns: turns2, limits: nativeLimits2 = [], state: next } = await c.collect(st);
|
|
1079
1802
|
st = next;
|
|
1803
|
+
if (refreshLimits) limits.push(...nativeLimits2);
|
|
1080
1804
|
for (const t of turns2) {
|
|
1081
1805
|
collected.push(t);
|
|
1082
1806
|
byProvider[t.provider] = (byProvider[t.provider] ?? 0) + 1;
|
|
1083
1807
|
}
|
|
1084
1808
|
}
|
|
1809
|
+
if (refreshLimits) {
|
|
1810
|
+
const providerLimits = await Promise.all(
|
|
1811
|
+
limitCollectors.map(async (collector) => {
|
|
1812
|
+
try {
|
|
1813
|
+
return await collector.collect();
|
|
1814
|
+
} catch {
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
})
|
|
1818
|
+
);
|
|
1819
|
+
limits.push(...providerLimits.filter((limit) => Boolean(limit)));
|
|
1820
|
+
}
|
|
1085
1821
|
const unique = /* @__PURE__ */ new Map();
|
|
1086
1822
|
for (const t of collected) unique.set(t.turnId, t);
|
|
1087
1823
|
const turns = [...unique.values()];
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1824
|
+
const nativeLimits = refreshLimits ? dedupeLimits(limits) : [];
|
|
1825
|
+
const changedLimits = nativeLimits.filter(
|
|
1826
|
+
(limit) => publication.limitFingerprints?.[limit.provider] !== limitFingerprint(limit)
|
|
1827
|
+
);
|
|
1828
|
+
if (changedLimits.length > 0) {
|
|
1829
|
+
publication.limitFingerprints ??= {};
|
|
1830
|
+
for (const limit of changedLimits) {
|
|
1831
|
+
publication.limitFingerprints[limit.provider] = limitFingerprint(limit);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
if (refreshLimits) publication.lastLimitCheckAt = now.toISOString();
|
|
1835
|
+
const heartbeatMs = options.deviceHeartbeatMs ?? 0;
|
|
1836
|
+
const previousHeartbeat = Date.parse(publication.lastDeviceAt ?? "");
|
|
1837
|
+
const heartbeatDue = heartbeatMs === 0 || !Number.isFinite(previousHeartbeat) || now.getTime() - previousHeartbeat >= heartbeatMs;
|
|
1838
|
+
const updateDevice = turns.length > 0 || changedLimits.length > 0 || !options.dashboard?.device || options.forceDeviceHeartbeat || heartbeatDue;
|
|
1839
|
+
const dashboard = mergeDashboardSnapshot(
|
|
1840
|
+
options.dashboard,
|
|
1841
|
+
turns,
|
|
1842
|
+
changedLimits,
|
|
1843
|
+
updateDevice ? { ...device, lastSyncAt: now.toISOString() } : void 0,
|
|
1844
|
+
now
|
|
1845
|
+
);
|
|
1846
|
+
if (updateDevice) {
|
|
1847
|
+
publication.lastDeviceAt = now.toISOString();
|
|
1848
|
+
}
|
|
1849
|
+
const changed = turns.length > 0 || changedLimits.length > 0 || updateDevice;
|
|
1850
|
+
if (changed) publication.lastLocalEventAt = now.toISOString();
|
|
1851
|
+
publication.dashboardDirty = Boolean(publication.dashboardDirty || changed);
|
|
1852
|
+
resetDailyBudget(publication, now);
|
|
1853
|
+
const lastCloudWrite = Date.parse(publication.lastCloudWriteAt ?? "");
|
|
1854
|
+
const cloudWriteDue = options.forceCloudPublish || !Number.isFinite(lastCloudWrite) || now.getTime() - lastCloudWrite >= cloudSyncIntervalMs;
|
|
1855
|
+
const hasBudget = (publication.cloudWritesToday ?? 0) < maxCloudWritesPerDay;
|
|
1856
|
+
let published = false;
|
|
1857
|
+
let publicationError;
|
|
1858
|
+
dashboard.sync = {
|
|
1859
|
+
agentVersion: AGENT_VERSION,
|
|
1860
|
+
cloudWritesToday: publication.cloudWritesToday ?? 0,
|
|
1861
|
+
maxCloudWritesPerDay,
|
|
1862
|
+
localChangesPending: Boolean(publication.dashboardDirty),
|
|
1863
|
+
lastLocalEventAt: publication.lastLocalEventAt,
|
|
1864
|
+
lastCloudWriteAt: publication.lastCloudWriteAt
|
|
1865
|
+
};
|
|
1866
|
+
if (publication.dashboardDirty && cloudWriteDue && hasBudget) {
|
|
1867
|
+
try {
|
|
1868
|
+
dashboard.sync = {
|
|
1869
|
+
...dashboard.sync,
|
|
1870
|
+
cloudWritesToday: (publication.cloudWritesToday ?? 0) + 1,
|
|
1871
|
+
localChangesPending: false,
|
|
1872
|
+
lastCloudWriteAt: now.toISOString()
|
|
1873
|
+
};
|
|
1874
|
+
await sink.writeDashboardSnapshot(dashboard);
|
|
1875
|
+
publication.cloudWritesToday = (publication.cloudWritesToday ?? 0) + 1;
|
|
1876
|
+
publication.lastCloudWriteAt = now.toISOString();
|
|
1877
|
+
publication.dashboardDirty = false;
|
|
1878
|
+
published = true;
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
dashboard.sync.localChangesPending = true;
|
|
1881
|
+
publicationError = error;
|
|
1882
|
+
}
|
|
1094
1883
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1884
|
+
return {
|
|
1885
|
+
processedTurns: turns.length,
|
|
1886
|
+
processedLimits: changedLimits.length,
|
|
1887
|
+
byProvider,
|
|
1888
|
+
state: st,
|
|
1889
|
+
dashboard,
|
|
1890
|
+
publication,
|
|
1891
|
+
published,
|
|
1892
|
+
publicationError
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
function dedupeLimits(limits) {
|
|
1896
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
1897
|
+
for (const limit of limits) {
|
|
1898
|
+
const current = byProvider.get(limit.provider);
|
|
1899
|
+
if (!current || limitScore(limit) >= limitScore(current)) byProvider.set(limit.provider, limit);
|
|
1900
|
+
}
|
|
1901
|
+
return [...byProvider.values()];
|
|
1902
|
+
}
|
|
1903
|
+
function limitScore(limit) {
|
|
1904
|
+
const available = (limit.status ?? (limit.windows.length > 0 ? "available" : "unavailable")) === "available";
|
|
1905
|
+
const oauth = limit.source.endsWith("oauth_usage");
|
|
1906
|
+
return (available ? 100 : 0) + (oauth ? 10 : 0) + limit.windows.length;
|
|
1907
|
+
}
|
|
1908
|
+
function limitFingerprint(limit) {
|
|
1909
|
+
return JSON.stringify({
|
|
1910
|
+
provider: limit.provider,
|
|
1911
|
+
source: limit.source,
|
|
1912
|
+
status: limit.status,
|
|
1913
|
+
statusMessage: limit.statusMessage,
|
|
1914
|
+
planType: limit.planType,
|
|
1915
|
+
windows: limit.windows
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
function resetDailyBudget(publication, now) {
|
|
1919
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
1920
|
+
timeZone: "America/Los_Angeles",
|
|
1921
|
+
year: "numeric",
|
|
1922
|
+
month: "2-digit",
|
|
1923
|
+
day: "2-digit"
|
|
1924
|
+
}).formatToParts(now);
|
|
1925
|
+
const value = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
1926
|
+
const day = `${value("year")}-${value("month")}-${value("day")}`;
|
|
1927
|
+
if (publication.cloudWriteDay === day) return;
|
|
1928
|
+
publication.cloudWriteDay = day;
|
|
1929
|
+
publication.cloudWritesToday = 0;
|
|
1097
1930
|
}
|
|
1098
1931
|
|
|
1099
1932
|
// src/runner.ts
|
|
@@ -1102,6 +1935,7 @@ async function createRunner() {
|
|
|
1102
1935
|
const session = await restoreSession(cfg);
|
|
1103
1936
|
if (!session) throw new Error('Not signed in. Run "tokelytics login" first.');
|
|
1104
1937
|
const connectors = buildConnectors();
|
|
1938
|
+
const limitCollectors = buildNativeLimitCollectors();
|
|
1105
1939
|
const rest = new FirestoreRest({
|
|
1106
1940
|
projectId: cfg.firebase.projectId,
|
|
1107
1941
|
getToken: () => session.getToken(),
|
|
@@ -1113,34 +1947,55 @@ async function createRunner() {
|
|
|
1113
1947
|
watchPaths() {
|
|
1114
1948
|
return connectors.flatMap((c) => c.watchPaths());
|
|
1115
1949
|
},
|
|
1116
|
-
async syncOnce() {
|
|
1950
|
+
async syncOnce(options = {}) {
|
|
1117
1951
|
const state = await loadState();
|
|
1118
1952
|
const device = {
|
|
1119
1953
|
deviceId: state.deviceId,
|
|
1120
|
-
name:
|
|
1954
|
+
name: os5.hostname(),
|
|
1121
1955
|
lastSyncAt: "",
|
|
1122
1956
|
providers: connectors.map((c) => c.id)
|
|
1123
1957
|
};
|
|
1124
|
-
const
|
|
1125
|
-
await
|
|
1126
|
-
|
|
1958
|
+
const scan = state.dashboard ? state.scan : emptyScanState();
|
|
1959
|
+
const res = await runSync(connectors, sink, scan, device, limitCollectors, {
|
|
1960
|
+
...options,
|
|
1961
|
+
publication: state.publication,
|
|
1962
|
+
dashboard: state.dashboard
|
|
1963
|
+
});
|
|
1964
|
+
const nextState = {
|
|
1965
|
+
deviceId: state.deviceId,
|
|
1966
|
+
scan: res.state,
|
|
1967
|
+
dashboard: res.dashboard,
|
|
1968
|
+
publication: res.publication
|
|
1969
|
+
};
|
|
1970
|
+
if (!sameState(state, nextState)) await saveState(nextState);
|
|
1971
|
+
if (res.publicationError) throw res.publicationError;
|
|
1972
|
+
return {
|
|
1973
|
+
processedTurns: res.processedTurns,
|
|
1974
|
+
processedLimits: res.processedLimits,
|
|
1975
|
+
byProvider: res.byProvider,
|
|
1976
|
+
published: res.published,
|
|
1977
|
+
cloudWritesToday: res.publication.cloudWritesToday ?? 0
|
|
1978
|
+
};
|
|
1127
1979
|
}
|
|
1128
1980
|
};
|
|
1129
1981
|
}
|
|
1982
|
+
function sameState(left, right) {
|
|
1983
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1984
|
+
}
|
|
1130
1985
|
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
1986
|
const state = await loadState();
|
|
1138
1987
|
const connectors = buildConnectors();
|
|
1139
|
-
|
|
1988
|
+
const now = /* @__PURE__ */ new Date();
|
|
1989
|
+
const device = {
|
|
1140
1990
|
deviceId: state.deviceId,
|
|
1141
|
-
name:
|
|
1142
|
-
lastSyncAt:
|
|
1991
|
+
name: os5.hostname(),
|
|
1992
|
+
lastSyncAt: now.toISOString(),
|
|
1143
1993
|
providers: connectors.map((c) => c.id)
|
|
1994
|
+
};
|
|
1995
|
+
await saveState({
|
|
1996
|
+
...state,
|
|
1997
|
+
dashboard: mergeDashboardSnapshot(state.dashboard, [], [], device, now),
|
|
1998
|
+
publication: { ...state.publication, dashboardDirty: true, lastDeviceAt: now.toISOString() }
|
|
1144
1999
|
});
|
|
1145
2000
|
}
|
|
1146
2001
|
|
|
@@ -1220,7 +2075,7 @@ var ReaddirpStream = class extends Readable {
|
|
|
1220
2075
|
this._directoryFilter = normalizeFilter(opts.directoryFilter);
|
|
1221
2076
|
const statMethod = opts.lstat ? lstat : stat;
|
|
1222
2077
|
if (wantBigintFsStats) {
|
|
1223
|
-
this._stat = (
|
|
2078
|
+
this._stat = (path8) => statMethod(path8, { bigint: true });
|
|
1224
2079
|
} else {
|
|
1225
2080
|
this._stat = statMethod;
|
|
1226
2081
|
}
|
|
@@ -1245,8 +2100,8 @@ var ReaddirpStream = class extends Readable {
|
|
|
1245
2100
|
const par = this.parent;
|
|
1246
2101
|
const fil = par && par.files;
|
|
1247
2102
|
if (fil && fil.length > 0) {
|
|
1248
|
-
const { path:
|
|
1249
|
-
const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent,
|
|
2103
|
+
const { path: path8, depth } = par;
|
|
2104
|
+
const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path8));
|
|
1250
2105
|
const awaited = await Promise.all(slice);
|
|
1251
2106
|
for (const entry of awaited) {
|
|
1252
2107
|
if (!entry)
|
|
@@ -1286,21 +2141,21 @@ var ReaddirpStream = class extends Readable {
|
|
|
1286
2141
|
this.reading = false;
|
|
1287
2142
|
}
|
|
1288
2143
|
}
|
|
1289
|
-
async _exploreDir(
|
|
2144
|
+
async _exploreDir(path8, depth) {
|
|
1290
2145
|
let files;
|
|
1291
2146
|
try {
|
|
1292
|
-
files = await readdir(
|
|
2147
|
+
files = await readdir(path8, this._rdOptions);
|
|
1293
2148
|
} catch (error) {
|
|
1294
2149
|
this._onError(error);
|
|
1295
2150
|
}
|
|
1296
|
-
return { files, depth, path:
|
|
2151
|
+
return { files, depth, path: path8 };
|
|
1297
2152
|
}
|
|
1298
|
-
async _formatEntry(dirent,
|
|
2153
|
+
async _formatEntry(dirent, path8) {
|
|
1299
2154
|
let entry;
|
|
1300
|
-
const
|
|
2155
|
+
const basename5 = this._isDirent ? dirent.name : dirent;
|
|
1301
2156
|
try {
|
|
1302
|
-
const fullPath = presolve(pjoin(
|
|
1303
|
-
entry = { path: prelative(this._root, fullPath), fullPath, basename:
|
|
2157
|
+
const fullPath = presolve(pjoin(path8, basename5));
|
|
2158
|
+
entry = { path: prelative(this._root, fullPath), fullPath, basename: basename5 };
|
|
1304
2159
|
entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
|
|
1305
2160
|
} catch (err) {
|
|
1306
2161
|
this._onError(err);
|
|
@@ -1699,16 +2554,16 @@ var delFromSet = (main2, prop, item) => {
|
|
|
1699
2554
|
};
|
|
1700
2555
|
var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
|
|
1701
2556
|
var FsWatchInstances = /* @__PURE__ */ new Map();
|
|
1702
|
-
function createFsWatchInstance(
|
|
2557
|
+
function createFsWatchInstance(path8, options, listener, errHandler, emitRaw) {
|
|
1703
2558
|
const handleEvent = (rawEvent, evPath) => {
|
|
1704
|
-
listener(
|
|
1705
|
-
emitRaw(rawEvent, evPath, { watchedPath:
|
|
1706
|
-
if (evPath &&
|
|
1707
|
-
fsWatchBroadcast(sysPath.resolve(
|
|
2559
|
+
listener(path8);
|
|
2560
|
+
emitRaw(rawEvent, evPath, { watchedPath: path8 });
|
|
2561
|
+
if (evPath && path8 !== evPath) {
|
|
2562
|
+
fsWatchBroadcast(sysPath.resolve(path8, evPath), KEY_LISTENERS, sysPath.join(path8, evPath));
|
|
1708
2563
|
}
|
|
1709
2564
|
};
|
|
1710
2565
|
try {
|
|
1711
|
-
return fs_watch(
|
|
2566
|
+
return fs_watch(path8, {
|
|
1712
2567
|
persistent: options.persistent
|
|
1713
2568
|
}, handleEvent);
|
|
1714
2569
|
} catch (error) {
|
|
@@ -1724,12 +2579,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
|
|
|
1724
2579
|
listener(val1, val2, val3);
|
|
1725
2580
|
});
|
|
1726
2581
|
};
|
|
1727
|
-
var setFsWatchListener = (
|
|
2582
|
+
var setFsWatchListener = (path8, fullPath, options, handlers) => {
|
|
1728
2583
|
const { listener, errHandler, rawEmitter } = handlers;
|
|
1729
2584
|
let cont = FsWatchInstances.get(fullPath);
|
|
1730
2585
|
let watcher;
|
|
1731
2586
|
if (!options.persistent) {
|
|
1732
|
-
watcher = createFsWatchInstance(
|
|
2587
|
+
watcher = createFsWatchInstance(path8, options, listener, errHandler, rawEmitter);
|
|
1733
2588
|
if (!watcher)
|
|
1734
2589
|
return;
|
|
1735
2590
|
return watcher.close.bind(watcher);
|
|
@@ -1740,7 +2595,7 @@ var setFsWatchListener = (path6, fullPath, options, handlers) => {
|
|
|
1740
2595
|
addAndConvert(cont, KEY_RAW, rawEmitter);
|
|
1741
2596
|
} else {
|
|
1742
2597
|
watcher = createFsWatchInstance(
|
|
1743
|
-
|
|
2598
|
+
path8,
|
|
1744
2599
|
options,
|
|
1745
2600
|
fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
|
|
1746
2601
|
errHandler,
|
|
@@ -1755,7 +2610,7 @@ var setFsWatchListener = (path6, fullPath, options, handlers) => {
|
|
|
1755
2610
|
cont.watcherUnusable = true;
|
|
1756
2611
|
if (isWindows && error.code === "EPERM") {
|
|
1757
2612
|
try {
|
|
1758
|
-
const fd = await open(
|
|
2613
|
+
const fd = await open(path8, "r");
|
|
1759
2614
|
await fd.close();
|
|
1760
2615
|
broadcastErr(error);
|
|
1761
2616
|
} catch (err) {
|
|
@@ -1786,7 +2641,7 @@ var setFsWatchListener = (path6, fullPath, options, handlers) => {
|
|
|
1786
2641
|
};
|
|
1787
2642
|
};
|
|
1788
2643
|
var FsWatchFileInstances = /* @__PURE__ */ new Map();
|
|
1789
|
-
var setFsWatchFileListener = (
|
|
2644
|
+
var setFsWatchFileListener = (path8, fullPath, options, handlers) => {
|
|
1790
2645
|
const { listener, rawEmitter } = handlers;
|
|
1791
2646
|
let cont = FsWatchFileInstances.get(fullPath);
|
|
1792
2647
|
const copts = cont && cont.options;
|
|
@@ -1808,7 +2663,7 @@ var setFsWatchFileListener = (path6, fullPath, options, handlers) => {
|
|
|
1808
2663
|
});
|
|
1809
2664
|
const currmtime = curr.mtimeMs;
|
|
1810
2665
|
if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
|
|
1811
|
-
foreach(cont.listeners, (listener2) => listener2(
|
|
2666
|
+
foreach(cont.listeners, (listener2) => listener2(path8, curr));
|
|
1812
2667
|
}
|
|
1813
2668
|
})
|
|
1814
2669
|
};
|
|
@@ -1836,13 +2691,13 @@ var NodeFsHandler = class {
|
|
|
1836
2691
|
* @param listener on fs change
|
|
1837
2692
|
* @returns closer for the watcher instance
|
|
1838
2693
|
*/
|
|
1839
|
-
_watchWithNodeFs(
|
|
2694
|
+
_watchWithNodeFs(path8, listener) {
|
|
1840
2695
|
const opts = this.fsw.options;
|
|
1841
|
-
const directory = sysPath.dirname(
|
|
1842
|
-
const
|
|
2696
|
+
const directory = sysPath.dirname(path8);
|
|
2697
|
+
const basename5 = sysPath.basename(path8);
|
|
1843
2698
|
const parent = this.fsw._getWatchedDir(directory);
|
|
1844
|
-
parent.add(
|
|
1845
|
-
const absolutePath = sysPath.resolve(
|
|
2699
|
+
parent.add(basename5);
|
|
2700
|
+
const absolutePath = sysPath.resolve(path8);
|
|
1846
2701
|
const options = {
|
|
1847
2702
|
persistent: opts.persistent
|
|
1848
2703
|
};
|
|
@@ -1851,13 +2706,13 @@ var NodeFsHandler = class {
|
|
|
1851
2706
|
let closer;
|
|
1852
2707
|
if (opts.usePolling) {
|
|
1853
2708
|
const enableBin = opts.interval !== opts.binaryInterval;
|
|
1854
|
-
options.interval = enableBin && isBinaryPath(
|
|
1855
|
-
closer = setFsWatchFileListener(
|
|
2709
|
+
options.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
|
|
2710
|
+
closer = setFsWatchFileListener(path8, absolutePath, options, {
|
|
1856
2711
|
listener,
|
|
1857
2712
|
rawEmitter: this.fsw._emitRaw
|
|
1858
2713
|
});
|
|
1859
2714
|
} else {
|
|
1860
|
-
closer = setFsWatchListener(
|
|
2715
|
+
closer = setFsWatchListener(path8, absolutePath, options, {
|
|
1861
2716
|
listener,
|
|
1862
2717
|
errHandler: this._boundHandleError,
|
|
1863
2718
|
rawEmitter: this.fsw._emitRaw
|
|
@@ -1874,12 +2729,12 @@ var NodeFsHandler = class {
|
|
|
1874
2729
|
return;
|
|
1875
2730
|
}
|
|
1876
2731
|
const dirname3 = sysPath.dirname(file);
|
|
1877
|
-
const
|
|
2732
|
+
const basename5 = sysPath.basename(file);
|
|
1878
2733
|
const parent = this.fsw._getWatchedDir(dirname3);
|
|
1879
2734
|
let prevStats = stats;
|
|
1880
|
-
if (parent.has(
|
|
2735
|
+
if (parent.has(basename5))
|
|
1881
2736
|
return;
|
|
1882
|
-
const listener = async (
|
|
2737
|
+
const listener = async (path8, newStats) => {
|
|
1883
2738
|
if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
|
|
1884
2739
|
return;
|
|
1885
2740
|
if (!newStats || newStats.mtimeMs === 0) {
|
|
@@ -1893,18 +2748,18 @@ var NodeFsHandler = class {
|
|
|
1893
2748
|
this.fsw._emit(EV.CHANGE, file, newStats2);
|
|
1894
2749
|
}
|
|
1895
2750
|
if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
|
|
1896
|
-
this.fsw._closeFile(
|
|
2751
|
+
this.fsw._closeFile(path8);
|
|
1897
2752
|
prevStats = newStats2;
|
|
1898
2753
|
const closer2 = this._watchWithNodeFs(file, listener);
|
|
1899
2754
|
if (closer2)
|
|
1900
|
-
this.fsw._addPathCloser(
|
|
2755
|
+
this.fsw._addPathCloser(path8, closer2);
|
|
1901
2756
|
} else {
|
|
1902
2757
|
prevStats = newStats2;
|
|
1903
2758
|
}
|
|
1904
2759
|
} catch (error) {
|
|
1905
|
-
this.fsw._remove(dirname3,
|
|
2760
|
+
this.fsw._remove(dirname3, basename5);
|
|
1906
2761
|
}
|
|
1907
|
-
} else if (parent.has(
|
|
2762
|
+
} else if (parent.has(basename5)) {
|
|
1908
2763
|
const at = newStats.atimeMs;
|
|
1909
2764
|
const mt = newStats.mtimeMs;
|
|
1910
2765
|
if (!at || at <= mt || mt !== prevStats.mtimeMs) {
|
|
@@ -1929,7 +2784,7 @@ var NodeFsHandler = class {
|
|
|
1929
2784
|
* @param item basename of this item
|
|
1930
2785
|
* @returns true if no more processing is needed for this entry.
|
|
1931
2786
|
*/
|
|
1932
|
-
async _handleSymlink(entry, directory,
|
|
2787
|
+
async _handleSymlink(entry, directory, path8, item) {
|
|
1933
2788
|
if (this.fsw.closed) {
|
|
1934
2789
|
return;
|
|
1935
2790
|
}
|
|
@@ -1939,7 +2794,7 @@ var NodeFsHandler = class {
|
|
|
1939
2794
|
this.fsw._incrReadyCount();
|
|
1940
2795
|
let linkPath;
|
|
1941
2796
|
try {
|
|
1942
|
-
linkPath = await fsrealpath(
|
|
2797
|
+
linkPath = await fsrealpath(path8);
|
|
1943
2798
|
} catch (e) {
|
|
1944
2799
|
this.fsw._emitReady();
|
|
1945
2800
|
return true;
|
|
@@ -1949,12 +2804,12 @@ var NodeFsHandler = class {
|
|
|
1949
2804
|
if (dir.has(item)) {
|
|
1950
2805
|
if (this.fsw._symlinkPaths.get(full) !== linkPath) {
|
|
1951
2806
|
this.fsw._symlinkPaths.set(full, linkPath);
|
|
1952
|
-
this.fsw._emit(EV.CHANGE,
|
|
2807
|
+
this.fsw._emit(EV.CHANGE, path8, entry.stats);
|
|
1953
2808
|
}
|
|
1954
2809
|
} else {
|
|
1955
2810
|
dir.add(item);
|
|
1956
2811
|
this.fsw._symlinkPaths.set(full, linkPath);
|
|
1957
|
-
this.fsw._emit(EV.ADD,
|
|
2812
|
+
this.fsw._emit(EV.ADD, path8, entry.stats);
|
|
1958
2813
|
}
|
|
1959
2814
|
this.fsw._emitReady();
|
|
1960
2815
|
return true;
|
|
@@ -1983,9 +2838,9 @@ var NodeFsHandler = class {
|
|
|
1983
2838
|
return;
|
|
1984
2839
|
}
|
|
1985
2840
|
const item = entry.path;
|
|
1986
|
-
let
|
|
2841
|
+
let path8 = sysPath.join(directory, item);
|
|
1987
2842
|
current.add(item);
|
|
1988
|
-
if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory,
|
|
2843
|
+
if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path8, item)) {
|
|
1989
2844
|
return;
|
|
1990
2845
|
}
|
|
1991
2846
|
if (this.fsw.closed) {
|
|
@@ -1994,11 +2849,11 @@ var NodeFsHandler = class {
|
|
|
1994
2849
|
}
|
|
1995
2850
|
if (item === target || !target && !previous.has(item)) {
|
|
1996
2851
|
this.fsw._incrReadyCount();
|
|
1997
|
-
|
|
1998
|
-
this._addToNodeFs(
|
|
2852
|
+
path8 = sysPath.join(dir, sysPath.relative(dir, path8));
|
|
2853
|
+
this._addToNodeFs(path8, initialAdd, wh, depth + 1);
|
|
1999
2854
|
}
|
|
2000
2855
|
}).on(EV.ERROR, this._boundHandleError);
|
|
2001
|
-
return new Promise((
|
|
2856
|
+
return new Promise((resolve4, reject) => {
|
|
2002
2857
|
if (!stream)
|
|
2003
2858
|
return reject();
|
|
2004
2859
|
stream.once(STR_END, () => {
|
|
@@ -2007,7 +2862,7 @@ var NodeFsHandler = class {
|
|
|
2007
2862
|
return;
|
|
2008
2863
|
}
|
|
2009
2864
|
const wasThrottled = throttler ? throttler.clear() : false;
|
|
2010
|
-
|
|
2865
|
+
resolve4(void 0);
|
|
2011
2866
|
previous.getChildren().filter((item) => {
|
|
2012
2867
|
return item !== directory && !current.has(item);
|
|
2013
2868
|
}).forEach((item) => {
|
|
@@ -2064,13 +2919,13 @@ var NodeFsHandler = class {
|
|
|
2064
2919
|
* @param depth Child path actually targeted for watch
|
|
2065
2920
|
* @param target Child path actually targeted for watch
|
|
2066
2921
|
*/
|
|
2067
|
-
async _addToNodeFs(
|
|
2922
|
+
async _addToNodeFs(path8, initialAdd, priorWh, depth, target) {
|
|
2068
2923
|
const ready = this.fsw._emitReady;
|
|
2069
|
-
if (this.fsw._isIgnored(
|
|
2924
|
+
if (this.fsw._isIgnored(path8) || this.fsw.closed) {
|
|
2070
2925
|
ready();
|
|
2071
2926
|
return false;
|
|
2072
2927
|
}
|
|
2073
|
-
const wh = this.fsw._getWatchHelpers(
|
|
2928
|
+
const wh = this.fsw._getWatchHelpers(path8);
|
|
2074
2929
|
if (priorWh) {
|
|
2075
2930
|
wh.filterPath = (entry) => priorWh.filterPath(entry);
|
|
2076
2931
|
wh.filterDir = (entry) => priorWh.filterDir(entry);
|
|
@@ -2086,8 +2941,8 @@ var NodeFsHandler = class {
|
|
|
2086
2941
|
const follow = this.fsw.options.followSymlinks;
|
|
2087
2942
|
let closer;
|
|
2088
2943
|
if (stats.isDirectory()) {
|
|
2089
|
-
const absPath = sysPath.resolve(
|
|
2090
|
-
const targetPath = follow ? await fsrealpath(
|
|
2944
|
+
const absPath = sysPath.resolve(path8);
|
|
2945
|
+
const targetPath = follow ? await fsrealpath(path8) : path8;
|
|
2091
2946
|
if (this.fsw.closed)
|
|
2092
2947
|
return;
|
|
2093
2948
|
closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
|
|
@@ -2097,29 +2952,29 @@ var NodeFsHandler = class {
|
|
|
2097
2952
|
this.fsw._symlinkPaths.set(absPath, targetPath);
|
|
2098
2953
|
}
|
|
2099
2954
|
} else if (stats.isSymbolicLink()) {
|
|
2100
|
-
const targetPath = follow ? await fsrealpath(
|
|
2955
|
+
const targetPath = follow ? await fsrealpath(path8) : path8;
|
|
2101
2956
|
if (this.fsw.closed)
|
|
2102
2957
|
return;
|
|
2103
2958
|
const parent = sysPath.dirname(wh.watchPath);
|
|
2104
2959
|
this.fsw._getWatchedDir(parent).add(wh.watchPath);
|
|
2105
2960
|
this.fsw._emit(EV.ADD, wh.watchPath, stats);
|
|
2106
|
-
closer = await this._handleDir(parent, stats, initialAdd, depth,
|
|
2961
|
+
closer = await this._handleDir(parent, stats, initialAdd, depth, path8, wh, targetPath);
|
|
2107
2962
|
if (this.fsw.closed)
|
|
2108
2963
|
return;
|
|
2109
2964
|
if (targetPath !== void 0) {
|
|
2110
|
-
this.fsw._symlinkPaths.set(sysPath.resolve(
|
|
2965
|
+
this.fsw._symlinkPaths.set(sysPath.resolve(path8), targetPath);
|
|
2111
2966
|
}
|
|
2112
2967
|
} else {
|
|
2113
2968
|
closer = this._handleFile(wh.watchPath, stats, initialAdd);
|
|
2114
2969
|
}
|
|
2115
2970
|
ready();
|
|
2116
2971
|
if (closer)
|
|
2117
|
-
this.fsw._addPathCloser(
|
|
2972
|
+
this.fsw._addPathCloser(path8, closer);
|
|
2118
2973
|
return false;
|
|
2119
2974
|
} catch (error) {
|
|
2120
2975
|
if (this.fsw._handleError(error)) {
|
|
2121
2976
|
ready();
|
|
2122
|
-
return
|
|
2977
|
+
return path8;
|
|
2123
2978
|
}
|
|
2124
2979
|
}
|
|
2125
2980
|
}
|
|
@@ -2162,26 +3017,26 @@ function createPattern(matcher) {
|
|
|
2162
3017
|
}
|
|
2163
3018
|
return () => false;
|
|
2164
3019
|
}
|
|
2165
|
-
function normalizePath(
|
|
2166
|
-
if (typeof
|
|
3020
|
+
function normalizePath(path8) {
|
|
3021
|
+
if (typeof path8 !== "string")
|
|
2167
3022
|
throw new Error("string expected");
|
|
2168
|
-
|
|
2169
|
-
|
|
3023
|
+
path8 = sysPath2.normalize(path8);
|
|
3024
|
+
path8 = path8.replace(/\\/g, "/");
|
|
2170
3025
|
let prepend = false;
|
|
2171
|
-
if (
|
|
3026
|
+
if (path8.startsWith("//"))
|
|
2172
3027
|
prepend = true;
|
|
2173
3028
|
const DOUBLE_SLASH_RE2 = /\/\//;
|
|
2174
|
-
while (
|
|
2175
|
-
|
|
3029
|
+
while (path8.match(DOUBLE_SLASH_RE2))
|
|
3030
|
+
path8 = path8.replace(DOUBLE_SLASH_RE2, "/");
|
|
2176
3031
|
if (prepend)
|
|
2177
|
-
|
|
2178
|
-
return
|
|
3032
|
+
path8 = "/" + path8;
|
|
3033
|
+
return path8;
|
|
2179
3034
|
}
|
|
2180
3035
|
function matchPatterns(patterns, testString, stats) {
|
|
2181
|
-
const
|
|
3036
|
+
const path8 = normalizePath(testString);
|
|
2182
3037
|
for (let index = 0; index < patterns.length; index++) {
|
|
2183
3038
|
const pattern = patterns[index];
|
|
2184
|
-
if (pattern(
|
|
3039
|
+
if (pattern(path8, stats)) {
|
|
2185
3040
|
return true;
|
|
2186
3041
|
}
|
|
2187
3042
|
}
|
|
@@ -2221,19 +3076,19 @@ var toUnix = (string) => {
|
|
|
2221
3076
|
}
|
|
2222
3077
|
return str;
|
|
2223
3078
|
};
|
|
2224
|
-
var normalizePathToUnix = (
|
|
2225
|
-
var normalizeIgnored = (cwd = "") => (
|
|
2226
|
-
if (typeof
|
|
2227
|
-
return normalizePathToUnix(sysPath2.isAbsolute(
|
|
3079
|
+
var normalizePathToUnix = (path8) => toUnix(sysPath2.normalize(toUnix(path8)));
|
|
3080
|
+
var normalizeIgnored = (cwd = "") => (path8) => {
|
|
3081
|
+
if (typeof path8 === "string") {
|
|
3082
|
+
return normalizePathToUnix(sysPath2.isAbsolute(path8) ? path8 : sysPath2.join(cwd, path8));
|
|
2228
3083
|
} else {
|
|
2229
|
-
return
|
|
3084
|
+
return path8;
|
|
2230
3085
|
}
|
|
2231
3086
|
};
|
|
2232
|
-
var getAbsolutePath = (
|
|
2233
|
-
if (sysPath2.isAbsolute(
|
|
2234
|
-
return
|
|
3087
|
+
var getAbsolutePath = (path8, cwd) => {
|
|
3088
|
+
if (sysPath2.isAbsolute(path8)) {
|
|
3089
|
+
return path8;
|
|
2235
3090
|
}
|
|
2236
|
-
return sysPath2.join(cwd,
|
|
3091
|
+
return sysPath2.join(cwd, path8);
|
|
2237
3092
|
};
|
|
2238
3093
|
var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
|
|
2239
3094
|
var DirEntry = class {
|
|
@@ -2288,10 +3143,10 @@ var DirEntry = class {
|
|
|
2288
3143
|
var STAT_METHOD_F = "stat";
|
|
2289
3144
|
var STAT_METHOD_L = "lstat";
|
|
2290
3145
|
var WatchHelper = class {
|
|
2291
|
-
constructor(
|
|
3146
|
+
constructor(path8, follow, fsw) {
|
|
2292
3147
|
this.fsw = fsw;
|
|
2293
|
-
const watchPath =
|
|
2294
|
-
this.path =
|
|
3148
|
+
const watchPath = path8;
|
|
3149
|
+
this.path = path8 = path8.replace(REPLACER_RE, "");
|
|
2295
3150
|
this.watchPath = watchPath;
|
|
2296
3151
|
this.fullWatchPath = sysPath2.resolve(watchPath);
|
|
2297
3152
|
this.dirParts = [];
|
|
@@ -2413,20 +3268,20 @@ var FSWatcher = class extends EventEmitter {
|
|
|
2413
3268
|
this._closePromise = void 0;
|
|
2414
3269
|
let paths = unifyPaths(paths_);
|
|
2415
3270
|
if (cwd) {
|
|
2416
|
-
paths = paths.map((
|
|
2417
|
-
const absPath = getAbsolutePath(
|
|
3271
|
+
paths = paths.map((path8) => {
|
|
3272
|
+
const absPath = getAbsolutePath(path8, cwd);
|
|
2418
3273
|
return absPath;
|
|
2419
3274
|
});
|
|
2420
3275
|
}
|
|
2421
|
-
paths.forEach((
|
|
2422
|
-
this._removeIgnoredPath(
|
|
3276
|
+
paths.forEach((path8) => {
|
|
3277
|
+
this._removeIgnoredPath(path8);
|
|
2423
3278
|
});
|
|
2424
3279
|
this._userIgnored = void 0;
|
|
2425
3280
|
if (!this._readyCount)
|
|
2426
3281
|
this._readyCount = 0;
|
|
2427
3282
|
this._readyCount += paths.length;
|
|
2428
|
-
Promise.all(paths.map(async (
|
|
2429
|
-
const res = await this._nodeFsHandler._addToNodeFs(
|
|
3283
|
+
Promise.all(paths.map(async (path8) => {
|
|
3284
|
+
const res = await this._nodeFsHandler._addToNodeFs(path8, !_internal, void 0, 0, _origAdd);
|
|
2430
3285
|
if (res)
|
|
2431
3286
|
this._emitReady();
|
|
2432
3287
|
return res;
|
|
@@ -2448,17 +3303,17 @@ var FSWatcher = class extends EventEmitter {
|
|
|
2448
3303
|
return this;
|
|
2449
3304
|
const paths = unifyPaths(paths_);
|
|
2450
3305
|
const { cwd } = this.options;
|
|
2451
|
-
paths.forEach((
|
|
2452
|
-
if (!sysPath2.isAbsolute(
|
|
3306
|
+
paths.forEach((path8) => {
|
|
3307
|
+
if (!sysPath2.isAbsolute(path8) && !this._closers.has(path8)) {
|
|
2453
3308
|
if (cwd)
|
|
2454
|
-
|
|
2455
|
-
|
|
3309
|
+
path8 = sysPath2.join(cwd, path8);
|
|
3310
|
+
path8 = sysPath2.resolve(path8);
|
|
2456
3311
|
}
|
|
2457
|
-
this._closePath(
|
|
2458
|
-
this._addIgnoredPath(
|
|
2459
|
-
if (this._watched.has(
|
|
3312
|
+
this._closePath(path8);
|
|
3313
|
+
this._addIgnoredPath(path8);
|
|
3314
|
+
if (this._watched.has(path8)) {
|
|
2460
3315
|
this._addIgnoredPath({
|
|
2461
|
-
path:
|
|
3316
|
+
path: path8,
|
|
2462
3317
|
recursive: true
|
|
2463
3318
|
});
|
|
2464
3319
|
}
|
|
@@ -2522,38 +3377,38 @@ var FSWatcher = class extends EventEmitter {
|
|
|
2522
3377
|
* @param stats arguments to be passed with event
|
|
2523
3378
|
* @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
|
|
2524
3379
|
*/
|
|
2525
|
-
async _emit(event,
|
|
3380
|
+
async _emit(event, path8, stats) {
|
|
2526
3381
|
if (this.closed)
|
|
2527
3382
|
return;
|
|
2528
3383
|
const opts = this.options;
|
|
2529
3384
|
if (isWindows)
|
|
2530
|
-
|
|
3385
|
+
path8 = sysPath2.normalize(path8);
|
|
2531
3386
|
if (opts.cwd)
|
|
2532
|
-
|
|
2533
|
-
const args = [
|
|
3387
|
+
path8 = sysPath2.relative(opts.cwd, path8);
|
|
3388
|
+
const args = [path8];
|
|
2534
3389
|
if (stats != null)
|
|
2535
3390
|
args.push(stats);
|
|
2536
3391
|
const awf = opts.awaitWriteFinish;
|
|
2537
3392
|
let pw;
|
|
2538
|
-
if (awf && (pw = this._pendingWrites.get(
|
|
3393
|
+
if (awf && (pw = this._pendingWrites.get(path8))) {
|
|
2539
3394
|
pw.lastChange = /* @__PURE__ */ new Date();
|
|
2540
3395
|
return this;
|
|
2541
3396
|
}
|
|
2542
3397
|
if (opts.atomic) {
|
|
2543
3398
|
if (event === EVENTS.UNLINK) {
|
|
2544
|
-
this._pendingUnlinks.set(
|
|
3399
|
+
this._pendingUnlinks.set(path8, [event, ...args]);
|
|
2545
3400
|
setTimeout(() => {
|
|
2546
|
-
this._pendingUnlinks.forEach((entry,
|
|
3401
|
+
this._pendingUnlinks.forEach((entry, path9) => {
|
|
2547
3402
|
this.emit(...entry);
|
|
2548
3403
|
this.emit(EVENTS.ALL, ...entry);
|
|
2549
|
-
this._pendingUnlinks.delete(
|
|
3404
|
+
this._pendingUnlinks.delete(path9);
|
|
2550
3405
|
});
|
|
2551
3406
|
}, typeof opts.atomic === "number" ? opts.atomic : 100);
|
|
2552
3407
|
return this;
|
|
2553
3408
|
}
|
|
2554
|
-
if (event === EVENTS.ADD && this._pendingUnlinks.has(
|
|
3409
|
+
if (event === EVENTS.ADD && this._pendingUnlinks.has(path8)) {
|
|
2555
3410
|
event = EVENTS.CHANGE;
|
|
2556
|
-
this._pendingUnlinks.delete(
|
|
3411
|
+
this._pendingUnlinks.delete(path8);
|
|
2557
3412
|
}
|
|
2558
3413
|
}
|
|
2559
3414
|
if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
|
|
@@ -2571,16 +3426,16 @@ var FSWatcher = class extends EventEmitter {
|
|
|
2571
3426
|
this.emitWithAll(event, args);
|
|
2572
3427
|
}
|
|
2573
3428
|
};
|
|
2574
|
-
this._awaitWriteFinish(
|
|
3429
|
+
this._awaitWriteFinish(path8, awf.stabilityThreshold, event, awfEmit);
|
|
2575
3430
|
return this;
|
|
2576
3431
|
}
|
|
2577
3432
|
if (event === EVENTS.CHANGE) {
|
|
2578
|
-
const isThrottled = !this._throttle(EVENTS.CHANGE,
|
|
3433
|
+
const isThrottled = !this._throttle(EVENTS.CHANGE, path8, 50);
|
|
2579
3434
|
if (isThrottled)
|
|
2580
3435
|
return this;
|
|
2581
3436
|
}
|
|
2582
3437
|
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,
|
|
3438
|
+
const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path8) : path8;
|
|
2584
3439
|
let stats2;
|
|
2585
3440
|
try {
|
|
2586
3441
|
stats2 = await stat3(fullPath);
|
|
@@ -2611,23 +3466,23 @@ var FSWatcher = class extends EventEmitter {
|
|
|
2611
3466
|
* @param timeout duration of time to suppress duplicate actions
|
|
2612
3467
|
* @returns tracking object or false if action should be suppressed
|
|
2613
3468
|
*/
|
|
2614
|
-
_throttle(actionType,
|
|
3469
|
+
_throttle(actionType, path8, timeout) {
|
|
2615
3470
|
if (!this._throttled.has(actionType)) {
|
|
2616
3471
|
this._throttled.set(actionType, /* @__PURE__ */ new Map());
|
|
2617
3472
|
}
|
|
2618
3473
|
const action = this._throttled.get(actionType);
|
|
2619
3474
|
if (!action)
|
|
2620
3475
|
throw new Error("invalid throttle");
|
|
2621
|
-
const actionPath = action.get(
|
|
3476
|
+
const actionPath = action.get(path8);
|
|
2622
3477
|
if (actionPath) {
|
|
2623
3478
|
actionPath.count++;
|
|
2624
3479
|
return false;
|
|
2625
3480
|
}
|
|
2626
3481
|
let timeoutObject;
|
|
2627
3482
|
const clear = () => {
|
|
2628
|
-
const item = action.get(
|
|
3483
|
+
const item = action.get(path8);
|
|
2629
3484
|
const count = item ? item.count : 0;
|
|
2630
|
-
action.delete(
|
|
3485
|
+
action.delete(path8);
|
|
2631
3486
|
clearTimeout(timeoutObject);
|
|
2632
3487
|
if (item)
|
|
2633
3488
|
clearTimeout(item.timeoutObject);
|
|
@@ -2635,7 +3490,7 @@ var FSWatcher = class extends EventEmitter {
|
|
|
2635
3490
|
};
|
|
2636
3491
|
timeoutObject = setTimeout(clear, timeout);
|
|
2637
3492
|
const thr = { timeoutObject, clear, count: 0 };
|
|
2638
|
-
action.set(
|
|
3493
|
+
action.set(path8, thr);
|
|
2639
3494
|
return thr;
|
|
2640
3495
|
}
|
|
2641
3496
|
_incrReadyCount() {
|
|
@@ -2649,44 +3504,44 @@ var FSWatcher = class extends EventEmitter {
|
|
|
2649
3504
|
* @param event
|
|
2650
3505
|
* @param awfEmit Callback to be called when ready for event to be emitted.
|
|
2651
3506
|
*/
|
|
2652
|
-
_awaitWriteFinish(
|
|
3507
|
+
_awaitWriteFinish(path8, threshold, event, awfEmit) {
|
|
2653
3508
|
const awf = this.options.awaitWriteFinish;
|
|
2654
3509
|
if (typeof awf !== "object")
|
|
2655
3510
|
return;
|
|
2656
3511
|
const pollInterval = awf.pollInterval;
|
|
2657
3512
|
let timeoutHandler;
|
|
2658
|
-
let fullPath =
|
|
2659
|
-
if (this.options.cwd && !sysPath2.isAbsolute(
|
|
2660
|
-
fullPath = sysPath2.join(this.options.cwd,
|
|
3513
|
+
let fullPath = path8;
|
|
3514
|
+
if (this.options.cwd && !sysPath2.isAbsolute(path8)) {
|
|
3515
|
+
fullPath = sysPath2.join(this.options.cwd, path8);
|
|
2661
3516
|
}
|
|
2662
3517
|
const now = /* @__PURE__ */ new Date();
|
|
2663
3518
|
const writes = this._pendingWrites;
|
|
2664
3519
|
function awaitWriteFinishFn(prevStat) {
|
|
2665
3520
|
statcb(fullPath, (err, curStat) => {
|
|
2666
|
-
if (err || !writes.has(
|
|
3521
|
+
if (err || !writes.has(path8)) {
|
|
2667
3522
|
if (err && err.code !== "ENOENT")
|
|
2668
3523
|
awfEmit(err);
|
|
2669
3524
|
return;
|
|
2670
3525
|
}
|
|
2671
3526
|
const now2 = Number(/* @__PURE__ */ new Date());
|
|
2672
3527
|
if (prevStat && curStat.size !== prevStat.size) {
|
|
2673
|
-
writes.get(
|
|
3528
|
+
writes.get(path8).lastChange = now2;
|
|
2674
3529
|
}
|
|
2675
|
-
const pw = writes.get(
|
|
3530
|
+
const pw = writes.get(path8);
|
|
2676
3531
|
const df = now2 - pw.lastChange;
|
|
2677
3532
|
if (df >= threshold) {
|
|
2678
|
-
writes.delete(
|
|
3533
|
+
writes.delete(path8);
|
|
2679
3534
|
awfEmit(void 0, curStat);
|
|
2680
3535
|
} else {
|
|
2681
3536
|
timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
|
|
2682
3537
|
}
|
|
2683
3538
|
});
|
|
2684
3539
|
}
|
|
2685
|
-
if (!writes.has(
|
|
2686
|
-
writes.set(
|
|
3540
|
+
if (!writes.has(path8)) {
|
|
3541
|
+
writes.set(path8, {
|
|
2687
3542
|
lastChange: now,
|
|
2688
3543
|
cancelWait: () => {
|
|
2689
|
-
writes.delete(
|
|
3544
|
+
writes.delete(path8);
|
|
2690
3545
|
clearTimeout(timeoutHandler);
|
|
2691
3546
|
return event;
|
|
2692
3547
|
}
|
|
@@ -2697,8 +3552,8 @@ var FSWatcher = class extends EventEmitter {
|
|
|
2697
3552
|
/**
|
|
2698
3553
|
* Determines whether user has asked to ignore this path.
|
|
2699
3554
|
*/
|
|
2700
|
-
_isIgnored(
|
|
2701
|
-
if (this.options.atomic && DOT_RE.test(
|
|
3555
|
+
_isIgnored(path8, stats) {
|
|
3556
|
+
if (this.options.atomic && DOT_RE.test(path8))
|
|
2702
3557
|
return true;
|
|
2703
3558
|
if (!this._userIgnored) {
|
|
2704
3559
|
const { cwd } = this.options;
|
|
@@ -2708,17 +3563,17 @@ var FSWatcher = class extends EventEmitter {
|
|
|
2708
3563
|
const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
|
|
2709
3564
|
this._userIgnored = anymatch(list, void 0);
|
|
2710
3565
|
}
|
|
2711
|
-
return this._userIgnored(
|
|
3566
|
+
return this._userIgnored(path8, stats);
|
|
2712
3567
|
}
|
|
2713
|
-
_isntIgnored(
|
|
2714
|
-
return !this._isIgnored(
|
|
3568
|
+
_isntIgnored(path8, stat4) {
|
|
3569
|
+
return !this._isIgnored(path8, stat4);
|
|
2715
3570
|
}
|
|
2716
3571
|
/**
|
|
2717
3572
|
* Provides a set of common helpers and properties relating to symlink handling.
|
|
2718
3573
|
* @param path file or directory pattern being watched
|
|
2719
3574
|
*/
|
|
2720
|
-
_getWatchHelpers(
|
|
2721
|
-
return new WatchHelper(
|
|
3575
|
+
_getWatchHelpers(path8) {
|
|
3576
|
+
return new WatchHelper(path8, this.options.followSymlinks, this);
|
|
2722
3577
|
}
|
|
2723
3578
|
// Directory helpers
|
|
2724
3579
|
// -----------------
|
|
@@ -2750,63 +3605,63 @@ var FSWatcher = class extends EventEmitter {
|
|
|
2750
3605
|
* @param item base path of item/directory
|
|
2751
3606
|
*/
|
|
2752
3607
|
_remove(directory, item, isDirectory) {
|
|
2753
|
-
const
|
|
2754
|
-
const fullPath = sysPath2.resolve(
|
|
2755
|
-
isDirectory = isDirectory != null ? isDirectory : this._watched.has(
|
|
2756
|
-
if (!this._throttle("remove",
|
|
3608
|
+
const path8 = sysPath2.join(directory, item);
|
|
3609
|
+
const fullPath = sysPath2.resolve(path8);
|
|
3610
|
+
isDirectory = isDirectory != null ? isDirectory : this._watched.has(path8) || this._watched.has(fullPath);
|
|
3611
|
+
if (!this._throttle("remove", path8, 100))
|
|
2757
3612
|
return;
|
|
2758
3613
|
if (!isDirectory && this._watched.size === 1) {
|
|
2759
3614
|
this.add(directory, item, true);
|
|
2760
3615
|
}
|
|
2761
|
-
const wp = this._getWatchedDir(
|
|
3616
|
+
const wp = this._getWatchedDir(path8);
|
|
2762
3617
|
const nestedDirectoryChildren = wp.getChildren();
|
|
2763
|
-
nestedDirectoryChildren.forEach((nested) => this._remove(
|
|
3618
|
+
nestedDirectoryChildren.forEach((nested) => this._remove(path8, nested));
|
|
2764
3619
|
const parent = this._getWatchedDir(directory);
|
|
2765
3620
|
const wasTracked = parent.has(item);
|
|
2766
3621
|
parent.remove(item);
|
|
2767
3622
|
if (this._symlinkPaths.has(fullPath)) {
|
|
2768
3623
|
this._symlinkPaths.delete(fullPath);
|
|
2769
3624
|
}
|
|
2770
|
-
let relPath =
|
|
3625
|
+
let relPath = path8;
|
|
2771
3626
|
if (this.options.cwd)
|
|
2772
|
-
relPath = sysPath2.relative(this.options.cwd,
|
|
3627
|
+
relPath = sysPath2.relative(this.options.cwd, path8);
|
|
2773
3628
|
if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
|
|
2774
3629
|
const event = this._pendingWrites.get(relPath).cancelWait();
|
|
2775
3630
|
if (event === EVENTS.ADD)
|
|
2776
3631
|
return;
|
|
2777
3632
|
}
|
|
2778
|
-
this._watched.delete(
|
|
3633
|
+
this._watched.delete(path8);
|
|
2779
3634
|
this._watched.delete(fullPath);
|
|
2780
3635
|
const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
|
|
2781
|
-
if (wasTracked && !this._isIgnored(
|
|
2782
|
-
this._emit(eventName,
|
|
2783
|
-
this._closePath(
|
|
3636
|
+
if (wasTracked && !this._isIgnored(path8))
|
|
3637
|
+
this._emit(eventName, path8);
|
|
3638
|
+
this._closePath(path8);
|
|
2784
3639
|
}
|
|
2785
3640
|
/**
|
|
2786
3641
|
* Closes all watchers for a path
|
|
2787
3642
|
*/
|
|
2788
|
-
_closePath(
|
|
2789
|
-
this._closeFile(
|
|
2790
|
-
const dir = sysPath2.dirname(
|
|
2791
|
-
this._getWatchedDir(dir).remove(sysPath2.basename(
|
|
3643
|
+
_closePath(path8) {
|
|
3644
|
+
this._closeFile(path8);
|
|
3645
|
+
const dir = sysPath2.dirname(path8);
|
|
3646
|
+
this._getWatchedDir(dir).remove(sysPath2.basename(path8));
|
|
2792
3647
|
}
|
|
2793
3648
|
/**
|
|
2794
3649
|
* Closes only file-specific watchers
|
|
2795
3650
|
*/
|
|
2796
|
-
_closeFile(
|
|
2797
|
-
const closers = this._closers.get(
|
|
3651
|
+
_closeFile(path8) {
|
|
3652
|
+
const closers = this._closers.get(path8);
|
|
2798
3653
|
if (!closers)
|
|
2799
3654
|
return;
|
|
2800
3655
|
closers.forEach((closer) => closer());
|
|
2801
|
-
this._closers.delete(
|
|
3656
|
+
this._closers.delete(path8);
|
|
2802
3657
|
}
|
|
2803
|
-
_addPathCloser(
|
|
3658
|
+
_addPathCloser(path8, closer) {
|
|
2804
3659
|
if (!closer)
|
|
2805
3660
|
return;
|
|
2806
|
-
let list = this._closers.get(
|
|
3661
|
+
let list = this._closers.get(path8);
|
|
2807
3662
|
if (!list) {
|
|
2808
3663
|
list = [];
|
|
2809
|
-
this._closers.set(
|
|
3664
|
+
this._closers.set(path8, list);
|
|
2810
3665
|
}
|
|
2811
3666
|
list.push(closer);
|
|
2812
3667
|
}
|
|
@@ -2837,56 +3692,184 @@ var esm_default = { watch, FSWatcher };
|
|
|
2837
3692
|
|
|
2838
3693
|
// src/watch.ts
|
|
2839
3694
|
var DEBOUNCE_MS = 1200;
|
|
3695
|
+
var LIMIT_REFRESH_MS = 6e4;
|
|
3696
|
+
var FALLBACK_SCAN_MS = 5e3;
|
|
3697
|
+
var DEVICE_HEARTBEAT_MS = 60 * 6e4;
|
|
3698
|
+
var CLOUD_SYNC_INTERVAL_MS = 30 * 6e4;
|
|
3699
|
+
var MAX_CLOUD_WRITES_PER_DAY = 16;
|
|
3700
|
+
var QUOTA_RETRY_MS = 15 * 6e4;
|
|
3701
|
+
var QUOTA_RETRY_JITTER_MS = 5 * 6e4;
|
|
2840
3702
|
async function watch2(runner) {
|
|
2841
|
-
await safeSync(runner,
|
|
3703
|
+
const startupRetryAfter = await safeSync(runner, {
|
|
3704
|
+
reason: "startup",
|
|
3705
|
+
refreshLimits: true,
|
|
3706
|
+
forceDeviceHeartbeat: true,
|
|
3707
|
+
deviceHeartbeatMs: DEVICE_HEARTBEAT_MS,
|
|
3708
|
+
cloudSyncIntervalMs: CLOUD_SYNC_INTERVAL_MS,
|
|
3709
|
+
maxCloudWritesPerDay: MAX_CLOUD_WRITES_PER_DAY
|
|
3710
|
+
});
|
|
2842
3711
|
const paths = runner.watchPaths();
|
|
2843
3712
|
const watcher = esm_default.watch(paths, {
|
|
2844
3713
|
ignoreInitial: true,
|
|
2845
3714
|
awaitWriteFinish: { stabilityThreshold: 400, pollInterval: 100 },
|
|
2846
|
-
|
|
3715
|
+
// Never infer "file" from dots in the absolute path: the provider roots
|
|
3716
|
+
// themselves are named .claude and .codex on every supported platform.
|
|
3717
|
+
ignored: (p, stats) => Boolean(stats?.isFile() && !isUsageFile(p))
|
|
2847
3718
|
});
|
|
2848
3719
|
let timer;
|
|
2849
|
-
let pending
|
|
3720
|
+
let pending;
|
|
2850
3721
|
let running = false;
|
|
2851
|
-
|
|
3722
|
+
let scheduledAt = 0;
|
|
3723
|
+
let blockedUntil = startupRetryAfter ? Date.now() + startupRetryAfter : 0;
|
|
3724
|
+
const schedule = (request, delay = DEBOUNCE_MS) => {
|
|
3725
|
+
pending = mergeRequests(pending, request);
|
|
3726
|
+
if (running) return;
|
|
3727
|
+
const target = Math.max(Date.now() + delay, blockedUntil);
|
|
3728
|
+
if (timer && scheduledAt <= target) return;
|
|
2852
3729
|
if (timer) clearTimeout(timer);
|
|
3730
|
+
scheduledAt = target;
|
|
2853
3731
|
timer = setTimeout(async () => {
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
3732
|
+
timer = void 0;
|
|
3733
|
+
scheduledAt = 0;
|
|
3734
|
+
const next = pending;
|
|
3735
|
+
pending = void 0;
|
|
3736
|
+
if (!next) return;
|
|
2858
3737
|
running = true;
|
|
2859
|
-
await safeSync(runner,
|
|
3738
|
+
const retryAfterMs = await safeSync(runner, next);
|
|
2860
3739
|
running = false;
|
|
3740
|
+
if (retryAfterMs) {
|
|
3741
|
+
blockedUntil = Date.now() + retryAfterMs;
|
|
3742
|
+
pending = mergeRequests(pending, next);
|
|
3743
|
+
} else {
|
|
3744
|
+
blockedUntil = 0;
|
|
3745
|
+
}
|
|
2861
3746
|
if (pending) {
|
|
2862
|
-
|
|
2863
|
-
|
|
3747
|
+
const queued = pending;
|
|
3748
|
+
pending = void 0;
|
|
3749
|
+
schedule(queued, 0);
|
|
2864
3750
|
}
|
|
2865
|
-
},
|
|
3751
|
+
}, Math.max(0, target - Date.now()));
|
|
2866
3752
|
};
|
|
2867
|
-
|
|
3753
|
+
const turnRequest = (reason) => ({
|
|
3754
|
+
reason,
|
|
3755
|
+
refreshLimits: false,
|
|
3756
|
+
deviceHeartbeatMs: DEVICE_HEARTBEAT_MS,
|
|
3757
|
+
cloudSyncIntervalMs: CLOUD_SYNC_INTERVAL_MS,
|
|
3758
|
+
maxCloudWritesPerDay: MAX_CLOUD_WRITES_PER_DAY
|
|
3759
|
+
});
|
|
3760
|
+
watcher.on("add", () => schedule(turnRequest("change"))).on("change", () => schedule(turnRequest("change")));
|
|
3761
|
+
const fallbackTimer = setInterval(() => schedule(turnRequest("fallback"), 0), FALLBACK_SCAN_MS);
|
|
3762
|
+
const refreshTimer = setInterval(
|
|
3763
|
+
() => schedule(
|
|
3764
|
+
{
|
|
3765
|
+
reason: "limits",
|
|
3766
|
+
refreshLimits: true,
|
|
3767
|
+
deviceHeartbeatMs: DEVICE_HEARTBEAT_MS,
|
|
3768
|
+
cloudSyncIntervalMs: CLOUD_SYNC_INTERVAL_MS,
|
|
3769
|
+
maxCloudWritesPerDay: MAX_CLOUD_WRITES_PER_DAY
|
|
3770
|
+
},
|
|
3771
|
+
0
|
|
3772
|
+
),
|
|
3773
|
+
LIMIT_REFRESH_MS
|
|
3774
|
+
);
|
|
2868
3775
|
console.log(`Watching for usage in:
|
|
2869
3776
|
${paths.join("\n ")}
|
|
2870
3777
|
Press Ctrl+C to stop.`);
|
|
2871
|
-
await new Promise((
|
|
3778
|
+
await new Promise((resolve4) => {
|
|
2872
3779
|
process.on("SIGINT", () => {
|
|
2873
|
-
|
|
3780
|
+
clearInterval(refreshTimer);
|
|
3781
|
+
clearInterval(fallbackTimer);
|
|
3782
|
+
if (timer) clearTimeout(timer);
|
|
3783
|
+
void watcher.close().then(resolve4);
|
|
2874
3784
|
});
|
|
2875
3785
|
});
|
|
2876
3786
|
}
|
|
2877
|
-
async function safeSync(runner,
|
|
3787
|
+
async function safeSync(runner, request) {
|
|
2878
3788
|
try {
|
|
2879
|
-
const {
|
|
2880
|
-
|
|
3789
|
+
const { reason, ...options } = request;
|
|
3790
|
+
const { processedTurns, byProvider, published, cloudWritesToday } = await runner.syncOnce(options);
|
|
3791
|
+
if (processedTurns > 0) {
|
|
2881
3792
|
const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ");
|
|
2882
|
-
console.log(
|
|
3793
|
+
console.log(
|
|
3794
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] processed ${processedTurns} turn(s) locally (${detail}); ` + (published ? `cloud snapshot published (${cloudWritesToday}/16 today)` : "cloud snapshot queued")
|
|
3795
|
+
);
|
|
2883
3796
|
} else if (reason === "startup") {
|
|
2884
|
-
console.log(
|
|
3797
|
+
console.log(
|
|
3798
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] local snapshot ready; ` + (published ? `cloud snapshot published (${cloudWritesToday}/16 today)` : "cloud snapshot queued")
|
|
3799
|
+
);
|
|
2885
3800
|
}
|
|
2886
3801
|
} catch (err) {
|
|
3802
|
+
if (err instanceof FirestoreRestError && err.status === 429) {
|
|
3803
|
+
const retryAfter = Math.max(
|
|
3804
|
+
err.retryAfterMs ?? 0,
|
|
3805
|
+
QUOTA_RETRY_MS + Math.floor(Math.random() * QUOTA_RETRY_JITTER_MS)
|
|
3806
|
+
);
|
|
3807
|
+
console.error(
|
|
3808
|
+
`Cloud sync paused: Firestore quota is exhausted; the local snapshot is safe. Retrying in ${Math.ceil(retryAfter / 6e4)}m.`
|
|
3809
|
+
);
|
|
3810
|
+
return retryAfter;
|
|
3811
|
+
}
|
|
2887
3812
|
console.error(`Sync error: ${err.message}`);
|
|
3813
|
+
return 15e3;
|
|
2888
3814
|
}
|
|
2889
3815
|
}
|
|
3816
|
+
function mergeRequests(current, incoming) {
|
|
3817
|
+
if (!current) return incoming;
|
|
3818
|
+
return {
|
|
3819
|
+
reason: incoming.reason === "change" || current.reason === "change" ? "change" : incoming.reason,
|
|
3820
|
+
refreshLimits: Boolean(current.refreshLimits || incoming.refreshLimits),
|
|
3821
|
+
forceDeviceHeartbeat: Boolean(current.forceDeviceHeartbeat || incoming.forceDeviceHeartbeat),
|
|
3822
|
+
deviceHeartbeatMs: Math.min(
|
|
3823
|
+
current.deviceHeartbeatMs ?? DEVICE_HEARTBEAT_MS,
|
|
3824
|
+
incoming.deviceHeartbeatMs ?? DEVICE_HEARTBEAT_MS
|
|
3825
|
+
),
|
|
3826
|
+
cloudSyncIntervalMs: Math.min(
|
|
3827
|
+
current.cloudSyncIntervalMs ?? CLOUD_SYNC_INTERVAL_MS,
|
|
3828
|
+
incoming.cloudSyncIntervalMs ?? CLOUD_SYNC_INTERVAL_MS
|
|
3829
|
+
),
|
|
3830
|
+
maxCloudWritesPerDay: Math.min(
|
|
3831
|
+
current.maxCloudWritesPerDay ?? MAX_CLOUD_WRITES_PER_DAY,
|
|
3832
|
+
incoming.maxCloudWritesPerDay ?? MAX_CLOUD_WRITES_PER_DAY
|
|
3833
|
+
)
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
function isUsageFile(filePath) {
|
|
3837
|
+
return filePath.endsWith(".jsonl") || filePath.endsWith(".sqlite") || filePath.endsWith(".sqlite-wal");
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
// src/update-check.ts
|
|
3841
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3842
|
+
var REGISTRY_URL = "https://registry.npmjs.org/tokelytics/latest";
|
|
3843
|
+
async function agentUpdateMessage(force = false) {
|
|
3844
|
+
try {
|
|
3845
|
+
const state = await loadUpdateState();
|
|
3846
|
+
const checkedAt = Date.parse(state.checkedAt ?? "");
|
|
3847
|
+
let latest = state.latestVersion;
|
|
3848
|
+
if (force || !Number.isFinite(checkedAt) || Date.now() - checkedAt >= CHECK_INTERVAL_MS) {
|
|
3849
|
+
const response = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(5e3) });
|
|
3850
|
+
if (!response.ok) return void 0;
|
|
3851
|
+
const body = await response.json();
|
|
3852
|
+
latest = body.version;
|
|
3853
|
+
await saveUpdateState({
|
|
3854
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3855
|
+
latestVersion: latest
|
|
3856
|
+
});
|
|
3857
|
+
}
|
|
3858
|
+
if (latest && compareVersions(latest, AGENT_VERSION) > 0) {
|
|
3859
|
+
return `Tokelytics ${latest} is available. Update with: npm install -g tokelytics@latest`;
|
|
3860
|
+
}
|
|
3861
|
+
} catch {
|
|
3862
|
+
}
|
|
3863
|
+
return void 0;
|
|
3864
|
+
}
|
|
3865
|
+
function compareVersions(left, right) {
|
|
3866
|
+
const a = left.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
3867
|
+
const b = right.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
3868
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
3869
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0) ? 1 : -1;
|
|
3870
|
+
}
|
|
3871
|
+
return 0;
|
|
3872
|
+
}
|
|
2890
3873
|
|
|
2891
3874
|
// src/cli.ts
|
|
2892
3875
|
var USAGE = `Tokelytics agent
|
|
@@ -2894,7 +3877,7 @@ var USAGE = `Tokelytics agent
|
|
|
2894
3877
|
Usage:
|
|
2895
3878
|
tokelytics login Sign in by approving in your browser
|
|
2896
3879
|
tokelytics sync Run one incremental sync to your dashboard
|
|
2897
|
-
tokelytics watch
|
|
3880
|
+
tokelytics watch Watch usage and refresh the cloud snapshot
|
|
2898
3881
|
tokelytics status Show current sign-in and device
|
|
2899
3882
|
tokelytics logout Forget stored credentials
|
|
2900
3883
|
`;
|
|
@@ -2910,24 +3893,39 @@ async function main(argv) {
|
|
|
2910
3893
|
} catch (err) {
|
|
2911
3894
|
console.warn(`(Couldn't register device yet \u2014 will retry on first sync: ${err.message})`);
|
|
2912
3895
|
}
|
|
2913
|
-
console.log('Done. Now run "tokelytics watch" to
|
|
3896
|
+
console.log('Done. Now run "npx tokelytics@latest watch" to keep your dashboard updated.');
|
|
2914
3897
|
return 0;
|
|
2915
3898
|
}
|
|
2916
3899
|
case "sync": {
|
|
2917
|
-
const
|
|
2918
|
-
|
|
2919
|
-
const
|
|
2920
|
-
|
|
3900
|
+
const update = await agentUpdateMessage();
|
|
3901
|
+
if (update) console.warn(update);
|
|
3902
|
+
const lock = await acquireWatchLock();
|
|
3903
|
+
try {
|
|
3904
|
+
const runner = await createRunner();
|
|
3905
|
+
const { processedTurns, processedLimits, byProvider } = await runner.syncOnce();
|
|
3906
|
+
const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ") || "none";
|
|
3907
|
+
console.log(`Processed ${processedTurns} turn(s) (${detail}); refreshed ${processedLimits} usage provider(s).`);
|
|
3908
|
+
} finally {
|
|
3909
|
+
await lock.release();
|
|
3910
|
+
}
|
|
2921
3911
|
return 0;
|
|
2922
3912
|
}
|
|
2923
3913
|
case "watch": {
|
|
2924
|
-
const
|
|
2925
|
-
|
|
3914
|
+
const lock = await acquireWatchLock();
|
|
3915
|
+
try {
|
|
3916
|
+
const update = await agentUpdateMessage();
|
|
3917
|
+
if (update) console.warn(update);
|
|
3918
|
+
const runner = await createRunner();
|
|
3919
|
+
await watch2(runner);
|
|
3920
|
+
} finally {
|
|
3921
|
+
await lock.release();
|
|
3922
|
+
}
|
|
2926
3923
|
return 0;
|
|
2927
3924
|
}
|
|
2928
3925
|
case "status": {
|
|
2929
3926
|
const creds = await loadCredentials();
|
|
2930
3927
|
const state = await loadState();
|
|
3928
|
+
console.log(`Agent version: ${AGENT_VERSION}`);
|
|
2931
3929
|
if (!creds) {
|
|
2932
3930
|
console.log('Not signed in. Run "tokelytics login".');
|
|
2933
3931
|
} else {
|
|
@@ -2943,6 +3941,8 @@ async function main(argv) {
|
|
|
2943
3941
|
} catch (err) {
|
|
2944
3942
|
console.log(`Session needs refresh: ${err.message}`);
|
|
2945
3943
|
}
|
|
3944
|
+
const update = await agentUpdateMessage(true);
|
|
3945
|
+
if (update) console.log(update);
|
|
2946
3946
|
return 0;
|
|
2947
3947
|
}
|
|
2948
3948
|
case "logout": {
|