vibeusage 0.3.4 → 0.4.0

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.
@@ -63,6 +63,25 @@ async function listGeminiSessionFiles(tmpDir) {
63
63
  return out;
64
64
  }
65
65
 
66
+ async function listKimiSessionFiles(sessionsDir) {
67
+ const out = [];
68
+ const projects = await safeReadDir(sessionsDir);
69
+ for (const project of projects) {
70
+ if (!project.isDirectory()) continue;
71
+ const projectDir = path.join(sessionsDir, project.name);
72
+ const sessions = await safeReadDir(projectDir);
73
+ for (const session of sessions) {
74
+ if (!session.isDirectory()) continue;
75
+ const wirePath = path.join(projectDir, session.name, "wire.jsonl");
76
+ const st = await fs.stat(wirePath).catch(() => null);
77
+ if (!st || !st.isFile()) continue;
78
+ out.push(wirePath);
79
+ }
80
+ }
81
+ out.sort((a, b) => a.localeCompare(b));
82
+ return out;
83
+ }
84
+
66
85
  async function listOpencodeMessageFiles(storageDir) {
67
86
  const out = [];
68
87
  const messageDir = path.join(storageDir, "message");
@@ -397,23 +416,21 @@ async function parseGeminiIncremental({
397
416
  return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
398
417
  }
399
418
 
400
- async function parseOpencodeIncremental({
401
- messageFiles,
402
- opencodeDbPath,
419
+ async function parseKimiIncremental({
420
+ sessionFiles,
403
421
  cursors,
404
422
  queuePath,
405
423
  projectQueuePath,
406
424
  onProgress,
407
425
  source,
408
426
  publicRepoResolver,
409
- readSqliteRows,
410
427
  }) {
411
428
  await ensureDir(path.dirname(queuePath));
412
429
  let filesProcessed = 0;
413
430
  let eventsAggregated = 0;
414
431
 
415
432
  const cb = typeof onProgress === "function" ? onProgress : null;
416
- const files = Array.isArray(messageFiles) ? messageFiles : [];
433
+ const files = Array.isArray(sessionFiles) ? sessionFiles : [];
417
434
  const totalFiles = files.length;
418
435
  const hourlyState = normalizeHourlyState(cursors?.hourly);
419
436
  const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
@@ -421,14 +438,8 @@ async function parseOpencodeIncremental({
421
438
  const projectTouchedBuckets = projectEnabled ? new Set() : null;
422
439
  const projectMetaCache = projectEnabled ? new Map() : null;
423
440
  const publicRepoCache = projectEnabled ? new Map() : null;
424
- const opencodeState = normalizeOpencodeState(cursors?.opencode);
425
- const opencodeSqliteState = normalizeOpencodeSqliteState(cursors?.opencodeSqlite);
426
- const messageIndex = opencodeState.messages;
427
441
  const touchedBuckets = new Set();
428
- const defaultSource = normalizeSourceInput(source) || "opencode";
429
- let sqliteStatus = opencodeSqliteState.lastStatus || "never_checked";
430
- let sqliteCheckedAt = opencodeSqliteState.lastCheckedAt || null;
431
- let sqliteErrorCode = opencodeSqliteState.lastErrorCode || null;
442
+ const defaultSource = normalizeSourceInput(source) || "kimi";
432
443
 
433
444
  if (!cursors.files || typeof cursors.files !== "object") {
434
445
  cursors.files = {};
@@ -448,30 +459,8 @@ async function parseOpencodeIncremental({
448
459
  const key = filePath;
449
460
  const prev = cursors.files[key] || null;
450
461
  const inode = st.ino || 0;
451
- const size = Number.isFinite(st.size) ? st.size : 0;
452
- const mtimeMs = Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0;
453
- const unchanged =
454
- prev && prev.inode === inode && prev.size === size && prev.mtimeMs === mtimeMs;
455
- if (unchanged) {
456
- filesProcessed += 1;
457
- if (cb) {
458
- cb({
459
- index: idx + 1,
460
- total: totalFiles,
461
- filePath,
462
- filesProcessed,
463
- eventsAggregated,
464
- bucketsQueued: touchedBuckets.size,
465
- });
466
- }
467
- continue;
468
- }
462
+ const startOffset = prev && prev.inode === inode ? prev.offset || 0 : 0;
469
463
 
470
- const fallbackTotals = prev && typeof prev.lastTotals === "object" ? prev.lastTotals : null;
471
- const fallbackMessageKey =
472
- prev && typeof prev.messageKey === "string" && prev.messageKey.trim()
473
- ? prev.messageKey.trim()
474
- : null;
475
464
  const projectContext = projectEnabled
476
465
  ? await resolveProjectContextForFile({
477
466
  filePath,
@@ -484,11 +473,9 @@ async function parseOpencodeIncremental({
484
473
  const projectRef = projectContext?.projectRef || null;
485
474
  const projectKey = projectContext?.projectKey || null;
486
475
 
487
- const result = await parseOpencodeMessageFile({
476
+ const result = await parseKimiFile({
488
477
  filePath,
489
- messageIndex,
490
- fallbackTotals,
491
- fallbackMessageKey,
478
+ startOffset,
492
479
  hourlyState,
493
480
  touchedBuckets,
494
481
  source: fileSource,
@@ -500,23 +487,13 @@ async function parseOpencodeIncremental({
500
487
 
501
488
  cursors.files[key] = {
502
489
  inode,
503
- size,
504
- mtimeMs,
505
- lastTotals: result.lastTotals,
506
- messageKey: result.messageKey || null,
490
+ offset: result.endOffset,
507
491
  updatedAt: new Date().toISOString(),
508
492
  };
509
493
 
510
494
  filesProcessed += 1;
511
495
  eventsAggregated += result.eventsAggregated;
512
496
 
513
- if (result.messageKey && result.shouldUpdate) {
514
- messageIndex[result.messageKey] = {
515
- lastTotals: result.lastTotals,
516
- updatedAt: new Date().toISOString(),
517
- };
518
- }
519
-
520
497
  if (cb) {
521
498
  cb({
522
499
  index: idx + 1,
@@ -529,6 +506,54 @@ async function parseOpencodeIncremental({
529
506
  }
530
507
  }
531
508
 
509
+ const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
510
+ const projectBucketsQueued = projectEnabled
511
+ ? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
512
+ : 0;
513
+ hourlyState.updatedAt = new Date().toISOString();
514
+ cursors.hourly = hourlyState;
515
+ if (projectState) {
516
+ projectState.updatedAt = new Date().toISOString();
517
+ cursors.projectHourly = projectState;
518
+ }
519
+
520
+ return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
521
+ }
522
+
523
+ async function parseOpencodeIncremental({
524
+ opencodeDbPath,
525
+ cursors,
526
+ queuePath,
527
+ projectQueuePath,
528
+ onProgress,
529
+ source = "opencode",
530
+ publicRepoResolver = null,
531
+ readSqliteRows,
532
+ }) {
533
+ await ensureDir(path.dirname(queuePath));
534
+ let filesProcessed = 0;
535
+ let eventsAggregated = 0;
536
+
537
+ const cb = typeof onProgress === "function" ? onProgress : null;
538
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
539
+ const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
540
+ const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
541
+ const projectTouchedBuckets = projectEnabled ? new Set() : null;
542
+ const projectMetaCache = projectEnabled ? new Map() : null;
543
+ const publicRepoCache = projectEnabled ? new Map() : null;
544
+ const opencodeState = normalizeOpencodeState(cursors?.opencode);
545
+ const opencodeSqliteState = normalizeOpencodeSqliteState(cursors?.opencodeSqlite);
546
+ const messageIndex = opencodeState.messages;
547
+ const touchedBuckets = new Set();
548
+ const defaultSource = normalizeSourceInput(source) || "opencode";
549
+ let sqliteStatus = opencodeSqliteState.lastStatus || "never_checked";
550
+ let sqliteCheckedAt = opencodeSqliteState.lastCheckedAt || null;
551
+ let sqliteErrorCode = opencodeSqliteState.lastErrorCode || null;
552
+
553
+ if (!cursors.files || typeof cursors.files !== "object") {
554
+ cursors.files = {};
555
+ }
556
+
532
557
  if (typeof opencodeDbPath === "string" && opencodeDbPath.length > 0) {
533
558
  const readRows =
534
559
  typeof readSqliteRows === "function" ? readSqliteRows : readOpencodeSqliteRows;
@@ -818,6 +843,70 @@ async function parseClaudeFile({
818
843
  return { endOffset, eventsAggregated };
819
844
  }
820
845
 
846
+ async function parseKimiFile({
847
+ filePath,
848
+ startOffset,
849
+ hourlyState,
850
+ touchedBuckets,
851
+ source,
852
+ projectState,
853
+ projectTouchedBuckets,
854
+ projectRef,
855
+ projectKey,
856
+ }) {
857
+ const st = await fs.stat(filePath).catch(() => null);
858
+ if (!st || !st.isFile()) return { endOffset: startOffset, eventsAggregated: 0 };
859
+
860
+ const endOffset = st.size;
861
+ if (startOffset >= endOffset) return { endOffset, eventsAggregated: 0 };
862
+
863
+ const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
864
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
865
+
866
+ let eventsAggregated = 0;
867
+ for await (const line of rl) {
868
+ if (!line || !line.includes("StatusUpdate")) continue;
869
+ let obj;
870
+ try {
871
+ obj = JSON.parse(line);
872
+ } catch (_e) {
873
+ continue;
874
+ }
875
+ if (!obj || !obj.message || obj.message.type !== "StatusUpdate") continue;
876
+
877
+ const payload = obj.message.payload;
878
+ const delta = normalizeKimiUsage(payload?.token_usage);
879
+ if (!delta || isAllZeroUsage(delta)) continue;
880
+
881
+ const tsIso = kimiTimestampToIso(obj.timestamp);
882
+ if (!tsIso) continue;
883
+
884
+ const bucketStart = toUtcHalfHourStart(tsIso);
885
+ if (!bucketStart) continue;
886
+
887
+ const model = normalizeModelInput(payload?.model) || DEFAULT_MODEL;
888
+ const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
889
+ addTotals(bucket.totals, delta);
890
+ touchedBuckets.add(bucketKey(source, model, bucketStart));
891
+ if (projectKey && projectState && projectTouchedBuckets) {
892
+ const projectBucket = getProjectBucket(
893
+ projectState,
894
+ projectKey,
895
+ source,
896
+ bucketStart,
897
+ projectRef,
898
+ );
899
+ addTotals(projectBucket.totals, delta);
900
+ projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
901
+ }
902
+ eventsAggregated += 1;
903
+ }
904
+
905
+ rl.close();
906
+ stream.close?.();
907
+ return { endOffset, eventsAggregated };
908
+ }
909
+
821
910
  async function parseGeminiFile({
822
911
  filePath,
823
912
  startIndex,
@@ -2058,6 +2147,32 @@ function normalizeGeminiTokens(tokens) {
2058
2147
  };
2059
2148
  }
2060
2149
 
2150
+ function normalizeKimiUsage(usage) {
2151
+ if (!usage || typeof usage !== "object") return null;
2152
+ const inputOther = toNonNegativeInt(usage.input_other);
2153
+ const cacheCreation = toNonNegativeInt(usage.input_cache_creation);
2154
+ const cacheRead = toNonNegativeInt(usage.input_cache_read);
2155
+ const output = toNonNegativeInt(usage.output);
2156
+ const inputTokens = inputOther + cacheCreation;
2157
+ const total = inputTokens + cacheRead + output;
2158
+ return {
2159
+ input_tokens: inputTokens,
2160
+ cached_input_tokens: cacheRead,
2161
+ output_tokens: output,
2162
+ reasoning_output_tokens: 0,
2163
+ total_tokens: total,
2164
+ };
2165
+ }
2166
+
2167
+ function kimiTimestampToIso(value) {
2168
+ const n = Number(value);
2169
+ if (!Number.isFinite(n) || n <= 0) return null;
2170
+ const ms = n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);
2171
+ const date = new Date(ms);
2172
+ const iso = date.toISOString();
2173
+ return iso;
2174
+ }
2175
+
2061
2176
  function normalizeOpencodeTokens(tokens) {
2062
2177
  if (!tokens || typeof tokens !== "object") return null;
2063
2178
  const input = toNonNegativeInt(tokens.input);
@@ -2293,10 +2408,12 @@ module.exports = {
2293
2408
  listRolloutFiles,
2294
2409
  listClaudeProjectFiles,
2295
2410
  listGeminiSessionFiles,
2411
+ listKimiSessionFiles,
2296
2412
  listOpencodeMessageFiles,
2297
2413
  parseRolloutIncremental,
2298
2414
  parseClaudeIncremental,
2299
2415
  parseGeminiIncremental,
2416
+ parseKimiIncremental,
2300
2417
  parseOpencodeIncremental,
2301
2418
  normalizeHourlyState,
2302
2419
  getHourlyBucket,
@@ -0,0 +1,75 @@
1
+ # VIBEUSAGE_HERMES_PLUGIN
2
+ # Generated by VibeUsage. Do not edit manually.
3
+ import json
4
+ import os
5
+ from datetime import datetime, timezone
6
+
7
+ LEDGER_PATH = "__LEDGER_PATH__"
8
+ SOURCE = "hermes"
9
+ VERSION = 1
10
+
11
+
12
+ def _iso_now():
13
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
14
+
15
+
16
+ def _safe_int(value):
17
+ try:
18
+ return max(0, int(value or 0))
19
+ except Exception:
20
+ return 0
21
+
22
+
23
+ def _append_record(record):
24
+ try:
25
+ os.makedirs(os.path.dirname(LEDGER_PATH), exist_ok=True)
26
+ with open(LEDGER_PATH, "a", encoding="utf-8") as handle:
27
+ handle.write(json.dumps(record, ensure_ascii=False) + "\n")
28
+ except Exception:
29
+ return None
30
+ return None
31
+
32
+
33
+ def _base_record(record_type, session_id="", platform="", model="", provider=""):
34
+ return {
35
+ "version": VERSION,
36
+ "type": str(record_type or ""),
37
+ "source": SOURCE,
38
+ "session_id": str(session_id or ""),
39
+ "platform": str(platform or ""),
40
+ "model": str(model or ""),
41
+ "provider": str(provider or ""),
42
+ "emitted_at": _iso_now(),
43
+ }
44
+
45
+
46
+ def on_session_start(session_id="", model="", platform="", **_kwargs):
47
+ return _append_record(_base_record("session_start", session_id=session_id, platform=platform, model=model))
48
+
49
+
50
+ def post_api_request(session_id="", platform="", model="", provider="", api_mode="", api_call_count=0, finish_reason="", usage=None, **_kwargs):
51
+ if not isinstance(usage, dict):
52
+ return None
53
+ record = _base_record("usage", session_id=session_id, platform=platform, model=model, provider=provider)
54
+ record.update({
55
+ "api_mode": str(api_mode or ""),
56
+ "api_call_count": _safe_int(api_call_count),
57
+ "input_tokens": _safe_int(usage.get("input_tokens")),
58
+ "output_tokens": _safe_int(usage.get("output_tokens")),
59
+ "cache_read_tokens": _safe_int(usage.get("cache_read_tokens")),
60
+ "cache_write_tokens": _safe_int(usage.get("cache_write_tokens")),
61
+ "reasoning_tokens": _safe_int(usage.get("reasoning_tokens")),
62
+ "total_tokens": _safe_int(usage.get("total_tokens")),
63
+ "finish_reason": str(finish_reason or ""),
64
+ })
65
+ return _append_record(record)
66
+
67
+
68
+ def on_session_end(session_id="", model="", platform="", **_kwargs):
69
+ return _append_record(_base_record("session_end", session_id=session_id, platform=platform, model=model))
70
+
71
+
72
+ def register(ctx):
73
+ ctx.register_hook("on_session_start", on_session_start)
74
+ ctx.register_hook("post_api_request", post_api_request)
75
+ ctx.register_hook("on_session_end", on_session_end)
@@ -0,0 +1,9 @@
1
+ # VIBEUSAGE_HERMES_PLUGIN
2
+ name: vibeusage
3
+ version: "1"
4
+ description: "VibeUsage Hermes usage ledger plugin"
5
+ author: "VibeUsage"
6
+ provides_hooks:
7
+ - "on_session_start"
8
+ - "post_api_request"
9
+ - "on_session_end"