@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.7.14",
3
+ "version": "0.7.16",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
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
- log('API key invalid, exiting.');
32
- process.exit(1);
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
  }
@@ -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 out = execFileSync('sqlite3', ['-json', dbPath, sql], {
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
  }
@@ -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
- const output = execFileSync('sqlite3', [
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
  }
@@ -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
- const out = execFileSync('sqlite3', ['-json', dbPath, sql], {
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
  }
@@ -1,8 +1,8 @@
1
- import { execFileSync } from 'node:child_process';
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 output;
36
+ let rows;
37
37
  try {
38
- output = execFileSync('sqlite3', [
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
+ }