@vibe-cafe/vibe-usage 0.7.7 → 0.7.8

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 CHANGED
@@ -57,7 +57,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
57
57
  | Kimi Code | `~/.kimi/sessions/` |
58
58
  | Amp | `~/.local/share/amp/threads/` |
59
59
  | Droid | `~/.factory/sessions/` |
60
- | Hermes | `~/.hermes/state.db` (SQLite) |
60
+ | Hermes | `~/.hermes/state.db` + `~/.hermes/profiles/<name>/state.db` (SQLite, multi-profile) |
61
61
 
62
62
  ## How It Works
63
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,95 +1,129 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, readdirSync, statSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { aggregateToBuckets, extractSessions } from './index.js';
6
6
 
7
7
  const HERMES_HOME = process.env.HERMES_HOME || join(homedir(), '.hermes');
8
- const DB_PATH = join(HERMES_HOME, 'state.db');
9
8
 
10
9
  /**
11
- * Parse Hermes Agent usage data from its SQLite database (~/.hermes/state.db).
10
+ * Parse Hermes Agent usage data from its SQLite databases.
11
+ *
12
+ * Hermes supports multiple profiles — the default profile lives at
13
+ * ~/.hermes/state.db, while named profiles live at ~/.hermes/profiles/<name>/state.db.
14
+ * Each profile is an independent HERMES_HOME with its own state.db, so we scan all of them.
12
15
  *
13
16
  * Token buckets come from the sessions table (cumulative per-session totals).
14
17
  * Session timing comes from the messages table (per-message role + timestamp).
15
18
  */
16
19
  export async function parse() {
17
- if (!existsSync(DB_PATH)) return { buckets: [], sessions: [] };
18
-
19
- let sessionRows;
20
- try {
21
- sessionRows = queryDb(`SELECT
22
- id,
23
- model,
24
- started_at as startedAt,
25
- input_tokens as inputTokens,
26
- output_tokens as outputTokens,
27
- cache_read_tokens as cacheReadTokens,
28
- reasoning_tokens as reasoningTokens
29
- FROM sessions
30
- WHERE input_tokens > 0 OR output_tokens > 0`);
31
- } catch (err) {
32
- if (err.message && err.message.includes('ENOENT')) {
33
- throw new Error('sqlite3 CLI not found. Install sqlite3 to sync Hermes data.');
34
- }
35
- throw err;
36
- }
20
+ const dbs = discoverDbPaths(HERMES_HOME);
21
+ if (dbs.length === 0) return { buckets: [], sessions: [] };
37
22
 
38
23
  const entries = [];
39
- for (const row of sessionRows) {
40
- // started_at is a Unix timestamp (float)
41
- const timestamp = new Date(row.startedAt * 1000);
42
- if (isNaN(timestamp.getTime())) continue;
43
-
44
- // Hermes stores input_tokens exclusive of cache (Anthropic-style semantics)
45
- entries.push({
46
- source: 'hermes',
47
- model: row.model || 'unknown',
48
- project: 'unknown',
49
- timestamp,
50
- inputTokens: row.inputTokens || 0,
51
- outputTokens: row.outputTokens || 0,
52
- cachedInputTokens: row.cacheReadTokens || 0,
53
- reasoningOutputTokens: row.reasoningTokens || 0,
54
- });
55
- }
24
+ const sessionEvents = [];
56
25
 
57
- // Session events from messages table for active time calculation
58
- let messageRows;
59
- try {
60
- messageRows = queryDb(`SELECT
61
- session_id as sessionId,
62
- role,
63
- timestamp
64
- FROM messages
65
- WHERE role IN ('user', 'assistant')
66
- ORDER BY timestamp`);
67
- } catch {
68
- // Messages query failed — return buckets only
69
- return { buckets: aggregateToBuckets(entries), sessions: [] };
70
- }
26
+ for (const { path: dbPath, profile } of dbs) {
27
+ let sessionRows;
28
+ try {
29
+ sessionRows = queryDb(dbPath, `SELECT
30
+ id,
31
+ model,
32
+ started_at as startedAt,
33
+ input_tokens as inputTokens,
34
+ output_tokens as outputTokens,
35
+ cache_read_tokens as cacheReadTokens,
36
+ reasoning_tokens as reasoningTokens
37
+ FROM sessions
38
+ WHERE input_tokens > 0 OR output_tokens > 0`);
39
+ } catch (err) {
40
+ if (err.message && err.message.includes('ENOENT')) {
41
+ throw new Error('sqlite3 CLI not found. Install sqlite3 to sync Hermes data.');
42
+ }
43
+ throw err;
44
+ }
71
45
 
72
- const sessionEvents = [];
73
- for (const row of messageRows) {
74
- const timestamp = new Date(row.timestamp * 1000);
75
- if (isNaN(timestamp.getTime())) continue;
76
-
77
- sessionEvents.push({
78
- sessionId: row.sessionId,
79
- source: 'hermes',
80
- project: 'unknown',
81
- timestamp,
82
- role: row.role === 'user' ? 'user' : 'assistant',
83
- });
46
+ for (const row of sessionRows) {
47
+ // started_at is a Unix timestamp (float)
48
+ const timestamp = new Date(row.startedAt * 1000);
49
+ if (isNaN(timestamp.getTime())) continue;
50
+
51
+ // Hermes stores input_tokens exclusive of cache (Anthropic-style semantics)
52
+ entries.push({
53
+ source: 'hermes',
54
+ model: row.model || 'unknown',
55
+ project: profile,
56
+ timestamp,
57
+ inputTokens: row.inputTokens || 0,
58
+ outputTokens: row.outputTokens || 0,
59
+ cachedInputTokens: row.cacheReadTokens || 0,
60
+ reasoningOutputTokens: row.reasoningTokens || 0,
61
+ });
62
+ }
63
+
64
+ let messageRows;
65
+ try {
66
+ messageRows = queryDb(dbPath, `SELECT
67
+ session_id as sessionId,
68
+ role,
69
+ timestamp
70
+ FROM messages
71
+ WHERE role IN ('user', 'assistant')
72
+ ORDER BY timestamp`);
73
+ } catch {
74
+ // Messages query failed for this profile — skip its session events
75
+ continue;
76
+ }
77
+
78
+ for (const row of messageRows) {
79
+ const timestamp = new Date(row.timestamp * 1000);
80
+ if (isNaN(timestamp.getTime())) continue;
81
+
82
+ sessionEvents.push({
83
+ sessionId: row.sessionId,
84
+ source: 'hermes',
85
+ project: profile,
86
+ timestamp,
87
+ role: row.role === 'user' ? 'user' : 'assistant',
88
+ });
89
+ }
84
90
  }
85
91
 
86
92
  return { buckets: aggregateToBuckets(entries), sessions: extractSessions(sessionEvents) };
87
93
  }
88
94
 
89
- function queryDb(sql) {
95
+ function discoverDbPaths(home) {
96
+ const dbs = [];
97
+
98
+ const defaultDb = join(home, 'state.db');
99
+ if (existsSync(defaultDb)) dbs.push({ path: defaultDb, profile: 'default' });
100
+
101
+ const profilesDir = join(home, 'profiles');
102
+ if (existsSync(profilesDir)) {
103
+ let entries;
104
+ try {
105
+ entries = readdirSync(profilesDir, { withFileTypes: true });
106
+ } catch {
107
+ return dbs;
108
+ }
109
+ for (const entry of entries) {
110
+ if (!entry.isDirectory()) continue;
111
+ const profileDb = join(profilesDir, entry.name, 'state.db');
112
+ try {
113
+ if (statSync(profileDb).isFile()) dbs.push({ path: profileDb, profile: entry.name });
114
+ } catch {
115
+ // missing or unreadable — skip
116
+ }
117
+ }
118
+ }
119
+
120
+ return dbs;
121
+ }
122
+
123
+ function queryDb(dbPath, sql) {
90
124
  const output = execFileSync('sqlite3', [
91
125
  '-json',
92
- DB_PATH,
126
+ dbPath,
93
127
  sql,
94
128
  ], { encoding: 'utf-8', maxBuffer: 100 * 1024 * 1024, timeout: 30000 });
95
129