vibeusage 0.2.21 → 0.2.22

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.
Files changed (40) hide show
  1. package/README.md +306 -173
  2. package/README.old.md +324 -0
  3. package/README.zh-CN.md +304 -188
  4. package/package.json +32 -32
  5. package/src/cli.js +41 -37
  6. package/src/commands/activate-if-needed.js +41 -0
  7. package/src/commands/diagnostics.js +8 -9
  8. package/src/commands/doctor.js +31 -26
  9. package/src/commands/init.js +285 -218
  10. package/src/commands/status.js +86 -83
  11. package/src/commands/sync.js +182 -130
  12. package/src/commands/uninstall.js +66 -62
  13. package/src/lib/activation-check.js +290 -0
  14. package/src/lib/browser-auth.js +52 -54
  15. package/src/lib/claude-config.js +25 -25
  16. package/src/lib/cli-ui.js +35 -35
  17. package/src/lib/codex-config.js +40 -36
  18. package/src/lib/debug-flags.js +2 -2
  19. package/src/lib/diagnostics.js +70 -57
  20. package/src/lib/doctor.js +139 -132
  21. package/src/lib/fs.js +17 -17
  22. package/src/lib/gemini-config.js +44 -40
  23. package/src/lib/init-flow.js +16 -22
  24. package/src/lib/insforge-client.js +10 -10
  25. package/src/lib/insforge.js +9 -3
  26. package/src/lib/openclaw-hook.js +89 -66
  27. package/src/lib/openclaw-session-plugin.js +116 -92
  28. package/src/lib/opencode-config.js +31 -32
  29. package/src/lib/opencode-usage-audit.js +34 -31
  30. package/src/lib/progress.js +12 -13
  31. package/src/lib/project-usage-purge.js +23 -17
  32. package/src/lib/prompt.js +8 -4
  33. package/src/lib/rollout.js +342 -241
  34. package/src/lib/runtime-config.js +34 -22
  35. package/src/lib/subscriptions.js +94 -92
  36. package/src/lib/tracker-paths.js +6 -6
  37. package/src/lib/upload-throttle.js +35 -16
  38. package/src/lib/uploader.js +33 -29
  39. package/src/lib/vibeusage-api.js +72 -56
  40. package/src/lib/vibeusage-public-repo.js +41 -24
@@ -1,9 +1,9 @@
1
- const os = require('node:os');
2
- const path = require('node:path');
3
- const fs = require('node:fs/promises');
4
- const cp = require('node:child_process');
1
+ const os = require("node:os");
2
+ const path = require("node:path");
3
+ const fs = require("node:fs/promises");
4
+ const cp = require("node:child_process");
5
5
 
6
- const { ensureDir, readJson, writeJson, openLock } = require('../lib/fs');
6
+ const { ensureDir, readJson, writeJson, openLock } = require("../lib/fs");
7
7
  const {
8
8
  listRolloutFiles,
9
9
  listClaudeProjectFiles,
@@ -13,22 +13,23 @@ const {
13
13
  parseClaudeIncremental,
14
14
  parseGeminiIncremental,
15
15
  parseOpencodeIncremental,
16
- parseOpenclawIncremental
17
- } = require('../lib/rollout');
18
- const { drainQueueToCloud } = require('../lib/uploader');
19
- const { collectLocalSubscriptions } = require('../lib/subscriptions');
20
- const { createProgress, renderBar, formatNumber, formatBytes } = require('../lib/progress');
21
- const { syncHeartbeat } = require('../lib/vibeusage-api');
16
+ parseOpenclawIncremental,
17
+ } = require("../lib/rollout");
18
+ const { drainQueueToCloud } = require("../lib/uploader");
19
+ const { collectLocalSubscriptions } = require("../lib/subscriptions");
20
+ const { createProgress, renderBar, formatNumber, formatBytes } = require("../lib/progress");
21
+ const { syncHeartbeat } = require("../lib/vibeusage-api");
22
22
  const {
23
23
  DEFAULTS: UPLOAD_DEFAULTS,
24
24
  normalizeState: normalizeUploadState,
25
25
  decideAutoUpload,
26
26
  recordUploadSuccess,
27
- recordUploadFailure
28
- } = require('../lib/upload-throttle');
29
- const { purgeProjectUsage } = require('../lib/project-usage-purge');
30
- const { resolveTrackerPaths } = require('../lib/tracker-paths');
31
- const { resolveRuntimeConfig } = require('../lib/runtime-config');
27
+ recordUploadFailure,
28
+ } = require("../lib/upload-throttle");
29
+ const { purgeProjectUsage } = require("../lib/project-usage-purge");
30
+ const { resolveTrackerPaths } = require("../lib/tracker-paths");
31
+ const { resolveRuntimeConfig } = require("../lib/runtime-config");
32
+ const { checkAndActivate } = require("../lib/activation-check");
32
33
 
33
34
  async function cmdSync(argv) {
34
35
  const opts = parseArgs(argv);
@@ -40,42 +41,44 @@ async function cmdSync(argv) {
40
41
  await writeOpenclawSignal(trackerDir);
41
42
  }
42
43
 
43
- const lockPath = path.join(trackerDir, 'sync.lock');
44
+ const lockPath = path.join(trackerDir, "sync.lock");
44
45
  const lock = await openLock(lockPath, { quietIfLocked: opts.auto });
45
46
  if (!lock) return;
46
47
 
47
48
  let progress = null;
48
49
  try {
49
50
  progress = !opts.auto ? createProgress({ stream: process.stdout }) : null;
50
- const configPath = path.join(trackerDir, 'config.json');
51
- const cursorsPath = path.join(trackerDir, 'cursors.json');
52
- const queuePath = path.join(trackerDir, 'queue.jsonl');
53
- const queueStatePath = path.join(trackerDir, 'queue.state.json');
54
- const projectQueuePath = path.join(trackerDir, 'project.queue.jsonl');
55
- const projectQueueStatePath = path.join(trackerDir, 'project.queue.state.json');
56
- const uploadThrottlePath = path.join(trackerDir, 'upload.throttle.json');
51
+ const configPath = path.join(trackerDir, "config.json");
52
+ const cursorsPath = path.join(trackerDir, "cursors.json");
53
+ const queuePath = path.join(trackerDir, "queue.jsonl");
54
+ const queueStatePath = path.join(trackerDir, "queue.state.json");
55
+ const projectQueuePath = path.join(trackerDir, "project.queue.jsonl");
56
+ const projectQueueStatePath = path.join(trackerDir, "project.queue.state.json");
57
+ const uploadThrottlePath = path.join(trackerDir, "upload.throttle.json");
57
58
 
58
59
  const config = await readJson(configPath);
59
60
  const cursors = (await readJson(cursorsPath)) || { version: 1, files: {}, updatedAt: null };
60
61
  const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
61
62
  let uploadThrottleState = uploadThrottle;
62
63
 
63
- const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
64
- const codeHome = process.env.CODE_HOME || path.join(home, '.code');
65
- const claudeProjectsDir = path.join(home, '.claude', 'projects');
66
- const geminiHome = process.env.GEMINI_HOME || path.join(home, '.gemini');
67
- const geminiTmpDir = path.join(geminiHome, 'tmp');
68
- const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, '.local', 'share');
69
- const opencodeHome = process.env.OPENCODE_HOME || path.join(xdgDataHome, 'opencode');
70
- const opencodeStorageDir = path.join(opencodeHome, 'storage');
64
+ const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
65
+ const codeHome = process.env.CODE_HOME || path.join(home, ".code");
66
+ const claudeProjectsDir = path.join(home, ".claude", "projects");
67
+ const geminiHome = process.env.GEMINI_HOME || path.join(home, ".gemini");
68
+ const geminiTmpDir = path.join(geminiHome, "tmp");
69
+ const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
70
+ const opencodeHome = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
71
+ const opencodeStorageDir = path.join(opencodeHome, "storage");
71
72
 
72
73
  // OpenClaw hook integration: allow a hook to request incremental parsing for a single session jsonl.
73
74
  // We still parse all regular sources so model/source attribution stays complete (e.g. Kimi sessions).
74
- const openclawSignal = opts.fromOpenclaw ? resolveOpenclawSignal({ home, env: process.env }) : null;
75
+ const openclawSignal = opts.fromOpenclaw
76
+ ? resolveOpenclawSignal({ home, env: process.env })
77
+ : null;
75
78
 
76
79
  const sources = [
77
- { source: 'codex', sessionsDir: path.join(codexHome, 'sessions') },
78
- { source: 'every-code', sessionsDir: path.join(codeHome, 'sessions') }
80
+ { source: "codex", sessionsDir: path.join(codexHome, "sessions") },
81
+ { source: "every-code", sessionsDir: path.join(codeHome, "sessions") },
79
82
  ];
80
83
 
81
84
  const rolloutFiles = [];
@@ -89,10 +92,14 @@ async function cmdSync(argv) {
89
92
  }
90
93
  }
91
94
 
92
- const openclawFiles = openclawSignal?.sessionFile ? [{ path: openclawSignal.sessionFile, source: 'openclaw' }] : [];
95
+ const openclawFiles = openclawSignal?.sessionFile
96
+ ? [{ path: openclawSignal.sessionFile, source: "openclaw" }]
97
+ : [];
93
98
 
94
99
  if (progress?.enabled) {
95
- progress.start(`Parsing ${renderBar(0)} 0/${formatNumber(rolloutFiles.length)} files | buckets 0`);
100
+ progress.start(
101
+ `Parsing ${renderBar(0)} 0/${formatNumber(rolloutFiles.length)} files | buckets 0`,
102
+ );
96
103
  }
97
104
 
98
105
  const parseResult = await parseRolloutIncremental({
@@ -105,10 +112,10 @@ async function cmdSync(argv) {
105
112
  const pct = p.total > 0 ? p.index / p.total : 1;
106
113
  progress.update(
107
114
  `Parsing ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
108
- p.bucketsQueued
109
- )}`
115
+ p.bucketsQueued,
116
+ )}`,
110
117
  );
111
- }
118
+ },
112
119
  });
113
120
 
114
121
  let openclawResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
@@ -119,7 +126,7 @@ async function cmdSync(argv) {
119
126
  cursors,
120
127
  queuePath,
121
128
  projectQueuePath,
122
- source: 'openclaw'
129
+ source: "openclaw",
123
130
  });
124
131
  }
125
132
 
@@ -128,7 +135,7 @@ async function cmdSync(argv) {
128
135
  signal: openclawSignal,
129
136
  cursors,
130
137
  queuePath,
131
- projectQueuePath
138
+ projectQueuePath,
132
139
  });
133
140
  openclawResult.filesProcessed += openclawFallback.filesProcessed;
134
141
  openclawResult.eventsAggregated += openclawFallback.eventsAggregated;
@@ -138,7 +145,9 @@ async function cmdSync(argv) {
138
145
  let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
139
146
  if (claudeFiles.length > 0) {
140
147
  if (progress?.enabled) {
141
- progress.start(`Parsing Claude ${renderBar(0)} 0/${formatNumber(claudeFiles.length)} files | buckets 0`);
148
+ progress.start(
149
+ `Parsing Claude ${renderBar(0)} 0/${formatNumber(claudeFiles.length)} files | buckets 0`,
150
+ );
142
151
  }
143
152
  claudeResult = await parseClaudeIncremental({
144
153
  projectFiles: claudeFiles,
@@ -150,11 +159,11 @@ async function cmdSync(argv) {
150
159
  const pct = p.total > 0 ? p.index / p.total : 1;
151
160
  progress.update(
152
161
  `Parsing Claude ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
153
- p.bucketsQueued
154
- )}`
162
+ p.bucketsQueued,
163
+ )}`,
155
164
  );
156
165
  },
157
- source: 'claude'
166
+ source: "claude",
158
167
  });
159
168
  }
160
169
 
@@ -162,7 +171,9 @@ async function cmdSync(argv) {
162
171
  let geminiResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
163
172
  if (geminiFiles.length > 0) {
164
173
  if (progress?.enabled) {
165
- progress.start(`Parsing Gemini ${renderBar(0)} 0/${formatNumber(geminiFiles.length)} files | buckets 0`);
174
+ progress.start(
175
+ `Parsing Gemini ${renderBar(0)} 0/${formatNumber(geminiFiles.length)} files | buckets 0`,
176
+ );
166
177
  }
167
178
  geminiResult = await parseGeminiIncremental({
168
179
  sessionFiles: geminiFiles,
@@ -174,11 +185,11 @@ async function cmdSync(argv) {
174
185
  const pct = p.total > 0 ? p.index / p.total : 1;
175
186
  progress.update(
176
187
  `Parsing Gemini ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
177
- p.bucketsQueued
178
- )}`
188
+ p.bucketsQueued,
189
+ )}`,
179
190
  );
180
191
  },
181
- source: 'gemini'
192
+ source: "gemini",
182
193
  });
183
194
  }
184
195
 
@@ -186,7 +197,9 @@ async function cmdSync(argv) {
186
197
  let opencodeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
187
198
  if (opencodeFiles.length > 0) {
188
199
  if (progress?.enabled) {
189
- progress.start(`Parsing Opencode ${renderBar(0)} 0/${formatNumber(opencodeFiles.length)} files | buckets 0`);
200
+ progress.start(
201
+ `Parsing Opencode ${renderBar(0)} 0/${formatNumber(opencodeFiles.length)} files | buckets 0`,
202
+ );
190
203
  }
191
204
  opencodeResult = await parseOpencodeIncremental({
192
205
  messageFiles: opencodeFiles,
@@ -198,23 +211,23 @@ async function cmdSync(argv) {
198
211
  const pct = p.total > 0 ? p.index / p.total : 1;
199
212
  progress.update(
200
213
  `Parsing Opencode ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
201
- p.total
202
- )} files | buckets ${formatNumber(p.bucketsQueued)}`
214
+ p.total,
215
+ )} files | buckets ${formatNumber(p.bucketsQueued)}`,
203
216
  );
204
217
  },
205
- source: 'opencode'
218
+ source: "opencode",
206
219
  });
207
220
  }
208
221
 
209
222
  if (cursors?.projectHourly?.projects && projectQueuePath && projectQueueStatePath) {
210
223
  for (const [projectKey, meta] of Object.entries(cursors.projectHourly.projects)) {
211
- if (!meta || typeof meta !== 'object') continue;
212
- if (meta.status !== 'blocked' || !meta.purge_pending) continue;
224
+ if (!meta || typeof meta !== "object") continue;
225
+ if (meta.status !== "blocked" || !meta.purge_pending) continue;
213
226
  await purgeProjectUsage({
214
227
  projectKey,
215
228
  projectQueuePath,
216
229
  projectQueueStatePath,
217
- projectState: cursors.projectHourly
230
+ projectState: cursors.projectHourly,
218
231
  });
219
232
  meta.purge_pending = false;
220
233
  meta.purged_at = new Date().toISOString();
@@ -249,7 +262,7 @@ async function cmdSync(argv) {
249
262
  autoDecision = decideAutoUpload({
250
263
  nowMs: Date.now(),
251
264
  pendingBytes,
252
- state: uploadThrottle
265
+ state: uploadThrottle,
253
266
  });
254
267
  allowUpload = allowUpload && autoDecision.allowed;
255
268
  maxBatches = autoDecision.allowed ? autoDecision.maxBatches : 0;
@@ -261,18 +274,19 @@ async function cmdSync(argv) {
261
274
  retryAtMs: autoDecision.blockedUntilMs,
262
275
  reason,
263
276
  pendingBytes,
264
- source: 'auto-throttled',
265
- autoRetryNoSpawn: runtime.autoRetryNoSpawn
277
+ source: "auto-throttled",
278
+ autoRetryNoSpawn: runtime.autoRetryNoSpawn,
266
279
  });
267
280
  }
268
281
  }
269
282
 
270
283
  if (progress?.enabled && pendingBytes > 0 && allowUpload) {
271
284
  const totalSize = queueSize + projectQueueSize;
272
- const totalOffset = Number(beforeState.offset || 0) + Number(projectBeforeState.offset || 0);
285
+ const totalOffset =
286
+ Number(beforeState.offset || 0) + Number(projectBeforeState.offset || 0);
273
287
  const pct = totalSize > 0 ? totalOffset / totalSize : 0;
274
288
  progress.start(
275
- `Uploading ${renderBar(pct)} ${formatBytes(totalOffset)}/${formatBytes(totalSize)} | inserted 0 skipped 0`
289
+ `Uploading ${renderBar(pct)} ${formatBytes(totalOffset)}/${formatBytes(totalSize)} | inserted 0 skipped 0`,
276
290
  );
277
291
  }
278
292
 
@@ -282,7 +296,7 @@ async function cmdSync(argv) {
282
296
  home,
283
297
  env: process.env,
284
298
  probeKeychain: true,
285
- probeKeychainDetails: true
299
+ probeKeychainDetails: true,
286
300
  });
287
301
  try {
288
302
  uploadResult = await drainQueueToCloud({
@@ -300,10 +314,10 @@ async function cmdSync(argv) {
300
314
  const pct = u.queueSize > 0 ? u.offset / u.queueSize : 1;
301
315
  progress.update(
302
316
  `Uploading ${renderBar(pct)} ${formatBytes(u.offset)}/${formatBytes(u.queueSize)} | inserted ${formatNumber(
303
- u.inserted
304
- )} skipped ${formatNumber(u.skipped)}`
317
+ u.inserted,
318
+ )} skipped ${formatNumber(u.skipped)}`,
305
319
  );
306
- }
320
+ },
307
321
  });
308
322
  if (uploadResult.attempted > 0) {
309
323
  const next = recordUploadSuccess({ nowMs: Date.now(), state: uploadThrottleState });
@@ -311,7 +325,11 @@ async function cmdSync(argv) {
311
325
  await writeJson(uploadThrottlePath, next);
312
326
  }
313
327
  } catch (e) {
314
- const next = recordUploadFailure({ nowMs: Date.now(), state: uploadThrottleState, error: e });
328
+ const next = recordUploadFailure({
329
+ nowMs: Date.now(),
330
+ state: uploadThrottleState,
331
+ error: e,
332
+ });
315
333
  uploadThrottleState = next;
316
334
  await writeJson(uploadThrottlePath, next);
317
335
  if (opts.auto && pendingBytes > 0) {
@@ -320,10 +338,10 @@ async function cmdSync(argv) {
320
338
  await scheduleAutoRetry({
321
339
  trackerDir,
322
340
  retryAtMs,
323
- reason: 'backoff',
341
+ reason: "backoff",
324
342
  pendingBytes,
325
- source: 'auto-error',
326
- autoRetryNoSpawn: runtime.autoRetryNoSpawn
343
+ source: "auto-error",
344
+ autoRetryNoSpawn: runtime.autoRetryNoSpawn,
327
345
  });
328
346
  }
329
347
  }
@@ -352,10 +370,10 @@ async function cmdSync(argv) {
352
370
  await scheduleAutoRetry({
353
371
  trackerDir,
354
372
  retryAtMs,
355
- reason: 'backlog',
373
+ reason: "backlog",
356
374
  pendingBytes,
357
- source: 'auto-backlog',
358
- autoRetryNoSpawn: runtime.autoRetryNoSpawn
375
+ source: "auto-backlog",
376
+ autoRetryNoSpawn: runtime.autoRetryNoSpawn,
359
377
  });
360
378
  }
361
379
  }
@@ -365,7 +383,7 @@ async function cmdSync(argv) {
365
383
  deviceToken,
366
384
  trackerDir,
367
385
  uploadResult,
368
- pendingBytes
386
+ pendingBytes,
369
387
  });
370
388
 
371
389
  if (!opts.auto) {
@@ -383,25 +401,28 @@ async function cmdSync(argv) {
383
401
  opencodeResult.bucketsQueued;
384
402
  process.stdout.write(
385
403
  [
386
- 'Sync finished:',
404
+ "Sync finished:",
387
405
  `- Parsed files: ${totalParsed}`,
388
406
  `- New 30-min buckets queued: ${totalBuckets}`,
389
407
  deviceToken
390
408
  ? `- Uploaded: ${uploadResult.inserted} inserted, ${uploadResult.skipped} skipped`
391
- : '- Uploaded: skipped (no device token)',
409
+ : "- Uploaded: skipped (no device token)",
392
410
  deviceToken && pendingBytes > 0 && !opts.drain
393
411
  ? `- Remaining: ${formatBytes(pendingBytes)} pending (run sync again, or use --drain)`
394
412
  : null,
395
- ''
413
+ "",
396
414
  ]
397
415
  .filter(Boolean)
398
- .join('\n')
416
+ .join("\n"),
399
417
  );
400
418
  }
401
419
  } finally {
402
420
  progress?.stop();
403
421
  await lock.release();
404
422
  await fs.unlink(lockPath).catch(() => {});
423
+
424
+ // 顺带检测并激活其他 AI CLI 集成(静默执行,不阻塞主流程)
425
+ checkAndActivate({ home, silent: true }).catch(() => {});
405
426
  }
406
427
  }
407
428
 
@@ -411,15 +432,15 @@ function parseArgs(argv) {
411
432
  fromNotify: false,
412
433
  fromRetry: false,
413
434
  fromOpenclaw: false,
414
- drain: false
435
+ drain: false,
415
436
  };
416
437
  for (let i = 0; i < argv.length; i++) {
417
438
  const a = argv[i];
418
- if (a === '--auto') out.auto = true;
419
- else if (a === '--from-notify') out.fromNotify = true;
420
- else if (a === '--from-retry') out.fromRetry = true;
421
- else if (a === '--from-openclaw') out.fromOpenclaw = true;
422
- else if (a === '--drain') out.drain = true;
439
+ if (a === "--auto") out.auto = true;
440
+ else if (a === "--from-notify") out.fromNotify = true;
441
+ else if (a === "--from-retry") out.fromRetry = true;
442
+ else if (a === "--from-openclaw") out.fromOpenclaw = true;
443
+ else if (a === "--drain") out.drain = true;
423
444
  else throw new Error(`Unknown option: ${a}`);
424
445
  }
425
446
  return out;
@@ -428,7 +449,7 @@ function parseArgs(argv) {
428
449
  module.exports = { cmdSync };
429
450
 
430
451
  function normalizeString(value) {
431
- if (typeof value !== 'string') return null;
452
+ if (typeof value !== "string") return null;
432
453
  const trimmed = value.trim();
433
454
  return trimmed.length > 0 ? trimmed : null;
434
455
  }
@@ -440,15 +461,16 @@ function resolveOpenclawSignal({ home, env } = {}) {
440
461
  const sessionId = normalizeString(env.VIBEUSAGE_OPENCLAW_PREV_SESSION_ID);
441
462
  if (!agentId || !sessionId) return null;
442
463
 
443
- const openclawHome = normalizeString(env.VIBEUSAGE_OPENCLAW_HOME) || path.join(home || os.homedir(), '.openclaw');
444
- const sessionFile = path.join(openclawHome, 'agents', agentId, 'sessions', `${sessionId}.jsonl`);
464
+ const openclawHome =
465
+ normalizeString(env.VIBEUSAGE_OPENCLAW_HOME) || path.join(home || os.homedir(), ".openclaw");
466
+ const sessionFile = path.join(openclawHome, "agents", agentId, "sessions", `${sessionId}.jsonl`);
445
467
 
446
468
  const prevTotals = {
447
469
  totalTokens: normalizeNonNegativeInt(env.VIBEUSAGE_OPENCLAW_PREV_TOTAL_TOKENS),
448
470
  inputTokens: normalizeNonNegativeInt(env.VIBEUSAGE_OPENCLAW_PREV_INPUT_TOKENS),
449
471
  outputTokens: normalizeNonNegativeInt(env.VIBEUSAGE_OPENCLAW_PREV_OUTPUT_TOKENS),
450
472
  model: normalizeString(env.VIBEUSAGE_OPENCLAW_PREV_MODEL),
451
- updatedAt: normalizeIsoOrEpoch(env.VIBEUSAGE_OPENCLAW_PREV_UPDATED_AT)
473
+ updatedAt: normalizeIsoOrEpoch(env.VIBEUSAGE_OPENCLAW_PREV_UPDATED_AT),
452
474
  };
453
475
 
454
476
  return {
@@ -457,40 +479,55 @@ function resolveOpenclawSignal({ home, env } = {}) {
457
479
  sessionKey: normalizeString(env.VIBEUSAGE_OPENCLAW_SESSION_KEY),
458
480
  openclawHome,
459
481
  sessionFile,
460
- prevTotals
482
+ prevTotals,
461
483
  };
462
484
  }
463
485
 
464
-
465
- async function applyOpenclawTotalsFallback({ trackerDir, signal, cursors, queuePath, projectQueuePath }) {
486
+ async function applyOpenclawTotalsFallback({
487
+ trackerDir,
488
+ signal,
489
+ cursors,
490
+ queuePath,
491
+ projectQueuePath,
492
+ }) {
466
493
  const totalTokens = Number(signal?.prevTotals?.totalTokens || 0);
467
494
  if (!trackerDir || !signal || totalTokens <= 0) {
468
495
  return { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
469
496
  }
470
497
 
471
498
  const sessionKey = `${signal.agentId}:${signal.sessionId}`;
472
- const statePath = path.join(trackerDir, 'openclaw.fallback.state.json');
473
- const fallbackFilePath = path.join(trackerDir, 'openclaw.fallback.jsonl');
499
+ const statePath = path.join(trackerDir, "openclaw.fallback.state.json");
500
+ const fallbackFilePath = path.join(trackerDir, "openclaw.fallback.jsonl");
474
501
  const state = (await readJson(statePath)) || { version: 1, sessions: {} };
475
- const sessions = state.sessions && typeof state.sessions === 'object' ? state.sessions : {};
476
- const prev = sessions[sessionKey] && typeof sessions[sessionKey] === 'object' ? sessions[sessionKey] : null;
502
+ const sessions = state.sessions && typeof state.sessions === "object" ? state.sessions : {};
503
+ const prev =
504
+ sessions[sessionKey] && typeof sessions[sessionKey] === "object" ? sessions[sessionKey] : null;
477
505
 
478
506
  const current = {
479
507
  totalTokens: normalizeNonNegativeInt(signal?.prevTotals?.totalTokens) || 0,
480
508
  inputTokens: normalizeNonNegativeInt(signal?.prevTotals?.inputTokens) || 0,
481
509
  outputTokens: normalizeNonNegativeInt(signal?.prevTotals?.outputTokens) || 0,
482
- model: normalizeString(signal?.prevTotals?.model) || 'unknown',
510
+ model: normalizeString(signal?.prevTotals?.model) || "unknown",
483
511
  updatedAt: normalizeIsoOrEpoch(signal?.prevTotals?.updatedAt) || new Date().toISOString(),
484
- seenAt: new Date().toISOString()
512
+ seenAt: new Date().toISOString(),
485
513
  };
486
514
 
487
515
  let deltaTotal = current.totalTokens;
488
516
  let deltaInput = current.inputTokens;
489
517
  let deltaOutput = current.outputTokens;
490
518
  if (prev) {
491
- deltaTotal = Math.max(0, current.totalTokens - (normalizeNonNegativeInt(prev.totalTokens) || 0));
492
- deltaInput = Math.max(0, current.inputTokens - (normalizeNonNegativeInt(prev.inputTokens) || 0));
493
- deltaOutput = Math.max(0, current.outputTokens - (normalizeNonNegativeInt(prev.outputTokens) || 0));
519
+ deltaTotal = Math.max(
520
+ 0,
521
+ current.totalTokens - (normalizeNonNegativeInt(prev.totalTokens) || 0),
522
+ );
523
+ deltaInput = Math.max(
524
+ 0,
525
+ current.inputTokens - (normalizeNonNegativeInt(prev.inputTokens) || 0),
526
+ );
527
+ deltaOutput = Math.max(
528
+ 0,
529
+ current.outputTokens - (normalizeNonNegativeInt(prev.outputTokens) || 0),
530
+ );
494
531
  }
495
532
 
496
533
  if (deltaTotal > 0 && deltaInput + deltaOutput === 0) {
@@ -508,29 +545,29 @@ async function applyOpenclawTotalsFallback({ trackerDir, signal, cursors, queueP
508
545
 
509
546
  await ensureDir(path.dirname(fallbackFilePath));
510
547
  const syntheticMessage = {
511
- type: 'message',
548
+ type: "message",
512
549
  timestamp: current.updatedAt,
513
550
  message: {
514
- role: 'assistant',
551
+ role: "assistant",
515
552
  model: current.model,
516
553
  usage: {
517
554
  input: deltaInput,
518
555
  output: deltaOutput,
519
556
  cacheRead: 0,
520
557
  cacheWrite: 0,
521
- totalTokens: deltaTotal
522
- }
523
- }
558
+ totalTokens: deltaTotal,
559
+ },
560
+ },
524
561
  };
525
- await fs.appendFile(fallbackFilePath, `${JSON.stringify(syntheticMessage)}\n`, 'utf8');
562
+ await fs.appendFile(fallbackFilePath, `${JSON.stringify(syntheticMessage)}\n`, "utf8");
526
563
  await writeJson(statePath, state);
527
564
 
528
565
  return parseOpenclawIncremental({
529
- sessionFiles: [{ path: fallbackFilePath, source: 'openclaw' }],
566
+ sessionFiles: [{ path: fallbackFilePath, source: "openclaw" }],
530
567
  cursors,
531
568
  queuePath,
532
569
  projectQueuePath,
533
- source: 'openclaw'
570
+ source: "openclaw",
534
571
  });
535
572
  }
536
573
 
@@ -541,7 +578,7 @@ function normalizeNonNegativeInt(value) {
541
578
  }
542
579
 
543
580
  function normalizeIsoOrEpoch(value) {
544
- if (typeof value === 'string') {
581
+ if (typeof value === "string") {
545
582
  const trimmed = value.trim();
546
583
  if (trimmed.length > 0 && !Number.isNaN(Date.parse(trimmed))) return trimmed;
547
584
  const numeric = Number(trimmed);
@@ -569,14 +606,20 @@ async function safeStatSize(p) {
569
606
  }
570
607
  }
571
608
 
572
- async function maybeSendHeartbeat({ baseUrl, deviceToken, trackerDir, uploadResult, pendingBytes }) {
609
+ async function maybeSendHeartbeat({
610
+ baseUrl,
611
+ deviceToken,
612
+ trackerDir,
613
+ uploadResult,
614
+ pendingBytes,
615
+ }) {
573
616
  if (!deviceToken || !uploadResult) return;
574
617
  if (pendingBytes > 0) return;
575
618
  if (Number(uploadResult.inserted || 0) !== 0) return;
576
619
 
577
- const heartbeatPath = path.join(trackerDir, 'sync.heartbeat.json');
620
+ const heartbeatPath = path.join(trackerDir, "sync.heartbeat.json");
578
621
  const heartbeatState = await readJson(heartbeatPath);
579
- const lastPingAt = Date.parse(heartbeatState?.lastPingAt || '');
622
+ const lastPingAt = Date.parse(heartbeatState?.lastPingAt || "");
580
623
  const nowMs = Date.now();
581
624
  if (Number.isFinite(lastPingAt) && nowMs - lastPingAt < HEARTBEAT_MIN_INTERVAL_MS) return;
582
625
 
@@ -584,7 +627,7 @@ async function maybeSendHeartbeat({ baseUrl, deviceToken, trackerDir, uploadResu
584
627
  await syncHeartbeat({ baseUrl, deviceToken });
585
628
  await writeJson(heartbeatPath, {
586
629
  lastPingAt: new Date(nowMs).toISOString(),
587
- minIntervalMinutes: HEARTBEAT_MIN_INTERVAL_MINUTES
630
+ minIntervalMinutes: HEARTBEAT_MIN_INTERVAL_MINUTES,
588
631
  });
589
632
  } catch (_e) {
590
633
  // best-effort heartbeat; ignore failures
@@ -592,14 +635,21 @@ async function maybeSendHeartbeat({ baseUrl, deviceToken, trackerDir, uploadResu
592
635
  }
593
636
 
594
637
  function deriveAutoSkipReason({ decision, state }) {
595
- if (!decision || decision.reason !== 'throttled') return decision?.reason || 'unknown';
638
+ if (!decision || decision.reason !== "throttled") return decision?.reason || "unknown";
596
639
  const backoffUntilMs = Number(state?.backoffUntilMs || 0);
597
640
  const nextAllowedAtMs = Number(state?.nextAllowedAtMs || 0);
598
- if (backoffUntilMs > 0 && backoffUntilMs >= nextAllowedAtMs) return 'backoff';
599
- return 'throttled';
641
+ if (backoffUntilMs > 0 && backoffUntilMs >= nextAllowedAtMs) return "backoff";
642
+ return "throttled";
600
643
  }
601
644
 
602
- async function scheduleAutoRetry({ trackerDir, retryAtMs, reason, pendingBytes, source, autoRetryNoSpawn }) {
645
+ async function scheduleAutoRetry({
646
+ trackerDir,
647
+ retryAtMs,
648
+ reason,
649
+ pendingBytes,
650
+ source,
651
+ autoRetryNoSpawn,
652
+ }) {
603
653
  const retryMs = coerceRetryMs(retryAtMs);
604
654
  if (!retryMs) return { scheduled: false, retryAtMs: 0 };
605
655
 
@@ -615,10 +665,10 @@ async function scheduleAutoRetry({ trackerDir, retryAtMs, reason, pendingBytes,
615
665
  version: 1,
616
666
  retryAtMs: retryMs,
617
667
  retryAt: new Date(retryMs).toISOString(),
618
- reason: typeof reason === 'string' && reason.length > 0 ? reason : 'throttled',
668
+ reason: typeof reason === "string" && reason.length > 0 ? reason : "throttled",
619
669
  pendingBytes: Math.max(0, Number(pendingBytes || 0)),
620
670
  scheduledAt: new Date(nowMs).toISOString(),
621
- source: typeof source === 'string' ? source : 'auto'
671
+ source: typeof source === "string" ? source : "auto",
622
672
  };
623
673
 
624
674
  await writeJson(retryPath, payload);
@@ -631,9 +681,9 @@ async function scheduleAutoRetry({ trackerDir, retryAtMs, reason, pendingBytes,
631
681
 
632
682
  spawnAutoRetryProcess({
633
683
  retryPath,
634
- trackerBinPath: path.join(trackerDir, 'app', 'bin', 'tracker.js'),
635
- fallbackPkg: 'vibeusage',
636
- delayMs
684
+ trackerBinPath: path.join(trackerDir, "app", "bin", "tracker.js"),
685
+ fallbackPkg: "vibeusage",
686
+ delayMs,
637
687
  });
638
688
  return { scheduled: true, retryAtMs: retryMs };
639
689
  }
@@ -646,17 +696,18 @@ async function clearAutoRetry(trackerDir) {
646
696
  function spawnAutoRetryProcess({ retryPath, trackerBinPath, fallbackPkg, delayMs }) {
647
697
  const script = buildAutoRetryScript({ retryPath, trackerBinPath, fallbackPkg, delayMs });
648
698
  try {
649
- const child = cp.spawn(process.execPath, ['-e', script], {
699
+ const child = cp.spawn(process.execPath, ["-e", script], {
650
700
  detached: true,
651
- stdio: 'ignore',
652
- env: process.env
701
+ stdio: "ignore",
702
+ env: process.env,
653
703
  });
654
704
  child.unref();
655
705
  } catch (_e) {}
656
706
  }
657
707
 
658
708
  function buildAutoRetryScript({ retryPath, trackerBinPath, fallbackPkg, delayMs }) {
659
- return `'use strict';\n` +
709
+ return (
710
+ `'use strict';\n` +
660
711
  `const fs = require('node:fs');\n` +
661
712
  `const cp = require('node:child_process');\n` +
662
713
  `const retryPath = ${JSON.stringify(retryPath)};\n` +
@@ -678,7 +729,8 @@ function buildAutoRetryScript({ retryPath, trackerBinPath, fallbackPkg, delayMs
678
729
  ` const child = cp.spawn(cmd[0], cmd.slice(1), { detached: true, stdio: 'ignore', env: process.env });\n` +
679
730
  ` child.unref();\n` +
680
731
  ` } catch (_) {}\n` +
681
- `}, delayMs);\n`;
732
+ `}, delayMs);\n`
733
+ );
682
734
  }
683
735
 
684
736
  function coerceRetryMs(v) {
@@ -688,9 +740,9 @@ function coerceRetryMs(v) {
688
740
  }
689
741
 
690
742
  async function writeOpenclawSignal(trackerDir) {
691
- const openclawSignalPath = path.join(trackerDir, 'openclaw.signal');
743
+ const openclawSignalPath = path.join(trackerDir, "openclaw.signal");
692
744
  try {
693
- await fs.writeFile(openclawSignalPath, new Date().toISOString(), 'utf8');
745
+ await fs.writeFile(openclawSignalPath, new Date().toISOString(), "utf8");
694
746
  } catch (_e) {
695
747
  // best-effort marker
696
748
  }
@@ -698,5 +750,5 @@ async function writeOpenclawSignal(trackerDir) {
698
750
 
699
751
  const HEARTBEAT_MIN_INTERVAL_MINUTES = 30;
700
752
  const HEARTBEAT_MIN_INTERVAL_MS = HEARTBEAT_MIN_INTERVAL_MINUTES * 60 * 1000;
701
- const AUTO_RETRY_FILENAME = 'auto.retry.json';
753
+ const AUTO_RETRY_FILENAME = "auto.retry.json";
702
754
  const AUTO_RETRY_MAX_DELAY_MS = 2 * 60 * 60 * 1000;