vibeusage 0.2.19 → 0.2.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeusage",
3
- "version": "0.2.19",
3
+ "version": "0.2.20",
4
4
  "description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -123,6 +123,17 @@ async function cmdSync(argv) {
123
123
  });
124
124
  }
125
125
 
126
+ const openclawFallback = await applyOpenclawTotalsFallback({
127
+ trackerDir,
128
+ signal: openclawSignal,
129
+ cursors,
130
+ queuePath,
131
+ projectQueuePath
132
+ });
133
+ openclawResult.filesProcessed += openclawFallback.filesProcessed;
134
+ openclawResult.eventsAggregated += openclawFallback.eventsAggregated;
135
+ openclawResult.bucketsQueued += openclawFallback.bucketsQueued;
136
+
126
137
  const claudeFiles = await listClaudeProjectFiles(claudeProjectsDir);
127
138
  let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
128
139
  if (claudeFiles.length > 0) {
@@ -432,10 +443,123 @@ function resolveOpenclawSignal({ home, env } = {}) {
432
443
  const openclawHome = normalizeString(env.VIBEUSAGE_OPENCLAW_HOME) || path.join(home || os.homedir(), '.openclaw');
433
444
  const sessionFile = path.join(openclawHome, 'agents', agentId, 'sessions', `${sessionId}.jsonl`);
434
445
 
435
- return { agentId, sessionId, openclawHome, sessionFile };
446
+ const prevTotals = {
447
+ totalTokens: normalizeNonNegativeInt(env.VIBEUSAGE_OPENCLAW_PREV_TOTAL_TOKENS),
448
+ inputTokens: normalizeNonNegativeInt(env.VIBEUSAGE_OPENCLAW_PREV_INPUT_TOKENS),
449
+ outputTokens: normalizeNonNegativeInt(env.VIBEUSAGE_OPENCLAW_PREV_OUTPUT_TOKENS),
450
+ model: normalizeString(env.VIBEUSAGE_OPENCLAW_PREV_MODEL),
451
+ updatedAt: normalizeIsoOrEpoch(env.VIBEUSAGE_OPENCLAW_PREV_UPDATED_AT)
452
+ };
453
+
454
+ return {
455
+ agentId,
456
+ sessionId,
457
+ sessionKey: normalizeString(env.VIBEUSAGE_OPENCLAW_SESSION_KEY),
458
+ openclawHome,
459
+ sessionFile,
460
+ prevTotals
461
+ };
436
462
  }
437
463
 
438
464
 
465
+ async function applyOpenclawTotalsFallback({ trackerDir, signal, cursors, queuePath, projectQueuePath }) {
466
+ const totalTokens = Number(signal?.prevTotals?.totalTokens || 0);
467
+ if (!trackerDir || !signal || totalTokens <= 0) {
468
+ return { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
469
+ }
470
+
471
+ 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');
474
+ 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;
477
+
478
+ const current = {
479
+ totalTokens: normalizeNonNegativeInt(signal?.prevTotals?.totalTokens) || 0,
480
+ inputTokens: normalizeNonNegativeInt(signal?.prevTotals?.inputTokens) || 0,
481
+ outputTokens: normalizeNonNegativeInt(signal?.prevTotals?.outputTokens) || 0,
482
+ model: normalizeString(signal?.prevTotals?.model) || 'unknown',
483
+ updatedAt: normalizeIsoOrEpoch(signal?.prevTotals?.updatedAt) || new Date().toISOString(),
484
+ seenAt: new Date().toISOString()
485
+ };
486
+
487
+ let deltaTotal = current.totalTokens;
488
+ let deltaInput = current.inputTokens;
489
+ let deltaOutput = current.outputTokens;
490
+ 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));
494
+ }
495
+
496
+ if (deltaTotal > 0 && deltaInput + deltaOutput === 0) {
497
+ deltaInput = deltaTotal;
498
+ }
499
+
500
+ sessions[sessionKey] = current;
501
+ state.version = 1;
502
+ state.sessions = sessions;
503
+
504
+ if (deltaTotal <= 0) {
505
+ await writeJson(statePath, state);
506
+ return { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
507
+ }
508
+
509
+ await ensureDir(path.dirname(fallbackFilePath));
510
+ const syntheticMessage = {
511
+ type: 'message',
512
+ timestamp: current.updatedAt,
513
+ message: {
514
+ role: 'assistant',
515
+ model: current.model,
516
+ usage: {
517
+ input: deltaInput,
518
+ output: deltaOutput,
519
+ cacheRead: 0,
520
+ cacheWrite: 0,
521
+ totalTokens: deltaTotal
522
+ }
523
+ }
524
+ };
525
+ await fs.appendFile(fallbackFilePath, `${JSON.stringify(syntheticMessage)}\n`, 'utf8');
526
+ await writeJson(statePath, state);
527
+
528
+ return parseOpenclawIncremental({
529
+ sessionFiles: [{ path: fallbackFilePath, source: 'openclaw' }],
530
+ cursors,
531
+ queuePath,
532
+ projectQueuePath,
533
+ source: 'openclaw'
534
+ });
535
+ }
536
+
537
+ function normalizeNonNegativeInt(value) {
538
+ const n = Number(value);
539
+ if (!Number.isFinite(n) || n < 0) return null;
540
+ return Math.floor(n);
541
+ }
542
+
543
+ function normalizeIsoOrEpoch(value) {
544
+ if (typeof value === 'string') {
545
+ const trimmed = value.trim();
546
+ if (trimmed.length > 0 && !Number.isNaN(Date.parse(trimmed))) return trimmed;
547
+ const numeric = Number(trimmed);
548
+ if (Number.isFinite(numeric) && numeric > 0) {
549
+ const ms = numeric < 1e12 ? Math.floor(numeric * 1000) : Math.floor(numeric);
550
+ const iso = new Date(ms).toISOString();
551
+ if (!Number.isNaN(Date.parse(iso))) return iso;
552
+ }
553
+ }
554
+
555
+ const n = Number(value);
556
+ if (!Number.isFinite(n) || n <= 0) return null;
557
+ const ms = n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);
558
+ const dt = new Date(ms);
559
+ if (Number.isNaN(dt.getTime())) return null;
560
+ return dt.toISOString();
561
+ }
562
+
439
563
  async function safeStatSize(p) {
440
564
  try {
441
565
  const st = await fs.stat(p);
@@ -252,12 +252,12 @@ function buildHookMarkdown() {
252
252
  name: ${OPENCLAW_HOOK_NAME}
253
253
  description: "Trigger vibeusage sync when OpenClaw sessions roll over"
254
254
  metadata:
255
- { "openclaw": { "emoji": "📈", "events": ["command:new", "command:stop"], "requires": { "bins": ["node"] } } }
255
+ { "openclaw": { "emoji": "📈", "events": ["command:new", "command:reset", "command:stop"], "requires": { "bins": ["node"] } } }
256
256
  ---
257
257
 
258
258
  # VibeUsage OpenClaw Sync Hook
259
259
 
260
- Triggers non-blocking 'vibeusage sync --auto --from-openclaw' runs when OpenClaw command events indicate a session rollover/stop.
260
+ Triggers non-blocking 'vibeusage sync --auto --from-openclaw' runs when OpenClaw command events indicate session rollover/reset/stop.
261
261
  `;
262
262
  }
263
263
 
@@ -281,13 +281,14 @@ function buildHookHandler({ trackerDir, packageName = 'vibeusage', openclawHome
281
281
  `module.exports = async function handler(event) {\n` +
282
282
  ` try {\n` +
283
283
  ` if (!event || event.type !== 'command') return;\n` +
284
- ` if (event.action !== 'new' && event.action !== 'stop') return;\n` +
284
+ ` if (event.action !== 'new' && event.action !== 'reset' && event.action !== 'stop') return;\n` +
285
285
  `\n` +
286
286
  ` const sessionKey = normalize(event.sessionKey);\n` +
287
287
  ` const agentId = parseAgentId(sessionKey);\n` +
288
288
  ` if (!agentId) return;\n` +
289
289
  `\n` +
290
- ` const sessionId = resolveSessionId(event);\n` +
290
+ ` const sessionEntry = resolveSessionEntry(event);\n` +
291
+ ` const sessionId = normalize(sessionEntry && sessionEntry.sessionId) || resolveSessionId(event);\n` +
291
292
  ` if (!sessionId) return;\n` +
292
293
  `\n` +
293
294
  ` const now = Date.now();\n` +
@@ -302,9 +303,20 @@ function buildHookHandler({ trackerDir, packageName = 'vibeusage', openclawHome
302
303
  ` const env = {\n` +
303
304
  ` ...process.env,\n` +
304
305
  ` VIBEUSAGE_OPENCLAW_AGENT_ID: agentId,\n` +
306
+ ` VIBEUSAGE_OPENCLAW_SESSION_KEY: sessionKey,\n` +
305
307
  ` VIBEUSAGE_OPENCLAW_PREV_SESSION_ID: sessionId,\n` +
306
308
  ` VIBEUSAGE_OPENCLAW_HOME: openclawHome\n` +
307
309
  ` };\n` +
310
+ ` const prevTotalTokens = toNonNegativeInt(sessionEntry && sessionEntry.totalTokens);\n` +
311
+ ` const prevInputTokens = toNonNegativeInt(sessionEntry && sessionEntry.inputTokens);\n` +
312
+ ` const prevOutputTokens = toNonNegativeInt(sessionEntry && sessionEntry.outputTokens);\n` +
313
+ ` const prevModel = normalize(sessionEntry && sessionEntry.model);\n` +
314
+ ` const prevUpdatedAt = toIso(sessionEntry && sessionEntry.updatedAt);\n` +
315
+ ` if (prevTotalTokens != null) env.VIBEUSAGE_OPENCLAW_PREV_TOTAL_TOKENS = String(prevTotalTokens);\n` +
316
+ ` if (prevInputTokens != null) env.VIBEUSAGE_OPENCLAW_PREV_INPUT_TOKENS = String(prevInputTokens);\n` +
317
+ ` if (prevOutputTokens != null) env.VIBEUSAGE_OPENCLAW_PREV_OUTPUT_TOKENS = String(prevOutputTokens);\n` +
318
+ ` if (prevModel) env.VIBEUSAGE_OPENCLAW_PREV_MODEL = prevModel;\n` +
319
+ ` if (prevUpdatedAt) env.VIBEUSAGE_OPENCLAW_PREV_UPDATED_AT = prevUpdatedAt;\n` +
308
320
  `\n` +
309
321
  ` const hasLocalRuntime = fs.existsSync(trackerBinPath);\n` +
310
322
  ` const hasLocalDeps = fs.existsSync(depsMarkerPath);\n` +
@@ -323,6 +335,32 @@ function buildHookHandler({ trackerDir, packageName = 'vibeusage', openclawHome
323
335
  ` return s.length > 0 ? s : null;\n` +
324
336
  `}\n` +
325
337
  `\n` +
338
+ `function resolveSessionEntry(event) {\n` +
339
+ ` const ctx = (event && event.context && typeof event.context === 'object') ? event.context : {};\n` +
340
+ ` if (event && event.action === 'stop') return (ctx.sessionEntry && typeof ctx.sessionEntry === 'object') ? ctx.sessionEntry : null;\n` +
341
+ ` if (ctx.previousSessionEntry && typeof ctx.previousSessionEntry === 'object') return ctx.previousSessionEntry;\n` +
342
+ ` if (ctx.sessionEntry && typeof ctx.sessionEntry === 'object') return ctx.sessionEntry;\n` +
343
+ ` return null;\n` +
344
+ `}\n` +
345
+ `\n` +
346
+ `function toNonNegativeInt(v) {\n` +
347
+ ` const n = Number(v);\n` +
348
+ ` if (!Number.isFinite(n) || n < 0) return null;\n` +
349
+ ` return Math.floor(n);\n` +
350
+ `}\n` +
351
+ `\n` +
352
+ `function toIso(v) {\n` +
353
+ ` if (typeof v === 'string') {\n` +
354
+ ` const s = normalize(v);\n` +
355
+ ` if (s && !Number.isNaN(Date.parse(s))) return s;\n` +
356
+ ` }\n` +
357
+ ` const n = Number(v);\n` +
358
+ ` if (!Number.isFinite(n) || n <= 0) return null;\n` +
359
+ ` const ms = n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);\n` +
360
+ ` const d = new Date(ms);\n` +
361
+ ` return Number.isNaN(d.getTime()) ? null : d.toISOString();\n` +
362
+ `}\n` +
363
+ `\n` +
326
364
  `function parseAgentId(sessionKey) {\n` +
327
365
  ` const s = normalize(sessionKey);\n` +
328
366
  ` if (!s || !s.startsWith('agent:')) return null;\n` +