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.
- package/README.md +22 -10
- package/README.zh-CN.md +1 -1
- package/package.json +9 -1
- package/src/commands/init.js +2 -2
- package/src/commands/status.js +16 -0
- package/src/commands/sync.js +113 -18
- package/src/commands/uninstall.js +10 -0
- package/src/lib/diagnostics.js +28 -0
- package/src/lib/hermes-config.js +172 -0
- package/src/lib/hermes-usage-ledger.js +123 -0
- package/src/lib/integrations/context.js +18 -0
- package/src/lib/integrations/hermes.js +96 -0
- package/src/lib/integrations/index.js +4 -0
- package/src/lib/integrations/kimi.js +105 -0
- package/src/lib/kimi-config.js +221 -0
- package/src/lib/opencode-usage-audit.js +2 -3
- package/src/lib/rollout.js +167 -50
- package/src/templates/hermes-vibeusage-plugin/__init__.py +75 -0
- package/src/templates/hermes-vibeusage-plugin/plugin.yaml +9 -0
package/src/lib/rollout.js
CHANGED
|
@@ -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
|
|
401
|
-
|
|
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(
|
|
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) || "
|
|
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
|
|
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
|
|
476
|
+
const result = await parseKimiFile({
|
|
488
477
|
filePath,
|
|
489
|
-
|
|
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
|
-
|
|
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)
|