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/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# VibeUsage
|
|
6
6
|
|
|
7
7
|
**Track token usage across AI coding CLIs.**
|
|
8
|
-
Local parsing, minimal data collection, and a shareable dashboard for Codex CLI, Claude Code, Gemini CLI, OpenCode, OpenClaw, and more.
|
|
8
|
+
Local parsing, minimal data collection, and a shareable dashboard for Codex CLI, Claude Code, Gemini CLI, OpenCode, Hermes, OpenClaw, and more.
|
|
9
9
|
|
|
10
10
|
[](https://www.npmjs.com/package/vibeusage)
|
|
11
11
|
[](LICENSE)
|
|
@@ -32,7 +32,7 @@ Give the install guide to ChatGPT, Claude, Codex, or your preferred agent — it
|
|
|
32
32
|
|
|
33
33
|
VibeUsage is a **token usage tracker for AI agent CLIs**. It installs lightweight local hooks/plugins, reads usage from local logs or local databases, aggregates usage into time buckets on your machine, and syncs only the data needed to power a dashboard, cost breakdowns, project usage views, public profiles, and leaderboards.
|
|
34
34
|
|
|
35
|
-
It is currently **macOS-first**, with support focused on real developer workflows around **Codex CLI, Every Code, Claude Code, Gemini CLI, OpenCode, and OpenClaw**.
|
|
35
|
+
It is currently **macOS-first**, with support focused on real developer workflows around **Codex CLI, Every Code, Claude Code, Gemini CLI, OpenCode, Hermes, and OpenClaw**.
|
|
36
36
|
|
|
37
37
|
## Why VibeUsage
|
|
38
38
|
|
|
@@ -86,9 +86,20 @@ This is useful when you want to copy an install command from the dashboard or le
|
|
|
86
86
|
| **Every Code** | Auto-detected | `notify` hook | `~/.code/sessions/**/rollout-*.jsonl` |
|
|
87
87
|
| **Claude Code** | Auto-detected | `Stop` + `SessionEnd` hooks | local hook output |
|
|
88
88
|
| **Gemini CLI** | Auto-detected | `SessionEnd` hook | `~/.gemini/tmp/**/chats/session-*.json` |
|
|
89
|
-
| **OpenCode** | Auto-detected | plugin + local parsing | `~/.local/share/opencode/opencode.db` (
|
|
89
|
+
| **OpenCode** | Auto-detected | plugin + local parsing | `~/.local/share/opencode/opencode.db` (SQLite is the only supported local accounting source) |
|
|
90
|
+
| **Hermes** | Auto-detected when installed | plugin + local parsing | `~/.vibeusage/tracker/hermes.usage.jsonl` |
|
|
90
91
|
| **OpenClaw** | Auto-detected when installed | session plugin | local sanitized usage ledger |
|
|
91
92
|
|
|
93
|
+
### Hermes note
|
|
94
|
+
|
|
95
|
+
Hermes uses a dedicated plugin-ledger path:
|
|
96
|
+
|
|
97
|
+
**`vibeusage init` installs Hermes plugin → Hermes lifecycle hooks append `~/.vibeusage/tracker/hermes.usage.jsonl` → `vibeusage sync` parses only that ledger**
|
|
98
|
+
|
|
99
|
+
- no prompt / response content upload
|
|
100
|
+
- no fallback parsing of `~/.hermes/state.db`, `~/.hermes/sessions/`, or trajectory files
|
|
101
|
+
- plugin hooks collect locally only; upload still happens in `vibeusage sync`
|
|
102
|
+
|
|
92
103
|
### OpenClaw note
|
|
93
104
|
|
|
94
105
|
OpenClaw uses a dedicated privacy-preserving path:
|
|
@@ -140,12 +151,13 @@ graph LR
|
|
|
140
151
|
C[Claude Code] --> G
|
|
141
152
|
D[Gemini CLI] --> G
|
|
142
153
|
E[OpenCode] --> G
|
|
143
|
-
F[
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
I --> J[
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
F[Hermes] --> G
|
|
155
|
+
H[OpenClaw] --> G
|
|
156
|
+
G --> I[Local aggregation into 30-min UTC buckets]
|
|
157
|
+
I --> J[VibeUsage backend]
|
|
158
|
+
J --> K[Dashboard]
|
|
159
|
+
J --> L[Project usage]
|
|
160
|
+
J --> M[Public profile / leaderboard]
|
|
149
161
|
```
|
|
150
162
|
|
|
151
163
|
At a high level:
|
|
@@ -261,7 +273,7 @@ npx vibeusage status
|
|
|
261
273
|
npx vibeusage doctor
|
|
262
274
|
```
|
|
263
275
|
|
|
264
|
-
|
|
276
|
+
VibeUsage reads OpenCode usage only from `opencode.db`, so the most common issues are missing `sqlite3` on `PATH`, a missing database file, or a local SQLite query failure.
|
|
265
277
|
|
|
266
278
|
### My OpenClaw usage is not showing up. What should I check?
|
|
267
279
|
|
package/README.zh-CN.md
CHANGED
|
@@ -86,7 +86,7 @@ npx --yes vibeusage init --link-code <code>
|
|
|
86
86
|
| **Every Code** | 自动检测 | `notify` hook | `~/.code/sessions/**/rollout-*.jsonl` |
|
|
87
87
|
| **Claude Code** | 自动检测 | `Stop` + `SessionEnd` hooks | 本地 hook 输出 |
|
|
88
88
|
| **Gemini CLI** | 自动检测 | `SessionEnd` hook | `~/.gemini/tmp/**/chats/session-*.json` |
|
|
89
|
-
| **OpenCode** | 自动检测 | plugin + 本地解析 | `~/.local/share/opencode/opencode.db
|
|
89
|
+
| **OpenCode** | 自动检测 | plugin + 本地解析 | `~/.local/share/opencode/opencode.db`(SQLite 是唯一受支持的本地 accounting source) |
|
|
90
90
|
| **OpenClaw** | 安装后自动检测 | session plugin | 本地 sanitized usage ledger |
|
|
91
91
|
|
|
92
92
|
### OpenClaw 说明
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeusage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/victorGPT/vibeusage.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/victorGPT/vibeusage/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/victorGPT/vibeusage#readme",
|
|
6
14
|
"bin": {
|
|
7
15
|
"tracker": "bin/tracker.js",
|
|
8
16
|
"vibeusage": "bin/tracker.js",
|
package/src/commands/init.js
CHANGED
|
@@ -190,7 +190,7 @@ function renderWelcome() {
|
|
|
190
190
|
DIVIDER,
|
|
191
191
|
"",
|
|
192
192
|
"This tool will:",
|
|
193
|
-
" - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Opencode, OpenClaw)",
|
|
193
|
+
" - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Kimi, Opencode, Hermes, OpenClaw)",
|
|
194
194
|
" - Set up lightweight hooks to track your flow state",
|
|
195
195
|
" - Link your device to your VibeScore account",
|
|
196
196
|
"",
|
|
@@ -486,7 +486,7 @@ try {
|
|
|
486
486
|
const originalPath =
|
|
487
487
|
source === 'every-code'
|
|
488
488
|
? codeOriginalPath
|
|
489
|
-
: source === 'claude' || source === 'opencode' || source === 'gemini'
|
|
489
|
+
: source === 'claude' || source === 'opencode' || source === 'gemini' || source === 'kimi'
|
|
490
490
|
? null
|
|
491
491
|
: codexOriginalPath;
|
|
492
492
|
if (originalPath) {
|
package/src/commands/status.js
CHANGED
|
@@ -6,6 +6,7 @@ const { readJson } = require("../lib/fs");
|
|
|
6
6
|
const { collectLocalSubscriptions } = require("../lib/subscriptions");
|
|
7
7
|
const { normalizeState: normalizeUploadState } = require("../lib/upload-throttle");
|
|
8
8
|
const { collectTrackerDiagnostics } = require("../lib/diagnostics");
|
|
9
|
+
const { readLastHermesUsageEvent, resolveHermesUsageLedgerPaths } = require("../lib/hermes-usage-ledger");
|
|
9
10
|
const { createIntegrationContext, listIntegrations, probeIntegrations } = require("../lib/integrations");
|
|
10
11
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
11
12
|
|
|
@@ -20,6 +21,7 @@ async function cmdStatus(argv = []) {
|
|
|
20
21
|
const home = os.homedir();
|
|
21
22
|
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
22
23
|
const configPath = path.join(trackerDir, "config.json");
|
|
24
|
+
const { ledgerPath: hermesLedgerPath } = resolveHermesUsageLedgerPaths({ trackerDir });
|
|
23
25
|
const queuePath = path.join(trackerDir, "queue.jsonl");
|
|
24
26
|
const queueStatePath = path.join(trackerDir, "queue.state.json");
|
|
25
27
|
const cursorsPath = path.join(trackerDir, "cursors.json");
|
|
@@ -80,8 +82,10 @@ async function cmdStatus(argv = []) {
|
|
|
80
82
|
const claudeProbe = probeByName.get("claude");
|
|
81
83
|
const geminiProbe = probeByName.get("gemini");
|
|
82
84
|
const opencodeProbe = probeByName.get("opencode");
|
|
85
|
+
const hermesProbe = probeByName.get("hermes");
|
|
83
86
|
const openclawSessionProbe = probeByName.get("openclaw-session");
|
|
84
87
|
const opencodeDbPresent = Boolean((await safeStat(opencodeDbPath))?.isFile?.());
|
|
88
|
+
const hermesLedgerPresent = Boolean((await safeStat(hermesLedgerPath))?.isFile?.());
|
|
85
89
|
const opencodeSqliteState =
|
|
86
90
|
cursors?.opencodeSqlite && typeof cursors.opencodeSqlite === "object"
|
|
87
91
|
? cursors.opencodeSqlite
|
|
@@ -90,6 +94,15 @@ async function cmdStatus(argv = []) {
|
|
|
90
94
|
typeof opencodeSqliteState.lastStatus === "string" && opencodeSqliteState.lastStatus.trim()
|
|
91
95
|
? opencodeSqliteState.lastStatus.trim()
|
|
92
96
|
: "never_checked";
|
|
97
|
+
const hermesLedgerState =
|
|
98
|
+
cursors?.hermesLedger && typeof cursors.hermesLedger === "object" ? cursors.hermesLedger : {};
|
|
99
|
+
const hermesLastLedgerEvent = await readLastHermesUsageEvent({ trackerDir });
|
|
100
|
+
const hermesLastEventAt =
|
|
101
|
+
typeof hermesLastLedgerEvent?.emitted_at === "string"
|
|
102
|
+
? hermesLastLedgerEvent.emitted_at
|
|
103
|
+
: typeof hermesLedgerState.lastEventAt === "string"
|
|
104
|
+
? hermesLedgerState.lastEventAt
|
|
105
|
+
: "never";
|
|
93
106
|
|
|
94
107
|
process.stdout.write(
|
|
95
108
|
[
|
|
@@ -111,6 +124,9 @@ async function cmdStatus(argv = []) {
|
|
|
111
124
|
`- Claude plugin: ${renderIntegrationStatus(descriptors.get("claude"), claudeProbe)}`,
|
|
112
125
|
`- Gemini hooks: ${renderIntegrationStatus(descriptors.get("gemini"), geminiProbe)}`,
|
|
113
126
|
`- Opencode plugin: ${renderIntegrationStatus(descriptors.get("opencode"), opencodeProbe)}`,
|
|
127
|
+
`- Hermes plugin: ${renderIntegrationStatus(descriptors.get("hermes"), hermesProbe)}`,
|
|
128
|
+
`- Hermes ledger: ${hermesLedgerPresent ? "present" : "missing"}`,
|
|
129
|
+
`- Hermes last ledger event: ${hermesLastEventAt}`,
|
|
114
130
|
`- OpenCode SQLite DB: ${opencodeDbPresent ? "present" : "missing"}`,
|
|
115
131
|
`- OpenCode SQLite reader: ${opencodeSqliteReader}`,
|
|
116
132
|
`- OpenClaw session plugin: ${renderIntegrationStatus(
|
package/src/commands/sync.js
CHANGED
|
@@ -8,10 +8,11 @@ const {
|
|
|
8
8
|
listRolloutFiles,
|
|
9
9
|
listClaudeProjectFiles,
|
|
10
10
|
listGeminiSessionFiles,
|
|
11
|
-
|
|
11
|
+
listKimiSessionFiles,
|
|
12
12
|
parseRolloutIncremental,
|
|
13
13
|
parseClaudeIncremental,
|
|
14
14
|
parseGeminiIncremental,
|
|
15
|
+
parseKimiIncremental,
|
|
15
16
|
parseOpencodeIncremental,
|
|
16
17
|
normalizeHourlyState,
|
|
17
18
|
getHourlyBucket,
|
|
@@ -22,6 +23,7 @@ const {
|
|
|
22
23
|
} = require("../lib/rollout");
|
|
23
24
|
const { drainQueueToCloud } = require("../lib/uploader");
|
|
24
25
|
const { readOpenclawUsageLedger } = require("../lib/openclaw-usage-ledger");
|
|
26
|
+
const { readHermesUsageLedger } = require("../lib/hermes-usage-ledger");
|
|
25
27
|
const { collectLocalSubscriptions } = require("../lib/subscriptions");
|
|
26
28
|
const { createProgress, renderBar, formatNumber, formatBytes } = require("../lib/progress");
|
|
27
29
|
const { syncHeartbeat } = require("../lib/vibeusage-api");
|
|
@@ -71,9 +73,10 @@ async function cmdSync(argv) {
|
|
|
71
73
|
const claudeProjectsDir = path.join(home, ".claude", "projects");
|
|
72
74
|
const geminiHome = process.env.GEMINI_HOME || path.join(home, ".gemini");
|
|
73
75
|
const geminiTmpDir = path.join(geminiHome, "tmp");
|
|
76
|
+
const kimiHome = process.env.KIMI_HOME || path.join(home, ".kimi");
|
|
77
|
+
const kimiSessionsDir = path.join(kimiHome, "sessions");
|
|
74
78
|
const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
|
|
75
79
|
const opencodeHome = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
|
|
76
|
-
const opencodeStorageDir = path.join(opencodeHome, "storage");
|
|
77
80
|
const opencodeDbPath = path.join(opencodeHome, "opencode.db");
|
|
78
81
|
|
|
79
82
|
const sources = [
|
|
@@ -114,6 +117,12 @@ async function cmdSync(argv) {
|
|
|
114
117
|
},
|
|
115
118
|
});
|
|
116
119
|
|
|
120
|
+
const hermesResult = await parseHermesUsageLedger({
|
|
121
|
+
trackerDir,
|
|
122
|
+
cursors,
|
|
123
|
+
queuePath,
|
|
124
|
+
});
|
|
125
|
+
|
|
117
126
|
const openclawResult = opts.fromOpenclaw
|
|
118
127
|
? await parseOpenclawSanitizedLedger({
|
|
119
128
|
trackerDir,
|
|
@@ -174,28 +183,38 @@ async function cmdSync(argv) {
|
|
|
174
183
|
});
|
|
175
184
|
}
|
|
176
185
|
|
|
177
|
-
const
|
|
178
|
-
let
|
|
179
|
-
if (
|
|
180
|
-
progress
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
const kimiFiles = await listKimiSessionFiles(kimiSessionsDir);
|
|
187
|
+
let kimiResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
188
|
+
if (kimiFiles.length > 0) {
|
|
189
|
+
if (progress?.enabled) {
|
|
190
|
+
progress.start(
|
|
191
|
+
`Parsing Kimi ${renderBar(0)} 0/${formatNumber(kimiFiles.length)} files | buckets 0`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
kimiResult = await parseKimiIncremental({
|
|
195
|
+
sessionFiles: kimiFiles,
|
|
196
|
+
cursors,
|
|
197
|
+
queuePath,
|
|
198
|
+
projectQueuePath,
|
|
199
|
+
onProgress: (p) => {
|
|
200
|
+
if (!progress?.enabled) return;
|
|
201
|
+
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
202
|
+
progress.update(
|
|
203
|
+
`Parsing Kimi ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
|
|
204
|
+
p.bucketsQueued,
|
|
205
|
+
)}`,
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
source: "kimi",
|
|
209
|
+
});
|
|
183
210
|
}
|
|
211
|
+
|
|
212
|
+
let opencodeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
184
213
|
opencodeResult = await parseOpencodeIncremental({
|
|
185
|
-
messageFiles: opencodeFiles,
|
|
186
214
|
opencodeDbPath,
|
|
187
215
|
cursors,
|
|
188
216
|
queuePath,
|
|
189
217
|
projectQueuePath,
|
|
190
|
-
onProgress: (p) => {
|
|
191
|
-
if (!progress?.enabled) return;
|
|
192
|
-
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
193
|
-
progress.update(
|
|
194
|
-
`Parsing Opencode ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
|
|
195
|
-
p.total,
|
|
196
|
-
)} files | buckets ${formatNumber(p.bucketsQueued)}`,
|
|
197
|
-
);
|
|
198
|
-
},
|
|
199
218
|
source: "opencode",
|
|
200
219
|
});
|
|
201
220
|
|
|
@@ -369,15 +388,19 @@ async function cmdSync(argv) {
|
|
|
369
388
|
if (!opts.auto) {
|
|
370
389
|
const totalParsed =
|
|
371
390
|
parseResult.filesProcessed +
|
|
391
|
+
hermesResult.filesProcessed +
|
|
372
392
|
openclawResult.filesProcessed +
|
|
373
393
|
claudeResult.filesProcessed +
|
|
374
394
|
geminiResult.filesProcessed +
|
|
395
|
+
kimiResult.filesProcessed +
|
|
375
396
|
opencodeResult.filesProcessed;
|
|
376
397
|
const totalBuckets =
|
|
377
398
|
parseResult.bucketsQueued +
|
|
399
|
+
hermesResult.bucketsQueued +
|
|
378
400
|
openclawResult.bucketsQueued +
|
|
379
401
|
claudeResult.bucketsQueued +
|
|
380
402
|
geminiResult.bucketsQueued +
|
|
403
|
+
kimiResult.bucketsQueued +
|
|
381
404
|
opencodeResult.bucketsQueued;
|
|
382
405
|
process.stdout.write(
|
|
383
406
|
[
|
|
@@ -436,6 +459,78 @@ function parseArgs(argv) {
|
|
|
436
459
|
|
|
437
460
|
module.exports = { cmdSync };
|
|
438
461
|
|
|
462
|
+
async function parseHermesUsageLedger({ trackerDir, cursors, queuePath }) {
|
|
463
|
+
const ledgerCursor =
|
|
464
|
+
cursors?.hermesLedger && typeof cursors.hermesLedger === "object" ? cursors.hermesLedger : {};
|
|
465
|
+
const offset = Math.max(0, Number(ledgerCursor.offset || 0));
|
|
466
|
+
const { events, endOffset } = await readHermesUsageLedger({ trackerDir, offset });
|
|
467
|
+
|
|
468
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
469
|
+
const touchedBuckets = new Set();
|
|
470
|
+
let eventsAggregated = 0;
|
|
471
|
+
|
|
472
|
+
for (const event of events) {
|
|
473
|
+
if (!event || typeof event !== "object") continue;
|
|
474
|
+
if (event.type !== "usage") continue;
|
|
475
|
+
const bucketStart = toUtcHalfHourStart(event.emitted_at);
|
|
476
|
+
if (!bucketStart) continue;
|
|
477
|
+
|
|
478
|
+
const model =
|
|
479
|
+
typeof event.model === "string" && event.model.trim() ? event.model.trim() : "unknown";
|
|
480
|
+
const source = "hermes";
|
|
481
|
+
const delta = {
|
|
482
|
+
input_tokens: Math.max(0, Number(event.input_tokens || 0)),
|
|
483
|
+
cached_input_tokens: Math.max(
|
|
484
|
+
0,
|
|
485
|
+
Number(event.cache_read_tokens || 0) + Number(event.cache_write_tokens || 0),
|
|
486
|
+
),
|
|
487
|
+
output_tokens: Math.max(0, Number(event.output_tokens || 0)),
|
|
488
|
+
reasoning_output_tokens: Math.max(0, Number(event.reasoning_tokens || 0)),
|
|
489
|
+
total_tokens: Math.max(0, Number(event.total_tokens || 0)),
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
if (
|
|
493
|
+
delta.input_tokens === 0 &&
|
|
494
|
+
delta.cached_input_tokens === 0 &&
|
|
495
|
+
delta.output_tokens === 0 &&
|
|
496
|
+
delta.reasoning_output_tokens === 0 &&
|
|
497
|
+
delta.total_tokens === 0
|
|
498
|
+
) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
503
|
+
addTotals(bucket.totals, delta);
|
|
504
|
+
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
505
|
+
eventsAggregated += 1;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
509
|
+
const lastUsageEvent = [...events].reverse().find((event) => {
|
|
510
|
+
if (!event || event.type !== "usage") return false;
|
|
511
|
+
return Boolean(toUtcHalfHourStart(event.emitted_at));
|
|
512
|
+
});
|
|
513
|
+
hourlyState.updatedAt = new Date().toISOString();
|
|
514
|
+
cursors.hourly = hourlyState;
|
|
515
|
+
cursors.hermesLedger = {
|
|
516
|
+
version: 1,
|
|
517
|
+
offset: endOffset,
|
|
518
|
+
updatedAt: new Date().toISOString(),
|
|
519
|
+
lastEventAt:
|
|
520
|
+
typeof lastUsageEvent?.emitted_at === "string"
|
|
521
|
+
? lastUsageEvent.emitted_at
|
|
522
|
+
: typeof ledgerCursor.lastEventAt === "string"
|
|
523
|
+
? ledgerCursor.lastEventAt
|
|
524
|
+
: null,
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
filesProcessed: endOffset > offset ? 1 : 0,
|
|
529
|
+
eventsAggregated,
|
|
530
|
+
bucketsQueued,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
439
534
|
async function parseOpenclawSanitizedLedger({ trackerDir, cursors, queuePath }) {
|
|
440
535
|
const ledgerCursor =
|
|
441
536
|
cursors?.openclawLedger && typeof cursors.openclawLedger === "object"
|
|
@@ -21,6 +21,7 @@ async function cmdUninstall(argv) {
|
|
|
21
21
|
const claudeConfigExists = await isDir(integrationContext.claude.configDir);
|
|
22
22
|
const geminiConfigExists = await isDir(integrationContext.gemini.configDir);
|
|
23
23
|
const opencodeConfigExists = await isDir(integrationContext.opencode.configDir);
|
|
24
|
+
const hermesConfigExists = await isDir(integrationContext.hermes.hermesHome);
|
|
24
25
|
const integrationResults = await uninstallIntegrations(integrationContext);
|
|
25
26
|
const resultByName = new Map(integrationResults.map((result) => [result.name, result]));
|
|
26
27
|
|
|
@@ -82,6 +83,15 @@ async function cmdUninstall(argv) {
|
|
|
82
83
|
noChangeText: "- Opencode plugin: no change",
|
|
83
84
|
skippedText: "- Opencode plugin: skipped (unexpected content)",
|
|
84
85
|
}),
|
|
86
|
+
renderHookLine({
|
|
87
|
+
exists: hermesConfigExists,
|
|
88
|
+
result: resultByName.get("hermes"),
|
|
89
|
+
missingText: `- Hermes plugin: skipped (${integrationContext.hermes.hermesHome} not found)`,
|
|
90
|
+
removedText: (result) =>
|
|
91
|
+
`- Hermes plugin removed: ${result.detail || integrationContext.hermes.pluginDir}`,
|
|
92
|
+
noChangeText: "- Hermes plugin: no change",
|
|
93
|
+
skippedText: "- Hermes plugin: skipped (unexpected content)",
|
|
94
|
+
}),
|
|
85
95
|
renderHookLine({
|
|
86
96
|
exists: true,
|
|
87
97
|
result: resultByName.get("openclaw-session"),
|
package/src/lib/diagnostics.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require("node:path");
|
|
|
3
3
|
const fs = require("node:fs/promises");
|
|
4
4
|
|
|
5
5
|
const { readJson } = require("./fs");
|
|
6
|
+
const { readLastHermesUsageEvent, resolveHermesUsageLedgerPaths } = require("./hermes-usage-ledger");
|
|
6
7
|
const { normalizeState: normalizeUploadState } = require("./upload-throttle");
|
|
7
8
|
const { createIntegrationContext, probeIntegrations } = require("./integrations");
|
|
8
9
|
|
|
@@ -25,6 +26,7 @@ async function collectTrackerDiagnostics({
|
|
|
25
26
|
});
|
|
26
27
|
const trackerDir = integrationContext.trackerPaths.trackerDir;
|
|
27
28
|
const configPath = path.join(trackerDir, "config.json");
|
|
29
|
+
const { ledgerPath: hermesLedgerPath } = resolveHermesUsageLedgerPaths({ trackerDir });
|
|
28
30
|
const queuePath = path.join(trackerDir, "queue.jsonl");
|
|
29
31
|
const queueStatePath = path.join(trackerDir, "queue.state.json");
|
|
30
32
|
const cursorsPath = path.join(trackerDir, "cursors.json");
|
|
@@ -45,7 +47,10 @@ async function collectTrackerDiagnostics({
|
|
|
45
47
|
const probeByName = new Map(probes.map((probe) => [probe.name, probe]));
|
|
46
48
|
const opencodeSqliteCursor =
|
|
47
49
|
cursors?.opencodeSqlite && typeof cursors.opencodeSqlite === "object" ? cursors.opencodeSqlite : {};
|
|
50
|
+
const hermesLedgerCursor =
|
|
51
|
+
cursors?.hermesLedger && typeof cursors.hermesLedger === "object" ? cursors.hermesLedger : {};
|
|
48
52
|
const opencodeDbStat = await safeStat(opencodeDbPath);
|
|
53
|
+
const hermesLedgerStat = await safeStat(hermesLedgerPath);
|
|
49
54
|
|
|
50
55
|
const queueSize = await safeStatSize(queuePath);
|
|
51
56
|
const offsetBytes = Number(queueState.offset || 0);
|
|
@@ -60,6 +65,7 @@ async function collectTrackerDiagnostics({
|
|
|
60
65
|
const claudeProbe = probeByName.get("claude");
|
|
61
66
|
const geminiProbe = probeByName.get("gemini");
|
|
62
67
|
const opencodeProbe = probeByName.get("opencode");
|
|
68
|
+
const hermesProbe = probeByName.get("hermes");
|
|
63
69
|
const openclawSessionProbe = probeByName.get("openclaw-session");
|
|
64
70
|
|
|
65
71
|
const codexNotify = Array.isArray(codexProbe?.currentNotify)
|
|
@@ -73,6 +79,7 @@ async function collectTrackerDiagnostics({
|
|
|
73
79
|
? new Date(uploadThrottle.lastSuccessMs).toISOString()
|
|
74
80
|
: null;
|
|
75
81
|
const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
|
|
82
|
+
const hermesLastLedgerEvent = await readLastHermesUsageEvent({ trackerDir });
|
|
76
83
|
|
|
77
84
|
return {
|
|
78
85
|
ok: true,
|
|
@@ -92,6 +99,9 @@ async function collectTrackerDiagnostics({
|
|
|
92
99
|
claude_config: redactValue(integrationContext.claude.settingsPath, home),
|
|
93
100
|
gemini_config: redactValue(integrationContext.gemini.settingsPath, home),
|
|
94
101
|
opencode_config: redactValue(integrationContext.opencode.configDir, home),
|
|
102
|
+
hermes_home: redactValue(integrationContext.hermes.hermesHome, home),
|
|
103
|
+
hermes_plugin_dir: redactValue(integrationContext.hermes.pluginDir, home),
|
|
104
|
+
hermes_ledger: redactValue(hermesLedgerPath, home),
|
|
95
105
|
},
|
|
96
106
|
config: {
|
|
97
107
|
base_url: typeof config?.baseUrl === "string" ? config.baseUrl : null,
|
|
@@ -131,6 +141,22 @@ async function collectTrackerDiagnostics({
|
|
|
131
141
|
? opencodeSqliteCursor.lastErrorCode
|
|
132
142
|
: null,
|
|
133
143
|
},
|
|
144
|
+
hermes: {
|
|
145
|
+
ledger_present: Boolean(hermesLedgerStat?.isFile?.()),
|
|
146
|
+
ledger_size_bytes: hermesLedgerStat?.isFile?.() ? Number(hermesLedgerStat.size || 0) : 0,
|
|
147
|
+
ledger_offset:
|
|
148
|
+
Number.isFinite(Number(hermesLedgerCursor.offset)) ? Math.max(0, Number(hermesLedgerCursor.offset)) : 0,
|
|
149
|
+
ledger_updated_at:
|
|
150
|
+
typeof hermesLedgerCursor.updatedAt === "string" ? hermesLedgerCursor.updatedAt : null,
|
|
151
|
+
last_event_at:
|
|
152
|
+
typeof hermesLastLedgerEvent?.emitted_at === "string"
|
|
153
|
+
? hermesLastLedgerEvent.emitted_at
|
|
154
|
+
: typeof hermesLedgerCursor.lastEventAt === "string"
|
|
155
|
+
? hermesLedgerCursor.lastEventAt
|
|
156
|
+
: null,
|
|
157
|
+
last_event_type:
|
|
158
|
+
typeof hermesLastLedgerEvent?.type === "string" ? hermesLastLedgerEvent.type : null,
|
|
159
|
+
},
|
|
134
160
|
notify: {
|
|
135
161
|
last_notify: lastNotify,
|
|
136
162
|
last_openclaw_triggered_sync: lastOpenclawSync,
|
|
@@ -147,6 +173,8 @@ async function collectTrackerDiagnostics({
|
|
|
147
173
|
gemini_hook_configured: Boolean(geminiProbe?.configured),
|
|
148
174
|
opencode_plugin_status: opencodeProbe?.status || "unknown",
|
|
149
175
|
opencode_plugin_configured: Boolean(opencodeProbe?.configured),
|
|
176
|
+
hermes_plugin_status: hermesProbe?.status || "unknown",
|
|
177
|
+
hermes_plugin_configured: Boolean(hermesProbe?.configured),
|
|
150
178
|
openclaw_session_plugin_status: openclawSessionProbe?.status || "unknown",
|
|
151
179
|
openclaw_session_plugin_configured: Boolean(openclawSessionProbe?.configured),
|
|
152
180
|
openclaw_session_plugin_linked: Boolean(openclawSessionProbe?.linked),
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
const fssync = require("node:fs");
|
|
5
|
+
|
|
6
|
+
const { ensureDir, writeFileAtomic } = require("./fs");
|
|
7
|
+
|
|
8
|
+
const HERMES_PLUGIN_ID = "vibeusage";
|
|
9
|
+
const HERMES_PLUGIN_MARKER = "VIBEUSAGE_HERMES_PLUGIN";
|
|
10
|
+
const HERMES_PLUGIN_TEMPLATE_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
function resolveHermesHome({ home = os.homedir(), env = process.env } = {}) {
|
|
13
|
+
const explicit = typeof env.HERMES_HOME === "string" ? env.HERMES_HOME.trim() : "";
|
|
14
|
+
if (explicit) return path.resolve(explicit);
|
|
15
|
+
return path.join(home, ".hermes");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveHermesPluginPaths({ home = os.homedir(), env = process.env, trackerDir } = {}) {
|
|
19
|
+
if (!trackerDir) throw new Error("trackerDir is required");
|
|
20
|
+
|
|
21
|
+
const hermesHome = resolveHermesHome({ home, env });
|
|
22
|
+
const pluginDir = path.join(hermesHome, "plugins", HERMES_PLUGIN_ID);
|
|
23
|
+
return {
|
|
24
|
+
hermesHome,
|
|
25
|
+
pluginId: HERMES_PLUGIN_ID,
|
|
26
|
+
pluginDir,
|
|
27
|
+
pluginYamlPath: path.join(pluginDir, "plugin.yaml"),
|
|
28
|
+
pluginInitPath: path.join(pluginDir, "__init__.py"),
|
|
29
|
+
ledgerPath: path.join(trackerDir, "hermes.usage.jsonl"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function probeHermesPlugin({ home = os.homedir(), env = process.env, trackerDir } = {}) {
|
|
34
|
+
const paths = resolveHermesPluginPaths({ home, env, trackerDir });
|
|
35
|
+
const expectedYaml = buildHermesPluginYaml();
|
|
36
|
+
const expectedInit = buildHermesPluginInit({ ledgerPath: paths.ledgerPath });
|
|
37
|
+
const hermesHomeExists = await pathExists(paths.hermesHome);
|
|
38
|
+
|
|
39
|
+
if (!hermesHomeExists) {
|
|
40
|
+
return {
|
|
41
|
+
configured: false,
|
|
42
|
+
status: "not_installed",
|
|
43
|
+
detail: "Hermes home not found",
|
|
44
|
+
initPreviewStatus: "updated",
|
|
45
|
+
initPreviewDetail: "Will install plugin",
|
|
46
|
+
...paths,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pluginDirExists = await pathExists(paths.pluginDir);
|
|
51
|
+
if (!pluginDirExists) {
|
|
52
|
+
return {
|
|
53
|
+
configured: false,
|
|
54
|
+
status: "not_installed",
|
|
55
|
+
detail: "Plugin not installed",
|
|
56
|
+
initPreviewStatus: "updated",
|
|
57
|
+
initPreviewDetail: "Will install plugin",
|
|
58
|
+
...paths,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const yamlState = await readTextStrict(paths.pluginYamlPath);
|
|
63
|
+
const initState = await readTextStrict(paths.pluginInitPath);
|
|
64
|
+
if (yamlState.status === "error" || initState.status === "error") {
|
|
65
|
+
return {
|
|
66
|
+
configured: false,
|
|
67
|
+
status: "unreadable",
|
|
68
|
+
detail: yamlState.error || initState.error || "Plugin unreadable",
|
|
69
|
+
...paths,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (yamlState.status === "missing" || initState.status === "missing") {
|
|
73
|
+
return {
|
|
74
|
+
configured: false,
|
|
75
|
+
status: "drifted",
|
|
76
|
+
detail: "Run vibeusage init to reconcile plugin",
|
|
77
|
+
...paths,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const configured = yamlState.value === expectedYaml && initState.value === expectedInit;
|
|
82
|
+
return {
|
|
83
|
+
configured,
|
|
84
|
+
status: configured ? "ready" : "drifted",
|
|
85
|
+
detail: configured ? "Plugin installed" : "Run vibeusage init to reconcile plugin",
|
|
86
|
+
...paths,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function installHermesPlugin({ home = os.homedir(), env = process.env, trackerDir } = {}) {
|
|
91
|
+
const paths = resolveHermesPluginPaths({ home, env, trackerDir });
|
|
92
|
+
const nextYaml = buildHermesPluginYaml();
|
|
93
|
+
const nextInit = buildHermesPluginInit({ ledgerPath: paths.ledgerPath });
|
|
94
|
+
const currentYaml = await fs.readFile(paths.pluginYamlPath, "utf8").catch(() => null);
|
|
95
|
+
const currentInit = await fs.readFile(paths.pluginInitPath, "utf8").catch(() => null);
|
|
96
|
+
const changed = currentYaml !== nextYaml || currentInit !== nextInit;
|
|
97
|
+
|
|
98
|
+
await ensureDir(paths.pluginDir);
|
|
99
|
+
await writeFileAtomic(paths.pluginYamlPath, nextYaml);
|
|
100
|
+
await writeFileAtomic(paths.pluginInitPath, nextInit);
|
|
101
|
+
return { configured: true, changed, ...paths };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function removeHermesPlugin({ home = os.homedir(), env = process.env, trackerDir } = {}) {
|
|
105
|
+
const paths = resolveHermesPluginPaths({ home, env, trackerDir });
|
|
106
|
+
const hadPluginDir = await pathExists(paths.pluginDir);
|
|
107
|
+
if (!hadPluginDir) {
|
|
108
|
+
return { removed: false, skippedReason: "plugin-missing", ...paths };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const yamlText = await fs.readFile(paths.pluginYamlPath, "utf8").catch(() => null);
|
|
112
|
+
const initText = await fs.readFile(paths.pluginInitPath, "utf8").catch(() => null);
|
|
113
|
+
const markerPresent = hasHermesPluginMarker(yamlText) && hasHermesPluginMarker(initText);
|
|
114
|
+
if (!markerPresent) {
|
|
115
|
+
return { removed: false, skippedReason: "unexpected-content", ...paths };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await fs.rm(paths.pluginDir, { recursive: true, force: true }).catch(() => {});
|
|
119
|
+
return { removed: true, ...paths };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildHermesPluginYaml() {
|
|
123
|
+
return readTemplateFile("plugin.yaml");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildHermesPluginInit({ ledgerPath }) {
|
|
127
|
+
const safeLedgerPath = typeof ledgerPath === "string" ? ledgerPath : "";
|
|
128
|
+
return readTemplateFile("__init__.py").replace("__LEDGER_PATH__", safeLedgerPath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function hasHermesPluginMarker(text) {
|
|
132
|
+
return typeof text === "string" && text.includes(HERMES_PLUGIN_MARKER);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function readTextStrict(filePath) {
|
|
136
|
+
try {
|
|
137
|
+
return { status: "ok", value: await fs.readFile(filePath, "utf8"), error: null };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err?.code === "ENOENT" || err?.code === "ENOTDIR") {
|
|
140
|
+
return { status: "missing", value: null, error: null };
|
|
141
|
+
}
|
|
142
|
+
return { status: "error", value: null, error: err?.message || String(err) };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function pathExists(targetPath) {
|
|
147
|
+
try {
|
|
148
|
+
await fs.stat(targetPath);
|
|
149
|
+
return true;
|
|
150
|
+
} catch (_err) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readTemplateFile(name) {
|
|
156
|
+
const templatePath = path.join(__dirname, "..", "templates", "hermes-vibeusage-plugin", name);
|
|
157
|
+
return fssync.readFileSync(templatePath, "utf8");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
HERMES_PLUGIN_ID,
|
|
162
|
+
HERMES_PLUGIN_MARKER,
|
|
163
|
+
HERMES_PLUGIN_TEMPLATE_VERSION,
|
|
164
|
+
resolveHermesHome,
|
|
165
|
+
resolveHermesPluginPaths,
|
|
166
|
+
probeHermesPlugin,
|
|
167
|
+
installHermesPlugin,
|
|
168
|
+
removeHermesPlugin,
|
|
169
|
+
buildHermesPluginYaml,
|
|
170
|
+
buildHermesPluginInit,
|
|
171
|
+
hasHermesPluginMarker,
|
|
172
|
+
};
|