tokelytics 0.3.4 → 0.3.6
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 +1 -1
- package/bin/tokelytics.mjs +185 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ prompts or responses.
|
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
npx tokelytics@latest login # opens your browser to approve — no setup, no keys
|
|
12
|
-
npx tokelytics@latest watch #
|
|
12
|
+
npx tokelytics@latest watch # one machine-wide watcher; cloud backup every 30m
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Keep `@latest` in install and onboarding commands. Agents older than 0.3.1
|
package/bin/tokelytics.mjs
CHANGED
|
@@ -85,9 +85,20 @@ function mergeDashboardSnapshot(previous, turns, limits, device, now = /* @__PUR
|
|
|
85
85
|
const next = structuredClone(previous ?? emptyDashboardSnapshot(now));
|
|
86
86
|
next.version = 2;
|
|
87
87
|
if (turns.length > 0) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
const previousById = new Map(next.recentTurns.map((turn) => [turn.turnId, turn]));
|
|
89
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
90
|
+
const changedTurns = turns.filter((turn) => {
|
|
91
|
+
const prior = previousById.get(turn.turnId);
|
|
92
|
+
if (!prior)
|
|
93
|
+
return true;
|
|
94
|
+
if (sameTurn(prior, turn))
|
|
95
|
+
return false;
|
|
96
|
+
replacements.set(turn.turnId, prior);
|
|
97
|
+
return true;
|
|
98
|
+
});
|
|
99
|
+
next.recentTurns = mergeRecentTurns(next.recentTurns, changedTurns);
|
|
100
|
+
next.sessions = mergeSessions(next.sessions, changedTurns, replacements);
|
|
101
|
+
next.rollups = mergeRollups(next.rollups, changedTurns, now, replacements);
|
|
91
102
|
}
|
|
92
103
|
if (limits.length > 0)
|
|
93
104
|
next.limits = mergeLimits(next.limits, limits);
|
|
@@ -130,6 +141,9 @@ function mergeRecentTurns(existing, incoming) {
|
|
|
130
141
|
byId.set(turn.turnId, turn);
|
|
131
142
|
return [...byId.values()].sort((a, b) => a.ts.localeCompare(b.ts)).slice(-SNAPSHOT_RECENT_TURNS);
|
|
132
143
|
}
|
|
144
|
+
function sameTurn(left, right) {
|
|
145
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
146
|
+
}
|
|
133
147
|
function sanitizeTurn(turn) {
|
|
134
148
|
return {
|
|
135
149
|
...turn,
|
|
@@ -156,17 +170,30 @@ function safePath(value) {
|
|
|
156
170
|
const parts = normalized.split("/").filter((part) => part && part !== "." && part !== ".." && !/^[A-Za-z]:$/.test(part));
|
|
157
171
|
return parts.slice(-3).join("/").slice(-200);
|
|
158
172
|
}
|
|
159
|
-
function mergeSessions(existing, turns) {
|
|
173
|
+
function mergeSessions(existing, turns, replacements = /* @__PURE__ */ new Map()) {
|
|
160
174
|
const byId = new Map(existing.map((session) => [session.sessionId, { ...session }]));
|
|
161
175
|
for (const turn of turns) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
176
|
+
const prior = replacements.get(turn.turnId);
|
|
177
|
+
if (prior)
|
|
178
|
+
applySessionTurn(byId, prior, -1);
|
|
179
|
+
applySessionTurn(byId, turn, 1);
|
|
180
|
+
}
|
|
181
|
+
return [...byId.values()].sort((a, b) => b.lastTs.localeCompare(a.lastTs)).slice(0, SNAPSHOT_SESSIONS);
|
|
182
|
+
}
|
|
183
|
+
function applySessionTurn(byId, turn, direction) {
|
|
184
|
+
const session = byId.get(turn.sessionId) ?? emptySession(turn);
|
|
185
|
+
session.turnCount += direction;
|
|
186
|
+
session.inputTokens = Math.max(0, session.inputTokens + direction * turn.inputTokens);
|
|
187
|
+
session.outputTokens = Math.max(0, session.outputTokens + direction * turn.outputTokens);
|
|
188
|
+
session.cacheReadTokens = Math.max(0, session.cacheReadTokens + direction * turn.cacheReadTokens);
|
|
189
|
+
session.cacheCreationTokens = Math.max(0, session.cacheCreationTokens + direction * turn.cacheCreationTokens);
|
|
190
|
+
session.cachedInputTokens = Math.max(0, session.cachedInputTokens + direction * turn.cachedInputTokens);
|
|
191
|
+
session.reasoningTokens = Math.max(0, session.reasoningTokens + direction * turn.reasoningTokens);
|
|
192
|
+
if (session.turnCount <= 0) {
|
|
193
|
+
byId.delete(turn.sessionId);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (direction > 0) {
|
|
170
197
|
if (!session.firstTs || turn.ts < session.firstTs)
|
|
171
198
|
session.firstTs = turn.ts;
|
|
172
199
|
if (!session.lastTs || turn.ts > session.lastTs)
|
|
@@ -178,9 +205,8 @@ function mergeSessions(existing, turns) {
|
|
|
178
205
|
if (turn.model && (!session.model || modelPriority(turn.model) > modelPriority(session.model))) {
|
|
179
206
|
session.model = turn.model;
|
|
180
207
|
}
|
|
181
|
-
byId.set(turn.sessionId, session);
|
|
182
208
|
}
|
|
183
|
-
|
|
209
|
+
byId.set(turn.sessionId, session);
|
|
184
210
|
}
|
|
185
211
|
function emptySession(turn) {
|
|
186
212
|
return {
|
|
@@ -200,48 +226,62 @@ function emptySession(turn) {
|
|
|
200
226
|
reasoningTokens: 0
|
|
201
227
|
};
|
|
202
228
|
}
|
|
203
|
-
function mergeRollups(existing, turns, now) {
|
|
229
|
+
function mergeRollups(existing, turns, now, replacements = /* @__PURE__ */ new Map()) {
|
|
204
230
|
const byId = new Map(existing.map((rollup) => [rollup.id, structuredClone(rollup)]));
|
|
205
231
|
for (const turn of turns) {
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
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;
|
|
233
|
-
bucket.turns++;
|
|
234
|
-
if (!rollup.hourly.some((item) => item.hour === hour))
|
|
235
|
-
rollup.hourly.push(bucket);
|
|
236
|
-
rollup.hourly.sort((a, b) => a.hour - b.hour);
|
|
237
|
-
}
|
|
238
|
-
byId.set(id, rollup);
|
|
232
|
+
const prior = replacements.get(turn.turnId);
|
|
233
|
+
if (prior)
|
|
234
|
+
applyRollupTurn(byId, prior, -1);
|
|
235
|
+
applyRollupTurn(byId, turn, 1);
|
|
239
236
|
}
|
|
240
237
|
const cutoff = new Date(now);
|
|
241
238
|
cutoff.setUTCDate(cutoff.getUTCDate() - (SNAPSHOT_RETENTION_DAYS - 1));
|
|
242
239
|
const cutoffDay = cutoff.toISOString().slice(0, 10);
|
|
243
240
|
return [...byId.values()].filter((rollup) => rollup.day >= cutoffDay).sort((a, b) => a.day.localeCompare(b.day) || a.provider.localeCompare(b.provider));
|
|
244
241
|
}
|
|
242
|
+
function applyRollupTurn(byId, turn, direction) {
|
|
243
|
+
const day = dayOf(turn.ts);
|
|
244
|
+
if (!day)
|
|
245
|
+
return;
|
|
246
|
+
const id = rollupId(turn.provider, day);
|
|
247
|
+
const rollup = byId.get(id) ?? { id, provider: turn.provider, day, models: {}, hourly: [] };
|
|
248
|
+
const model = turn.model || "unknown";
|
|
249
|
+
const totals = rollup.models[model] ??= {
|
|
250
|
+
model,
|
|
251
|
+
turns: 0,
|
|
252
|
+
inputTokens: 0,
|
|
253
|
+
outputTokens: 0,
|
|
254
|
+
cacheReadTokens: 0,
|
|
255
|
+
cacheCreationTokens: 0,
|
|
256
|
+
cachedInputTokens: 0,
|
|
257
|
+
reasoningTokens: 0
|
|
258
|
+
};
|
|
259
|
+
totals.turns += direction;
|
|
260
|
+
totals.inputTokens = Math.max(0, totals.inputTokens + direction * turn.inputTokens);
|
|
261
|
+
totals.outputTokens = Math.max(0, totals.outputTokens + direction * turn.outputTokens);
|
|
262
|
+
totals.cacheReadTokens = Math.max(0, totals.cacheReadTokens + direction * turn.cacheReadTokens);
|
|
263
|
+
totals.cacheCreationTokens = Math.max(0, totals.cacheCreationTokens + direction * turn.cacheCreationTokens);
|
|
264
|
+
totals.cachedInputTokens = Math.max(0, totals.cachedInputTokens + direction * turn.cachedInputTokens);
|
|
265
|
+
totals.reasoningTokens = Math.max(0, totals.reasoningTokens + direction * turn.reasoningTokens);
|
|
266
|
+
if (totals.turns <= 0)
|
|
267
|
+
delete rollup.models[model];
|
|
268
|
+
const hour = hourOf(turn.ts);
|
|
269
|
+
if (hour !== null) {
|
|
270
|
+
const bucket = rollup.hourly.find((item) => item.hour === hour) ?? { hour, output: 0, turns: 0 };
|
|
271
|
+
bucket.output = Math.max(0, bucket.output + direction * turn.outputTokens);
|
|
272
|
+
bucket.turns += direction;
|
|
273
|
+
if (bucket.turns <= 0) {
|
|
274
|
+
rollup.hourly = rollup.hourly.filter((item) => item.hour !== hour);
|
|
275
|
+
} else if (!rollup.hourly.some((item) => item.hour === hour)) {
|
|
276
|
+
rollup.hourly.push(bucket);
|
|
277
|
+
}
|
|
278
|
+
rollup.hourly.sort((a, b) => a.hour - b.hour);
|
|
279
|
+
}
|
|
280
|
+
if (Object.keys(rollup.models).length === 0)
|
|
281
|
+
byId.delete(id);
|
|
282
|
+
else
|
|
283
|
+
byId.set(id, rollup);
|
|
284
|
+
}
|
|
245
285
|
function mergeLimits(existing, incoming) {
|
|
246
286
|
const byProvider = new Map(existing.map((limit) => [limit.provider, limit]));
|
|
247
287
|
for (const limit of incoming)
|
|
@@ -1014,13 +1054,34 @@ async function loadUpdateState() {
|
|
|
1014
1054
|
async function saveUpdateState(state) {
|
|
1015
1055
|
await writeJson(updatePath(), state);
|
|
1016
1056
|
}
|
|
1017
|
-
|
|
1057
|
+
var AgentAlreadyRunningError = class extends Error {
|
|
1058
|
+
lock;
|
|
1059
|
+
constructor(lock, cause) {
|
|
1060
|
+
super(`Tokelytics is already running (PID ${lock.pid}).`, { cause });
|
|
1061
|
+
this.name = "AgentAlreadyRunningError";
|
|
1062
|
+
this.lock = lock;
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
async function activeAgentLock() {
|
|
1066
|
+
const lock = await readJson(watchLockPath());
|
|
1067
|
+
if (!lock?.pid || !processIsAlive(lock.pid)) return null;
|
|
1068
|
+
return lock;
|
|
1069
|
+
}
|
|
1070
|
+
async function acquireWatchLock(owner = "watch") {
|
|
1018
1071
|
await ensureDir();
|
|
1019
1072
|
const file = watchLockPath();
|
|
1020
1073
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1021
1074
|
try {
|
|
1022
1075
|
const handle = await fs3.open(file, "wx");
|
|
1023
|
-
await handle.writeFile(
|
|
1076
|
+
await handle.writeFile(
|
|
1077
|
+
JSON.stringify({
|
|
1078
|
+
pid: process.pid,
|
|
1079
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1080
|
+
owner,
|
|
1081
|
+
scope: "machine"
|
|
1082
|
+
}),
|
|
1083
|
+
"utf-8"
|
|
1084
|
+
);
|
|
1024
1085
|
await handle.close();
|
|
1025
1086
|
let released = false;
|
|
1026
1087
|
return {
|
|
@@ -1036,7 +1097,7 @@ async function acquireWatchLock() {
|
|
|
1036
1097
|
if (code !== "EEXIST") throw error;
|
|
1037
1098
|
const lock = await readJson(file);
|
|
1038
1099
|
if (lock?.pid && processIsAlive(lock.pid)) {
|
|
1039
|
-
throw new
|
|
1100
|
+
throw new AgentAlreadyRunningError(lock, error);
|
|
1040
1101
|
}
|
|
1041
1102
|
await fs3.rm(file, { force: true });
|
|
1042
1103
|
}
|
|
@@ -1781,7 +1842,7 @@ var FirestoreSink = class {
|
|
|
1781
1842
|
};
|
|
1782
1843
|
|
|
1783
1844
|
// src/version.ts
|
|
1784
|
-
var AGENT_VERSION = "0.3.
|
|
1845
|
+
var AGENT_VERSION = "0.3.6";
|
|
1785
1846
|
|
|
1786
1847
|
// src/sync.ts
|
|
1787
1848
|
async function runSync(connectors, sink, state, device, limitCollectors = [], options = {}) {
|
|
@@ -1796,15 +1857,11 @@ async function runSync(connectors, sink, state, device, limitCollectors = [], op
|
|
|
1796
1857
|
let st = state;
|
|
1797
1858
|
const collected = [];
|
|
1798
1859
|
const limits = [];
|
|
1799
|
-
const byProvider = {};
|
|
1800
1860
|
for (const c of connectors) {
|
|
1801
1861
|
const { turns: turns2, limits: nativeLimits2 = [], state: next } = await c.collect(st);
|
|
1802
1862
|
st = next;
|
|
1803
1863
|
if (refreshLimits) limits.push(...nativeLimits2);
|
|
1804
|
-
|
|
1805
|
-
collected.push(t);
|
|
1806
|
-
byProvider[t.provider] = (byProvider[t.provider] ?? 0) + 1;
|
|
1807
|
-
}
|
|
1864
|
+
collected.push(...turns2);
|
|
1808
1865
|
}
|
|
1809
1866
|
if (refreshLimits) {
|
|
1810
1867
|
const providerLimits = await Promise.all(
|
|
@@ -1820,7 +1877,16 @@ async function runSync(connectors, sink, state, device, limitCollectors = [], op
|
|
|
1820
1877
|
}
|
|
1821
1878
|
const unique = /* @__PURE__ */ new Map();
|
|
1822
1879
|
for (const t of collected) unique.set(t.turnId, t);
|
|
1823
|
-
const
|
|
1880
|
+
const collectedTurns = [...unique.values()];
|
|
1881
|
+
const previousTurns = new Map((options.dashboard?.recentTurns ?? []).map((turn) => [turn.turnId, turn]));
|
|
1882
|
+
const turns = collectedTurns.filter((turn) => {
|
|
1883
|
+
const previous = previousTurns.get(turn.turnId);
|
|
1884
|
+
return !previous || !sameTurn(previous, turn);
|
|
1885
|
+
});
|
|
1886
|
+
const changedByProvider = {};
|
|
1887
|
+
for (const turn of turns) {
|
|
1888
|
+
changedByProvider[turn.provider] = (changedByProvider[turn.provider] ?? 0) + 1;
|
|
1889
|
+
}
|
|
1824
1890
|
const nativeLimits = refreshLimits ? dedupeLimits(limits) : [];
|
|
1825
1891
|
const changedLimits = nativeLimits.filter(
|
|
1826
1892
|
(limit) => publication.limitFingerprints?.[limit.provider] !== limitFingerprint(limit)
|
|
@@ -1886,14 +1952,18 @@ async function runSync(connectors, sink, state, device, limitCollectors = [], op
|
|
|
1886
1952
|
publicationError = error;
|
|
1887
1953
|
}
|
|
1888
1954
|
}
|
|
1955
|
+
const scheduledCloudWrite = publication.dashboardDirty && hasBudget && !options.suppressCloudPublish ? Number.isFinite(Date.parse(publication.lastCloudWriteAt ?? "")) ? new Date(
|
|
1956
|
+
Math.max(now.getTime(), Date.parse(publication.lastCloudWriteAt) + cloudSyncIntervalMs)
|
|
1957
|
+
).toISOString() : now.toISOString() : void 0;
|
|
1889
1958
|
return {
|
|
1890
1959
|
processedTurns: turns.length,
|
|
1891
1960
|
processedLimits: changedLimits.length,
|
|
1892
|
-
byProvider,
|
|
1961
|
+
byProvider: changedByProvider,
|
|
1893
1962
|
state: st,
|
|
1894
1963
|
dashboard,
|
|
1895
1964
|
publication,
|
|
1896
1965
|
published,
|
|
1966
|
+
nextCloudWriteAt: scheduledCloudWrite,
|
|
1897
1967
|
publicationError
|
|
1898
1968
|
};
|
|
1899
1969
|
}
|
|
@@ -1984,6 +2054,7 @@ async function createRunner() {
|
|
|
1984
2054
|
byProvider: res.byProvider,
|
|
1985
2055
|
published: res.published,
|
|
1986
2056
|
cloudWritesToday: res.publication.cloudWritesToday ?? 0,
|
|
2057
|
+
nextCloudWriteAt: res.nextCloudWriteAt,
|
|
1987
2058
|
publicationError: res.publicationError
|
|
1988
2059
|
};
|
|
1989
2060
|
}
|
|
@@ -3777,7 +3848,7 @@ function close(server) {
|
|
|
3777
3848
|
}
|
|
3778
3849
|
|
|
3779
3850
|
// src/watch.ts
|
|
3780
|
-
var
|
|
3851
|
+
var TURN_SETTLE_MS = 2500;
|
|
3781
3852
|
var LIMIT_REFRESH_MS = 6e4;
|
|
3782
3853
|
var FALLBACK_SCAN_MS = 5e3;
|
|
3783
3854
|
var DEVICE_HEARTBEAT_MS = 60 * 6e4;
|
|
@@ -3786,6 +3857,7 @@ var MAX_CLOUD_WRITES_PER_DAY = 16;
|
|
|
3786
3857
|
var QUOTA_RETRY_MS = 15 * 6e4;
|
|
3787
3858
|
var QUOTA_RETRY_JITTER_MS = 5 * 6e4;
|
|
3788
3859
|
async function watch2(runner) {
|
|
3860
|
+
const logState = {};
|
|
3789
3861
|
let localServer;
|
|
3790
3862
|
try {
|
|
3791
3863
|
localServer = await startLocalDashboardServer(() => runner.localSnapshot(), runner.uid);
|
|
@@ -3800,7 +3872,7 @@ async function watch2(runner) {
|
|
|
3800
3872
|
deviceHeartbeatMs: DEVICE_HEARTBEAT_MS,
|
|
3801
3873
|
cloudSyncIntervalMs: CLOUD_SYNC_INTERVAL_MS,
|
|
3802
3874
|
maxCloudWritesPerDay: MAX_CLOUD_WRITES_PER_DAY
|
|
3803
|
-
}, false);
|
|
3875
|
+
}, false, logState);
|
|
3804
3876
|
const paths = runner.watchPaths();
|
|
3805
3877
|
const watcher = esm_default.watch(paths, {
|
|
3806
3878
|
ignoreInitial: true,
|
|
@@ -3814,7 +3886,12 @@ async function watch2(runner) {
|
|
|
3814
3886
|
let running = false;
|
|
3815
3887
|
let scheduledAt = 0;
|
|
3816
3888
|
let cloudBlockedUntil = startupRetryAfter ? Date.now() + startupRetryAfter : 0;
|
|
3817
|
-
const
|
|
3889
|
+
const takePending = () => {
|
|
3890
|
+
const request = pending;
|
|
3891
|
+
pending = void 0;
|
|
3892
|
+
return request;
|
|
3893
|
+
};
|
|
3894
|
+
const schedule = (request, delay = TURN_SETTLE_MS) => {
|
|
3818
3895
|
pending = mergeRequests(pending, request);
|
|
3819
3896
|
if (running) return;
|
|
3820
3897
|
const target = Date.now() + delay;
|
|
@@ -3824,21 +3901,19 @@ async function watch2(runner) {
|
|
|
3824
3901
|
timer = setTimeout(async () => {
|
|
3825
3902
|
timer = void 0;
|
|
3826
3903
|
scheduledAt = 0;
|
|
3827
|
-
const next =
|
|
3828
|
-
pending = void 0;
|
|
3904
|
+
const next = takePending();
|
|
3829
3905
|
if (!next) return;
|
|
3830
3906
|
running = true;
|
|
3831
|
-
const retryAfterMs = await safeSync(runner, next, Date.now() < cloudBlockedUntil);
|
|
3907
|
+
const retryAfterMs = await safeSync(runner, next, Date.now() < cloudBlockedUntil, logState);
|
|
3832
3908
|
running = false;
|
|
3833
3909
|
if (retryAfterMs) {
|
|
3834
3910
|
cloudBlockedUntil = Date.now() + retryAfterMs;
|
|
3835
3911
|
} else if (Date.now() >= cloudBlockedUntil) {
|
|
3836
3912
|
cloudBlockedUntil = 0;
|
|
3837
3913
|
}
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
schedule(queued, 0);
|
|
3914
|
+
const queued = takePending();
|
|
3915
|
+
if (queued) {
|
|
3916
|
+
schedule(queued, queued.reason === "change" ? TURN_SETTLE_MS : 0);
|
|
3842
3917
|
}
|
|
3843
3918
|
}, Math.max(0, target - Date.now()));
|
|
3844
3919
|
};
|
|
@@ -3878,21 +3953,37 @@ Press Ctrl+C to stop.`);
|
|
|
3878
3953
|
process.once("SIGTERM", stop);
|
|
3879
3954
|
});
|
|
3880
3955
|
}
|
|
3881
|
-
async function safeSync(runner, request, suppressCloudPublish) {
|
|
3956
|
+
async function safeSync(runner, request, suppressCloudPublish, logState) {
|
|
3882
3957
|
try {
|
|
3883
3958
|
const { reason, ...options } = request;
|
|
3884
|
-
const {
|
|
3959
|
+
const {
|
|
3960
|
+
processedTurns,
|
|
3961
|
+
byProvider,
|
|
3962
|
+
published,
|
|
3963
|
+
cloudWritesToday,
|
|
3964
|
+
nextCloudWriteAt,
|
|
3965
|
+
publicationError
|
|
3966
|
+
} = await runner.syncOnce({
|
|
3885
3967
|
...options,
|
|
3886
3968
|
suppressCloudPublish
|
|
3887
3969
|
});
|
|
3888
3970
|
if (processedTurns > 0) {
|
|
3889
3971
|
const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ");
|
|
3890
3972
|
console.log(
|
|
3891
|
-
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] processed ${processedTurns} turn(s) locally (${detail})
|
|
3973
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] processed ${processedTurns} completed turn(s) locally (${detail}).`
|
|
3892
3974
|
);
|
|
3893
3975
|
} else if (reason === "startup") {
|
|
3976
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] local snapshot ready.`);
|
|
3977
|
+
}
|
|
3978
|
+
if (published) {
|
|
3979
|
+
logState.cloudSchedule = void 0;
|
|
3980
|
+
console.log(
|
|
3981
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] cloud snapshot published (${cloudWritesToday}/${MAX_CLOUD_WRITES_PER_DAY} writes today).`
|
|
3982
|
+
);
|
|
3983
|
+
} else if (nextCloudWriteAt && nextCloudWriteAt !== logState.cloudSchedule) {
|
|
3984
|
+
logState.cloudSchedule = nextCloudWriteAt;
|
|
3894
3985
|
console.log(
|
|
3895
|
-
`
|
|
3986
|
+
`Cloud backup scheduled for ${new Date(nextCloudWriteAt).toLocaleTimeString()} (new turns will be combined into this snapshot).`
|
|
3896
3987
|
);
|
|
3897
3988
|
}
|
|
3898
3989
|
if (publicationError) return cloudRetryDelay(publicationError);
|
|
@@ -3979,7 +4070,7 @@ var USAGE = `Tokelytics agent
|
|
|
3979
4070
|
Usage:
|
|
3980
4071
|
tokelytics login Sign in by approving in your browser
|
|
3981
4072
|
tokelytics sync Run one incremental sync to your dashboard
|
|
3982
|
-
tokelytics watch Watch
|
|
4073
|
+
tokelytics watch Watch Claude Code and Codex usage across this machine
|
|
3983
4074
|
tokelytics status Show current sign-in and device
|
|
3984
4075
|
tokelytics logout Forget stored credentials
|
|
3985
4076
|
`;
|
|
@@ -4001,7 +4092,7 @@ async function main(argv) {
|
|
|
4001
4092
|
case "sync": {
|
|
4002
4093
|
const update = await agentUpdateMessage();
|
|
4003
4094
|
if (update) console.warn(update);
|
|
4004
|
-
const lock = await acquireWatchLock();
|
|
4095
|
+
const lock = await acquireWatchLock("sync");
|
|
4005
4096
|
try {
|
|
4006
4097
|
const runner = await createRunner();
|
|
4007
4098
|
const { processedTurns, processedLimits, byProvider, publicationError } = await runner.syncOnce();
|
|
@@ -4014,7 +4105,17 @@ async function main(argv) {
|
|
|
4014
4105
|
return 0;
|
|
4015
4106
|
}
|
|
4016
4107
|
case "watch": {
|
|
4017
|
-
|
|
4108
|
+
let lock;
|
|
4109
|
+
try {
|
|
4110
|
+
lock = await acquireWatchLock("watch");
|
|
4111
|
+
} catch (err) {
|
|
4112
|
+
if (err instanceof AgentAlreadyRunningError && err.lock.owner !== "sync") {
|
|
4113
|
+
console.log(`Tokelytics is already watching this machine (PID ${err.lock.pid}).`);
|
|
4114
|
+
console.log("Claude Code and Codex usage from every folder and repository is already covered.");
|
|
4115
|
+
return 0;
|
|
4116
|
+
}
|
|
4117
|
+
throw err;
|
|
4118
|
+
}
|
|
4018
4119
|
try {
|
|
4019
4120
|
const update = await agentUpdateMessage();
|
|
4020
4121
|
if (update) console.warn(update);
|
|
@@ -4028,7 +4129,15 @@ async function main(argv) {
|
|
|
4028
4129
|
case "status": {
|
|
4029
4130
|
const creds = await loadCredentials();
|
|
4030
4131
|
const state = await loadState();
|
|
4132
|
+
const lock = await activeAgentLock();
|
|
4031
4133
|
console.log(`Agent version: ${AGENT_VERSION}`);
|
|
4134
|
+
if (lock && lock.owner !== "sync") {
|
|
4135
|
+
console.log(`Watcher: active machine-wide (PID ${lock.pid}); all folders and repositories are covered.`);
|
|
4136
|
+
} else if (lock?.owner === "sync") {
|
|
4137
|
+
console.log(`Watcher: sync currently in progress (PID ${lock.pid}).`);
|
|
4138
|
+
} else {
|
|
4139
|
+
console.log('Watcher: not running. Run "tokelytics watch" once on this machine.');
|
|
4140
|
+
}
|
|
4032
4141
|
if (!creds) {
|
|
4033
4142
|
console.log('Not signed in. Run "tokelytics login".');
|
|
4034
4143
|
} else {
|