vibeusage 0.2.16 → 0.2.17
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 +136 -20
- package/README.zh-CN.md +135 -19
- package/package.json +1 -1
- package/src/cli.js +3 -2
- package/src/commands/status.js +52 -1
- package/src/commands/sync.js +76 -11
- package/src/lib/codex-config.js +132 -10
- package/src/lib/diagnostics.js +3 -0
- package/src/lib/rollout.js +155 -1
- package/src/lib/subscriptions.js +317 -0
- package/src/lib/uploader.js +7 -1
- package/src/lib/vibeusage-api.js +7 -2
package/src/commands/sync.js
CHANGED
|
@@ -12,9 +12,11 @@ const {
|
|
|
12
12
|
parseRolloutIncremental,
|
|
13
13
|
parseClaudeIncremental,
|
|
14
14
|
parseGeminiIncremental,
|
|
15
|
-
parseOpencodeIncremental
|
|
15
|
+
parseOpencodeIncremental,
|
|
16
|
+
parseOpenclawIncremental
|
|
16
17
|
} = require('../lib/rollout');
|
|
17
18
|
const { drainQueueToCloud } = require('../lib/uploader');
|
|
19
|
+
const { collectLocalSubscriptions } = require('../lib/subscriptions');
|
|
18
20
|
const { createProgress, renderBar, formatNumber, formatBytes } = require('../lib/progress');
|
|
19
21
|
const { syncHeartbeat } = require('../lib/vibeusage-api');
|
|
20
22
|
const {
|
|
@@ -34,6 +36,9 @@ async function cmdSync(argv) {
|
|
|
34
36
|
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
35
37
|
|
|
36
38
|
await ensureDir(trackerDir);
|
|
39
|
+
if (opts.fromOpenclaw) {
|
|
40
|
+
await writeOpenclawSignal(trackerDir);
|
|
41
|
+
}
|
|
37
42
|
|
|
38
43
|
const lockPath = path.join(trackerDir, 'sync.lock');
|
|
39
44
|
const lock = await openLock(lockPath, { quietIfLocked: opts.auto });
|
|
@@ -64,10 +69,16 @@ async function cmdSync(argv) {
|
|
|
64
69
|
const opencodeHome = process.env.OPENCODE_HOME || path.join(xdgDataHome, 'opencode');
|
|
65
70
|
const opencodeStorageDir = path.join(opencodeHome, 'storage');
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
// OpenClaw hook integration: allow a hook to request incremental parsing for a single session jsonl.
|
|
73
|
+
// When present, we skip all other sources to keep hook-triggered sync fast and deterministic.
|
|
74
|
+
const openclawSignal = opts.fromOpenclaw ? resolveOpenclawSignal({ home, env: process.env }) : null;
|
|
75
|
+
|
|
76
|
+
const sources = openclawSignal
|
|
77
|
+
? []
|
|
78
|
+
: [
|
|
79
|
+
{ source: 'codex', sessionsDir: path.join(codexHome, 'sessions') },
|
|
80
|
+
{ source: 'every-code', sessionsDir: path.join(codeHome, 'sessions') }
|
|
81
|
+
];
|
|
71
82
|
|
|
72
83
|
const rolloutFiles = [];
|
|
73
84
|
const seenSessions = new Set();
|
|
@@ -80,6 +91,8 @@ async function cmdSync(argv) {
|
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
|
|
94
|
+
const openclawFiles = openclawSignal?.sessionFile ? [{ path: openclawSignal.sessionFile, source: 'openclaw' }] : [];
|
|
95
|
+
|
|
83
96
|
if (progress?.enabled) {
|
|
84
97
|
progress.start(`Parsing ${renderBar(0)} 0/${formatNumber(rolloutFiles.length)} files | buckets 0`);
|
|
85
98
|
}
|
|
@@ -100,9 +113,21 @@ async function cmdSync(argv) {
|
|
|
100
113
|
}
|
|
101
114
|
});
|
|
102
115
|
|
|
103
|
-
|
|
116
|
+
let openclawResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
117
|
+
if (openclawFiles.length > 0) {
|
|
118
|
+
// Only runs when explicitly triggered by OpenClaw hooks.
|
|
119
|
+
openclawResult = await parseOpenclawIncremental({
|
|
120
|
+
sessionFiles: openclawFiles,
|
|
121
|
+
cursors,
|
|
122
|
+
queuePath,
|
|
123
|
+
projectQueuePath,
|
|
124
|
+
source: 'openclaw'
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const claudeFiles = openclawSignal ? [] : await listClaudeProjectFiles(claudeProjectsDir);
|
|
104
129
|
let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
105
|
-
if (claudeFiles.length > 0) {
|
|
130
|
+
if (!openclawSignal && claudeFiles.length > 0) {
|
|
106
131
|
if (progress?.enabled) {
|
|
107
132
|
progress.start(`Parsing Claude ${renderBar(0)} 0/${formatNumber(claudeFiles.length)} files | buckets 0`);
|
|
108
133
|
}
|
|
@@ -124,9 +149,9 @@ async function cmdSync(argv) {
|
|
|
124
149
|
});
|
|
125
150
|
}
|
|
126
151
|
|
|
127
|
-
const geminiFiles = await listGeminiSessionFiles(geminiTmpDir);
|
|
152
|
+
const geminiFiles = openclawSignal ? [] : await listGeminiSessionFiles(geminiTmpDir);
|
|
128
153
|
let geminiResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
129
|
-
if (geminiFiles.length > 0) {
|
|
154
|
+
if (!openclawSignal && geminiFiles.length > 0) {
|
|
130
155
|
if (progress?.enabled) {
|
|
131
156
|
progress.start(`Parsing Gemini ${renderBar(0)} 0/${formatNumber(geminiFiles.length)} files | buckets 0`);
|
|
132
157
|
}
|
|
@@ -148,9 +173,9 @@ async function cmdSync(argv) {
|
|
|
148
173
|
});
|
|
149
174
|
}
|
|
150
175
|
|
|
151
|
-
const opencodeFiles = await listOpencodeMessageFiles(opencodeStorageDir);
|
|
176
|
+
const opencodeFiles = openclawSignal ? [] : await listOpencodeMessageFiles(opencodeStorageDir);
|
|
152
177
|
let opencodeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
153
|
-
if (opencodeFiles.length > 0) {
|
|
178
|
+
if (!openclawSignal && opencodeFiles.length > 0) {
|
|
154
179
|
if (progress?.enabled) {
|
|
155
180
|
progress.start(`Parsing Opencode ${renderBar(0)} 0/${formatNumber(opencodeFiles.length)} files | buckets 0`);
|
|
156
181
|
}
|
|
@@ -244,10 +269,17 @@ async function cmdSync(argv) {
|
|
|
244
269
|
|
|
245
270
|
if (allowUpload && maxBatches > 0) {
|
|
246
271
|
uploadAttempted = true;
|
|
272
|
+
const deviceSubscriptions = await collectLocalSubscriptions({
|
|
273
|
+
home,
|
|
274
|
+
env: process.env,
|
|
275
|
+
probeKeychain: true,
|
|
276
|
+
probeKeychainDetails: true
|
|
277
|
+
});
|
|
247
278
|
try {
|
|
248
279
|
uploadResult = await drainQueueToCloud({
|
|
249
280
|
baseUrl,
|
|
250
281
|
deviceToken,
|
|
282
|
+
deviceSubscriptions,
|
|
251
283
|
queuePath,
|
|
252
284
|
queueStatePath,
|
|
253
285
|
projectQueuePath,
|
|
@@ -330,11 +362,13 @@ async function cmdSync(argv) {
|
|
|
330
362
|
if (!opts.auto) {
|
|
331
363
|
const totalParsed =
|
|
332
364
|
parseResult.filesProcessed +
|
|
365
|
+
openclawResult.filesProcessed +
|
|
333
366
|
claudeResult.filesProcessed +
|
|
334
367
|
geminiResult.filesProcessed +
|
|
335
368
|
opencodeResult.filesProcessed;
|
|
336
369
|
const totalBuckets =
|
|
337
370
|
parseResult.bucketsQueued +
|
|
371
|
+
openclawResult.bucketsQueued +
|
|
338
372
|
claudeResult.bucketsQueued +
|
|
339
373
|
geminiResult.bucketsQueued +
|
|
340
374
|
opencodeResult.bucketsQueued;
|
|
@@ -367,6 +401,7 @@ function parseArgs(argv) {
|
|
|
367
401
|
auto: false,
|
|
368
402
|
fromNotify: false,
|
|
369
403
|
fromRetry: false,
|
|
404
|
+
fromOpenclaw: false,
|
|
370
405
|
drain: false
|
|
371
406
|
};
|
|
372
407
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -374,6 +409,7 @@ function parseArgs(argv) {
|
|
|
374
409
|
if (a === '--auto') out.auto = true;
|
|
375
410
|
else if (a === '--from-notify') out.fromNotify = true;
|
|
376
411
|
else if (a === '--from-retry') out.fromRetry = true;
|
|
412
|
+
else if (a === '--from-openclaw') out.fromOpenclaw = true;
|
|
377
413
|
else if (a === '--drain') out.drain = true;
|
|
378
414
|
else throw new Error(`Unknown option: ${a}`);
|
|
379
415
|
}
|
|
@@ -382,6 +418,26 @@ function parseArgs(argv) {
|
|
|
382
418
|
|
|
383
419
|
module.exports = { cmdSync };
|
|
384
420
|
|
|
421
|
+
function normalizeString(value) {
|
|
422
|
+
if (typeof value !== 'string') return null;
|
|
423
|
+
const trimmed = value.trim();
|
|
424
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function resolveOpenclawSignal({ home, env } = {}) {
|
|
428
|
+
if (!env) return null;
|
|
429
|
+
|
|
430
|
+
const agentId = normalizeString(env.VIBEUSAGE_OPENCLAW_AGENT_ID);
|
|
431
|
+
const sessionId = normalizeString(env.VIBEUSAGE_OPENCLAW_PREV_SESSION_ID);
|
|
432
|
+
if (!agentId || !sessionId) return null;
|
|
433
|
+
|
|
434
|
+
const openclawHome = normalizeString(env.VIBEUSAGE_OPENCLAW_HOME) || path.join(home || os.homedir(), '.openclaw');
|
|
435
|
+
const sessionFile = path.join(openclawHome, 'agents', agentId, 'sessions', `${sessionId}.jsonl`);
|
|
436
|
+
|
|
437
|
+
return { agentId, sessionId, openclawHome, sessionFile };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
385
441
|
async function safeStatSize(p) {
|
|
386
442
|
try {
|
|
387
443
|
const st = await fs.stat(p);
|
|
@@ -509,6 +565,15 @@ function coerceRetryMs(v) {
|
|
|
509
565
|
return Math.floor(n);
|
|
510
566
|
}
|
|
511
567
|
|
|
568
|
+
async function writeOpenclawSignal(trackerDir) {
|
|
569
|
+
const openclawSignalPath = path.join(trackerDir, 'openclaw.signal');
|
|
570
|
+
try {
|
|
571
|
+
await fs.writeFile(openclawSignalPath, new Date().toISOString(), 'utf8');
|
|
572
|
+
} catch (_e) {
|
|
573
|
+
// best-effort marker
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
512
577
|
const HEARTBEAT_MIN_INTERVAL_MINUTES = 30;
|
|
513
578
|
const HEARTBEAT_MIN_INTERVAL_MS = HEARTBEAT_MIN_INTERVAL_MINUTES * 60 * 1000;
|
|
514
579
|
const AUTO_RETRY_FILENAME = 'auto.retry.json';
|
package/src/lib/codex-config.js
CHANGED
|
@@ -117,14 +117,25 @@ async function readEveryCodeNotify(codeConfigPath) {
|
|
|
117
117
|
|
|
118
118
|
function extractNotify(text) {
|
|
119
119
|
// Heuristic parse: find a line that starts with "notify =".
|
|
120
|
+
// Supports single-line arrays:
|
|
121
|
+
// - notify = ["a", "b"]
|
|
122
|
+
// And multi-line arrays:
|
|
123
|
+
// - notify = [
|
|
124
|
+
// "a",
|
|
125
|
+
// "b"
|
|
126
|
+
// ]
|
|
120
127
|
const lines = text.split(/\r?\n/);
|
|
121
|
-
for (
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
129
|
+
const line = lines[i];
|
|
130
|
+
const m = line.match(/^\s*notify\s*=\s*(.*)\s*$/);
|
|
131
|
+
if (!m) continue;
|
|
132
|
+
|
|
133
|
+
const rhs = (m[1] || '').trim();
|
|
134
|
+
const literal = readTomlArrayLiteral(lines, i, rhs);
|
|
135
|
+
if (!literal) continue;
|
|
136
|
+
|
|
137
|
+
const parsed = parseTomlStringArray(literal);
|
|
138
|
+
if (parsed) return parsed;
|
|
128
139
|
}
|
|
129
140
|
return null;
|
|
130
141
|
}
|
|
@@ -137,12 +148,15 @@ function setNotify(text, notifyCmd) {
|
|
|
137
148
|
let replaced = false;
|
|
138
149
|
for (let i = 0; i < lines.length; i++) {
|
|
139
150
|
const line = lines[i];
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
151
|
+
const m = line.match(/^\s*notify\s*=\s*(.*)\s*$/);
|
|
152
|
+
if (m) {
|
|
142
153
|
if (!replaced) {
|
|
143
154
|
out.push(notifyLine);
|
|
144
155
|
replaced = true;
|
|
145
156
|
}
|
|
157
|
+
|
|
158
|
+
const rhs = (m[1] || '').trim();
|
|
159
|
+
i = findTomlArrayBlockEnd(lines, i, rhs);
|
|
146
160
|
continue;
|
|
147
161
|
}
|
|
148
162
|
out.push(line);
|
|
@@ -160,7 +174,17 @@ function setNotify(text, notifyCmd) {
|
|
|
160
174
|
|
|
161
175
|
function removeNotify(text) {
|
|
162
176
|
const lines = text.split(/\r?\n/);
|
|
163
|
-
const out =
|
|
177
|
+
const out = [];
|
|
178
|
+
for (let i = 0; i < lines.length; i++) {
|
|
179
|
+
const line = lines[i];
|
|
180
|
+
const m = line.match(/^\s*notify\s*=\s*(.*)\s*$/);
|
|
181
|
+
if (m) {
|
|
182
|
+
const rhs = (m[1] || '').trim();
|
|
183
|
+
i = findTomlArrayBlockEnd(lines, i, rhs);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
out.push(line);
|
|
187
|
+
}
|
|
164
188
|
return out.join('\n').replace(/\n+$/, '\n');
|
|
165
189
|
}
|
|
166
190
|
|
|
@@ -201,6 +225,104 @@ function formatTomlStringArray(arr) {
|
|
|
201
225
|
return `[${arr.map((s) => JSON.stringify(String(s))).join(', ')}]`;
|
|
202
226
|
}
|
|
203
227
|
|
|
228
|
+
function readTomlArrayLiteral(lines, startIndex, rhs) {
|
|
229
|
+
const first = rhs.trim();
|
|
230
|
+
if (!first.startsWith('[')) return null;
|
|
231
|
+
|
|
232
|
+
let inString = false;
|
|
233
|
+
let quote = null;
|
|
234
|
+
let depth = 0;
|
|
235
|
+
let sawOpen = false;
|
|
236
|
+
|
|
237
|
+
function scanChunk(chunk) {
|
|
238
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
239
|
+
const ch = chunk[i];
|
|
240
|
+
if (!inString) {
|
|
241
|
+
if (ch === '"' || ch === "'") {
|
|
242
|
+
inString = true;
|
|
243
|
+
quote = ch;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (ch === '[') {
|
|
247
|
+
depth += 1;
|
|
248
|
+
sawOpen = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (ch === ']') {
|
|
252
|
+
depth -= 1;
|
|
253
|
+
if (sawOpen && depth === 0) return i;
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (ch === quote) {
|
|
258
|
+
inString = false;
|
|
259
|
+
quote = null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return -1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const parts = [first];
|
|
266
|
+
let endPos = scanChunk(first);
|
|
267
|
+
if (endPos !== -1) return first.slice(0, endPos + 1).trim();
|
|
268
|
+
|
|
269
|
+
for (let j = startIndex + 1; j < lines.length; j++) {
|
|
270
|
+
const line = lines[j];
|
|
271
|
+
endPos = scanChunk(line);
|
|
272
|
+
if (endPos !== -1) {
|
|
273
|
+
parts.push(line.slice(0, endPos + 1));
|
|
274
|
+
return parts.join('\n').trim();
|
|
275
|
+
}
|
|
276
|
+
parts.push(line);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function findTomlArrayBlockEnd(lines, startIndex, rhs) {
|
|
283
|
+
const first = rhs.trim();
|
|
284
|
+
if (!first.startsWith('[')) return startIndex;
|
|
285
|
+
|
|
286
|
+
let inString = false;
|
|
287
|
+
let quote = null;
|
|
288
|
+
let depth = 0;
|
|
289
|
+
let sawOpen = false;
|
|
290
|
+
|
|
291
|
+
function scanChunk(chunk) {
|
|
292
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
293
|
+
const ch = chunk[i];
|
|
294
|
+
if (!inString) {
|
|
295
|
+
if (ch === '"' || ch === "'") {
|
|
296
|
+
inString = true;
|
|
297
|
+
quote = ch;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (ch === '[') {
|
|
301
|
+
depth += 1;
|
|
302
|
+
sawOpen = true;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (ch === ']') {
|
|
306
|
+
depth -= 1;
|
|
307
|
+
if (sawOpen && depth === 0) return true;
|
|
308
|
+
}
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (ch === quote) {
|
|
312
|
+
inString = false;
|
|
313
|
+
quote = null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (scanChunk(first)) return startIndex;
|
|
320
|
+
for (let j = startIndex + 1; j < lines.length; j++) {
|
|
321
|
+
if (scanChunk(lines[j])) return j;
|
|
322
|
+
}
|
|
323
|
+
return startIndex;
|
|
324
|
+
}
|
|
325
|
+
|
|
204
326
|
function arraysEqual(a, b) {
|
|
205
327
|
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
206
328
|
if (a.length !== b.length) return false;
|
package/src/lib/diagnostics.js
CHANGED
|
@@ -26,6 +26,7 @@ async function collectTrackerDiagnostics({
|
|
|
26
26
|
const queueStatePath = path.join(trackerDir, 'queue.state.json');
|
|
27
27
|
const cursorsPath = path.join(trackerDir, 'cursors.json');
|
|
28
28
|
const notifySignalPath = path.join(trackerDir, 'notify.signal');
|
|
29
|
+
const openclawSignalPath = path.join(trackerDir, 'openclaw.signal');
|
|
29
30
|
const throttlePath = path.join(trackerDir, 'sync.throttle');
|
|
30
31
|
const uploadThrottlePath = path.join(trackerDir, 'upload.throttle.json');
|
|
31
32
|
const autoRetryPath = path.join(trackerDir, 'auto.retry.json');
|
|
@@ -47,6 +48,7 @@ async function collectTrackerDiagnostics({
|
|
|
47
48
|
const pendingBytes = Math.max(0, queueSize - offsetBytes);
|
|
48
49
|
|
|
49
50
|
const lastNotify = (await safeReadText(notifySignalPath))?.trim() || null;
|
|
51
|
+
const lastOpenclawSync = (await safeReadText(openclawSignalPath))?.trim() || null;
|
|
50
52
|
const lastNotifySpawn = parseEpochMsToIso((await safeReadText(throttlePath))?.trim() || null);
|
|
51
53
|
|
|
52
54
|
const codexNotifyRaw = await readCodexNotify(codexConfigPath);
|
|
@@ -107,6 +109,7 @@ async function collectTrackerDiagnostics({
|
|
|
107
109
|
},
|
|
108
110
|
notify: {
|
|
109
111
|
last_notify: lastNotify,
|
|
112
|
+
last_openclaw_triggered_sync: lastOpenclawSync,
|
|
110
113
|
last_notify_triggered_sync: lastNotifySpawn,
|
|
111
114
|
codex_notify_configured: notifyConfigured,
|
|
112
115
|
codex_notify: codexNotify,
|
package/src/lib/rollout.js
CHANGED
|
@@ -527,6 +527,159 @@ async function parseOpencodeIncremental({
|
|
|
527
527
|
return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
async function parseOpenclawIncremental({
|
|
531
|
+
sessionFiles,
|
|
532
|
+
cursors,
|
|
533
|
+
queuePath,
|
|
534
|
+
projectQueuePath,
|
|
535
|
+
onProgress,
|
|
536
|
+
source
|
|
537
|
+
}) {
|
|
538
|
+
await ensureDir(path.dirname(queuePath));
|
|
539
|
+
let filesProcessed = 0;
|
|
540
|
+
let eventsAggregated = 0;
|
|
541
|
+
|
|
542
|
+
const cb = typeof onProgress === 'function' ? onProgress : null;
|
|
543
|
+
const files = Array.isArray(sessionFiles) ? sessionFiles : [];
|
|
544
|
+
const totalFiles = files.length;
|
|
545
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
546
|
+
const projectEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
|
|
547
|
+
const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
|
|
548
|
+
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
549
|
+
const touchedBuckets = new Set();
|
|
550
|
+
const defaultSource = normalizeSourceInput(source) || 'openclaw';
|
|
551
|
+
|
|
552
|
+
if (!cursors.files || typeof cursors.files !== 'object') {
|
|
553
|
+
cursors.files = {};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
for (let idx = 0; idx < files.length; idx++) {
|
|
557
|
+
const entry = files[idx];
|
|
558
|
+
const filePath = typeof entry === 'string' ? entry : entry?.path;
|
|
559
|
+
if (!filePath) continue;
|
|
560
|
+
const fileSource =
|
|
561
|
+
typeof entry === 'string' ? defaultSource : normalizeSourceInput(entry?.source) || defaultSource;
|
|
562
|
+
const st = await fs.stat(filePath).catch(() => null);
|
|
563
|
+
if (!st || !st.isFile()) continue;
|
|
564
|
+
|
|
565
|
+
const key = filePath;
|
|
566
|
+
const prev = cursors.files[key] || null;
|
|
567
|
+
const inode = st.ino || 0;
|
|
568
|
+
const startOffset = prev && prev.inode === inode ? prev.offset || 0 : 0;
|
|
569
|
+
|
|
570
|
+
const result = await parseOpenclawSessionFile({
|
|
571
|
+
filePath,
|
|
572
|
+
startOffset,
|
|
573
|
+
hourlyState,
|
|
574
|
+
touchedBuckets,
|
|
575
|
+
source: fileSource,
|
|
576
|
+
projectState,
|
|
577
|
+
projectTouchedBuckets
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
cursors.files[key] = {
|
|
581
|
+
inode,
|
|
582
|
+
offset: result.endOffset,
|
|
583
|
+
updatedAt: new Date().toISOString()
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
filesProcessed += 1;
|
|
587
|
+
eventsAggregated += result.eventsAggregated;
|
|
588
|
+
|
|
589
|
+
if (cb) {
|
|
590
|
+
cb({
|
|
591
|
+
index: idx + 1,
|
|
592
|
+
total: totalFiles,
|
|
593
|
+
filePath,
|
|
594
|
+
filesProcessed,
|
|
595
|
+
eventsAggregated,
|
|
596
|
+
bucketsQueued: touchedBuckets.size
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
602
|
+
const projectBucketsQueued = projectEnabled
|
|
603
|
+
? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
|
|
604
|
+
: 0;
|
|
605
|
+
hourlyState.updatedAt = new Date().toISOString();
|
|
606
|
+
cursors.hourly = hourlyState;
|
|
607
|
+
if (projectState) {
|
|
608
|
+
projectState.updatedAt = new Date().toISOString();
|
|
609
|
+
cursors.projectHourly = projectState;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function parseOpenclawSessionFile({
|
|
616
|
+
filePath,
|
|
617
|
+
startOffset,
|
|
618
|
+
hourlyState,
|
|
619
|
+
touchedBuckets,
|
|
620
|
+
source,
|
|
621
|
+
projectState,
|
|
622
|
+
projectTouchedBuckets
|
|
623
|
+
}) {
|
|
624
|
+
const st = await fs.stat(filePath);
|
|
625
|
+
const endOffset = st.size;
|
|
626
|
+
if (startOffset >= endOffset) return { endOffset, eventsAggregated: 0 };
|
|
627
|
+
|
|
628
|
+
const stream = fssync.createReadStream(filePath, { encoding: 'utf8', start: startOffset });
|
|
629
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
630
|
+
|
|
631
|
+
let eventsAggregated = 0;
|
|
632
|
+
for await (const line of rl) {
|
|
633
|
+
if (!line) continue;
|
|
634
|
+
// Fast-path filter: OpenClaw assistant messages include message.usage.totalTokens.
|
|
635
|
+
if (!line.includes('"usage"') || !line.includes('totalTokens')) continue;
|
|
636
|
+
|
|
637
|
+
let obj;
|
|
638
|
+
try {
|
|
639
|
+
obj = JSON.parse(line);
|
|
640
|
+
} catch (_e) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (obj?.type !== 'message') continue;
|
|
645
|
+
const msg = obj?.message;
|
|
646
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
647
|
+
|
|
648
|
+
const usage = msg.usage;
|
|
649
|
+
if (!usage || typeof usage !== 'object') continue;
|
|
650
|
+
|
|
651
|
+
const tokenTimestamp = typeof obj?.timestamp === 'string' ? obj.timestamp : null;
|
|
652
|
+
if (!tokenTimestamp) continue;
|
|
653
|
+
|
|
654
|
+
const model = normalizeModelInput(msg.model) || DEFAULT_MODEL;
|
|
655
|
+
|
|
656
|
+
const delta = {
|
|
657
|
+
input_tokens: Number(usage.input || 0),
|
|
658
|
+
cached_input_tokens: Number((usage.cacheRead || 0) + (usage.cacheWrite || 0)),
|
|
659
|
+
output_tokens: Number(usage.output || 0),
|
|
660
|
+
reasoning_output_tokens: 0,
|
|
661
|
+
total_tokens: Number(usage.totalTokens || 0)
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
if (isAllZeroUsage(delta)) continue;
|
|
665
|
+
|
|
666
|
+
const bucketStart = toUtcHalfHourStart(tokenTimestamp);
|
|
667
|
+
if (!bucketStart) continue;
|
|
668
|
+
|
|
669
|
+
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
670
|
+
addTotals(bucket.totals, delta);
|
|
671
|
+
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
672
|
+
|
|
673
|
+
// Project-level OpenClaw attribution is not supported yet (no stable cwd info).
|
|
674
|
+
// If OpenClaw later records cwd per event, we can mirror rollout's project logic.
|
|
675
|
+
eventsAggregated += 1;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
rl.close();
|
|
679
|
+
stream.close?.();
|
|
680
|
+
return { endOffset, eventsAggregated };
|
|
681
|
+
}
|
|
682
|
+
|
|
530
683
|
async function parseRolloutFile({
|
|
531
684
|
filePath,
|
|
532
685
|
startOffset,
|
|
@@ -1972,5 +2125,6 @@ module.exports = {
|
|
|
1972
2125
|
parseRolloutIncremental,
|
|
1973
2126
|
parseClaudeIncremental,
|
|
1974
2127
|
parseGeminiIncremental,
|
|
1975
|
-
parseOpencodeIncremental
|
|
2128
|
+
parseOpencodeIncremental,
|
|
2129
|
+
parseOpenclawIncremental
|
|
1976
2130
|
};
|