@vibe-cafe/vibe-usage 0.7.14 → 0.7.16
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 +1 -0
- package/package.json +1 -1
- package/src/daemon.js +18 -3
- package/src/parsers/cursor.js +3 -10
- package/src/parsers/hermes.js +3 -12
- package/src/parsers/kiro.js +3 -10
- package/src/parsers/opencode.js +6 -19
- package/src/parsers/sqlite.js +74 -0
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
|
|
|
70
70
|
- Extracts session metadata from all parsers: active time (AI generation time, excluding queue/TTFT wait), total duration, message counts
|
|
71
71
|
- Uploads buckets + sessions to your vibecafe.ai dashboard (gzip-compressed when ≥ 1 KB, ~94% smaller)
|
|
72
72
|
- Stateless: computes full totals from local logs each sync (idempotent, no state files)
|
|
73
|
+
- SQLite-backed tools (Cursor, OpenCode, Kiro, Hermes) are read via Node's built-in `node:sqlite` on Node ≥ 22.5 — no `sqlite3` binary needed (works on Windows out of the box); on older Node it falls back to the system `sqlite3` CLI
|
|
73
74
|
- For continuous syncing, use `npx @vibe-cafe/vibe-usage daemon` or the [Vibe Usage Mac app](https://github.com/vibe-cafe/vibe-usage-app)
|
|
74
75
|
|
|
75
76
|
## AI Skill
|
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -22,16 +22,31 @@ export async function runDaemon() {
|
|
|
22
22
|
|
|
23
23
|
log('daemon started (sync every 30m, Ctrl+C to stop)');
|
|
24
24
|
|
|
25
|
+
// Why we don't exit on the first 401: launchd KeepAlive / systemd
|
|
26
|
+
// Restart=on-failure relaunch in ~10s, which used to turn a single bad/
|
|
27
|
+
// revoked key into ~360 ingest-401s per hour per machine. Sleeping a full
|
|
28
|
+
// INTERVAL between auth retries collapses that storm to the daemon's normal
|
|
29
|
+
// 30m cadence; only after MAX_AUTH_FAILURES consecutive 401s do we hand
|
|
30
|
+
// off to the supervisor, which by then can't relaunch fast enough to matter.
|
|
31
|
+
const MAX_AUTH_FAILURES = 5;
|
|
32
|
+
let consecutiveAuthFailures = 0;
|
|
33
|
+
|
|
25
34
|
// eslint-disable-next-line no-constant-condition
|
|
26
35
|
while (true) {
|
|
27
36
|
try {
|
|
28
37
|
await runSync({ throws: true, quiet: true });
|
|
38
|
+
consecutiveAuthFailures = 0;
|
|
29
39
|
} catch (err) {
|
|
30
40
|
if (err.message === 'UNAUTHORIZED') {
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
consecutiveAuthFailures++;
|
|
42
|
+
if (consecutiveAuthFailures >= MAX_AUTH_FAILURES) {
|
|
43
|
+
log(`API key invalid for ${MAX_AUTH_FAILURES} consecutive syncs, exiting.`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
log(`API key invalid (attempt ${consecutiveAuthFailures}/${MAX_AUTH_FAILURES}), retrying in 30m.`);
|
|
47
|
+
} else {
|
|
48
|
+
log(`sync error: ${err.message}`);
|
|
33
49
|
}
|
|
34
|
-
log(`sync error: ${err.message}`);
|
|
35
50
|
}
|
|
36
51
|
await sleep(INTERVAL);
|
|
37
52
|
}
|
package/src/parsers/cursor.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
1
|
import { copyFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
2
|
import { join, resolve } from 'node:path';
|
|
4
3
|
import { homedir, tmpdir } from 'node:os';
|
|
5
4
|
import { aggregateToBuckets } from './index.js';
|
|
5
|
+
import { queryDbJson } from './sqlite.js';
|
|
6
6
|
|
|
7
7
|
const STATE_DB_RELATIVE = join('User', 'globalStorage', 'state.vscdb');
|
|
8
8
|
const ACCESS_TOKEN_KEY = 'cursorAuth/accessToken';
|
|
@@ -66,14 +66,7 @@ function readAccessToken(dbPath) {
|
|
|
66
66
|
|
|
67
67
|
function queryAccessToken(dbPath) {
|
|
68
68
|
const sql = `SELECT value FROM ItemTable WHERE key = '${ACCESS_TOKEN_KEY}' LIMIT 1`;
|
|
69
|
-
const
|
|
70
|
-
encoding: 'utf-8',
|
|
71
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
72
|
-
timeout: 15000,
|
|
73
|
-
});
|
|
74
|
-
const trimmed = out.trim();
|
|
75
|
-
if (!trimmed || trimmed === '[]') return null;
|
|
76
|
-
const rows = JSON.parse(trimmed);
|
|
69
|
+
const rows = queryDbJson(dbPath, sql, { maxBuffer: 4 * 1024 * 1024, timeout: 15000 });
|
|
77
70
|
const value = rows[0]?.value;
|
|
78
71
|
if (typeof value !== 'string') return null;
|
|
79
72
|
const t = value.trim();
|
|
@@ -182,7 +175,7 @@ export async function parse() {
|
|
|
182
175
|
token = readAccessToken(dbPath);
|
|
183
176
|
} catch (err) {
|
|
184
177
|
if (err && typeof err.message === 'string' && err.message.includes('ENOENT')) {
|
|
185
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 to sync Cursor data.');
|
|
178
|
+
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Cursor data.');
|
|
186
179
|
}
|
|
187
180
|
throw err;
|
|
188
181
|
}
|
package/src/parsers/hermes.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
1
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { homedir } from 'node:os';
|
|
5
4
|
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
|
+
import { queryDbJson } from './sqlite.js';
|
|
6
6
|
|
|
7
7
|
const HERMES_HOME = process.env.HERMES_HOME || join(homedir(), '.hermes');
|
|
8
8
|
|
|
@@ -38,7 +38,7 @@ export async function parse() {
|
|
|
38
38
|
WHERE input_tokens > 0 OR output_tokens > 0`);
|
|
39
39
|
} catch (err) {
|
|
40
40
|
if (err.message && err.message.includes('ENOENT')) {
|
|
41
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 to sync Hermes data.');
|
|
41
|
+
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Hermes data.');
|
|
42
42
|
}
|
|
43
43
|
throw err;
|
|
44
44
|
}
|
|
@@ -121,14 +121,5 @@ function discoverDbPaths(home) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
function queryDb(dbPath, sql) {
|
|
124
|
-
|
|
125
|
-
'-json',
|
|
126
|
-
dbPath,
|
|
127
|
-
sql,
|
|
128
|
-
], { encoding: 'utf-8', maxBuffer: 100 * 1024 * 1024, timeout: 30000 });
|
|
129
|
-
|
|
130
|
-
const trimmed = output.trim();
|
|
131
|
-
if (!trimmed || trimmed === '[]') return [];
|
|
132
|
-
|
|
133
|
-
return JSON.parse(trimmed);
|
|
124
|
+
return queryDbJson(dbPath, sql);
|
|
134
125
|
}
|
package/src/parsers/kiro.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
1
|
import {
|
|
3
2
|
copyFileSync,
|
|
4
3
|
existsSync,
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
import { join, resolve } from 'node:path';
|
|
12
11
|
import { homedir, tmpdir } from 'node:os';
|
|
13
12
|
import { aggregateToBuckets } from './index.js';
|
|
13
|
+
import { queryDbJson } from './sqlite.js';
|
|
14
14
|
|
|
15
15
|
const KIROAGENT_RELATIVE = join('User', 'globalStorage', 'kiro.kiroagent');
|
|
16
16
|
|
|
@@ -41,14 +41,7 @@ function isLockError(err) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function queryDb(dbPath, sql) {
|
|
44
|
-
|
|
45
|
-
encoding: 'utf-8',
|
|
46
|
-
maxBuffer: 100 * 1024 * 1024,
|
|
47
|
-
timeout: 30000,
|
|
48
|
-
});
|
|
49
|
-
const trimmed = out.trim();
|
|
50
|
-
if (!trimmed || trimmed === '[]') return [];
|
|
51
|
-
return JSON.parse(trimmed);
|
|
44
|
+
return queryDbJson(dbPath, sql);
|
|
52
45
|
}
|
|
53
46
|
|
|
54
47
|
const TOKENS_SQL =
|
|
@@ -195,7 +188,7 @@ export async function parse() {
|
|
|
195
188
|
}
|
|
196
189
|
} catch (err) {
|
|
197
190
|
if (err && typeof err.message === 'string' && err.message.includes('ENOENT')) {
|
|
198
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 to sync Kiro data.');
|
|
191
|
+
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Kiro data.');
|
|
199
192
|
}
|
|
200
193
|
throw err;
|
|
201
194
|
}
|
package/src/parsers/opencode.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
3
2
|
import { join, basename } from 'node:path';
|
|
4
3
|
import { homedir } from 'node:os';
|
|
5
4
|
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
|
+
import { queryDbJson } from './sqlite.js';
|
|
6
6
|
|
|
7
7
|
const DATA_DIR = join(homedir(), '.local', 'share', 'opencode');
|
|
8
8
|
const DB_PATH = join(DATA_DIR, 'opencode.db');
|
|
@@ -33,29 +33,16 @@ function parseFromSqlite() {
|
|
|
33
33
|
json_extract(data, '$.path.root') as rootPath
|
|
34
34
|
FROM message`;
|
|
35
35
|
|
|
36
|
-
let
|
|
36
|
+
let rows;
|
|
37
37
|
try {
|
|
38
|
-
|
|
39
|
-
'-json',
|
|
40
|
-
DB_PATH,
|
|
41
|
-
query,
|
|
42
|
-
], { encoding: 'utf-8', maxBuffer: 100 * 1024 * 1024, timeout: 30000 });
|
|
38
|
+
rows = queryDbJson(DB_PATH, query);
|
|
43
39
|
} catch (err) {
|
|
44
40
|
if (err.status === 127 || (err.message && err.message.includes('ENOENT'))) {
|
|
45
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 to sync opencode data.');
|
|
41
|
+
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync opencode data.');
|
|
46
42
|
}
|
|
47
43
|
throw err;
|
|
48
44
|
}
|
|
49
|
-
|
|
50
|
-
output = output.trim();
|
|
51
|
-
if (!output || output === '[]') return { buckets: [], sessions: [] };
|
|
52
|
-
|
|
53
|
-
let rows;
|
|
54
|
-
try {
|
|
55
|
-
rows = JSON.parse(output);
|
|
56
|
-
} catch {
|
|
57
|
-
throw new Error('Failed to parse sqlite3 JSON output');
|
|
58
|
-
}
|
|
45
|
+
if (!rows.length) return { buckets: [], sessions: [] };
|
|
59
46
|
|
|
60
47
|
const entries = [];
|
|
61
48
|
const sessionEvents = [];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run a SQL query against a SQLite database and return rows as plain objects
|
|
8
|
+
* (column name → value), mirroring the shape of `sqlite3 -json` output.
|
|
9
|
+
*
|
|
10
|
+
* Prefers Node's built-in `node:sqlite` (available on Node >= 22.5, no external
|
|
11
|
+
* binary needed — important on Windows where the `sqlite3` CLI is rarely on
|
|
12
|
+
* PATH). Falls back to shelling out to the `sqlite3` CLI on older Node.
|
|
13
|
+
*
|
|
14
|
+
* If neither is available, throws an Error whose message contains "ENOENT" so
|
|
15
|
+
* callers can surface an "Install sqlite3" hint, matching the previous behavior.
|
|
16
|
+
*/
|
|
17
|
+
export function queryDbJson(dbPath, sql, { timeout = 30000, maxBuffer = 100 * 1024 * 1024 } = {}) {
|
|
18
|
+
const db = openNodeSqlite(dbPath);
|
|
19
|
+
if (db) {
|
|
20
|
+
try {
|
|
21
|
+
return db.prepare(sql).all();
|
|
22
|
+
} finally {
|
|
23
|
+
db.close();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return queryViaCli(dbPath, sql, { timeout, maxBuffer });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let nodeSqlite; // undefined = not tried, null = unavailable
|
|
30
|
+
|
|
31
|
+
function getNodeSqlite() {
|
|
32
|
+
if (nodeSqlite !== undefined) return nodeSqlite;
|
|
33
|
+
try {
|
|
34
|
+
// Suppress the one-time "SQLite is an experimental feature" ExperimentalWarning
|
|
35
|
+
// on Node versions where node:sqlite is still flagged experimental.
|
|
36
|
+
const prevEmit = process.emitWarning;
|
|
37
|
+
process.emitWarning = (warning, ...rest) => {
|
|
38
|
+
const opts = rest[0];
|
|
39
|
+
const type = typeof opts === 'object' && opts ? opts.type : opts;
|
|
40
|
+
const name = typeof warning === 'object' && warning ? warning.name : undefined;
|
|
41
|
+
if ((type === 'ExperimentalWarning' || name === 'ExperimentalWarning') && String(warning).includes('SQLite')) return;
|
|
42
|
+
return prevEmit.call(process, warning, ...rest);
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
nodeSqlite = require('node:sqlite');
|
|
46
|
+
} finally {
|
|
47
|
+
process.emitWarning = prevEmit;
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
nodeSqlite = null;
|
|
51
|
+
}
|
|
52
|
+
return nodeSqlite;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function openNodeSqlite(dbPath) {
|
|
56
|
+
const mod = getNodeSqlite();
|
|
57
|
+
if (!mod || !mod.DatabaseSync) return null;
|
|
58
|
+
try {
|
|
59
|
+
return new mod.DatabaseSync(dbPath, { readOnly: true });
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function queryViaCli(dbPath, sql, { timeout, maxBuffer }) {
|
|
66
|
+
const out = execFileSync('sqlite3', ['-json', dbPath, sql], {
|
|
67
|
+
encoding: 'utf-8',
|
|
68
|
+
maxBuffer,
|
|
69
|
+
timeout,
|
|
70
|
+
});
|
|
71
|
+
const trimmed = out.trim();
|
|
72
|
+
if (!trimmed || trimmed === '[]') return [];
|
|
73
|
+
return JSON.parse(trimmed);
|
|
74
|
+
}
|