@vibe-cafe/vibe-usage 0.9.5 → 0.9.6

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
@@ -63,7 +63,7 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
63
63
  | Amp | `~/.local/share/amp/threads/` |
64
64
  | Droid | `~/.factory/sessions/` |
65
65
  | Hermes | `~/.hermes/state.db` + `~/.hermes/profiles/<name>/state.db` (SQLite, multi-profile) |
66
- | Kiro | `~/Library/Application Support/Kiro/logs/**/kiro.kiroAgent/q-client.log` (`GetUsageLimitsCommand` credit snapshots; emits incremental `kiro-credits`, because Kiro bills by Credits / `INVOCATIONS`, not local model tokens). Legacy `dev_data/devdata.sqlite` token telemetry is available only with `VIBE_USAGE_KIRO_LEGACY_TOKENS=1` |
66
+ | Kiro | Kiro CLI `~/Library/Application Support/kiro-cli/data.sqlite3` / `~/.local/share/kiro-cli/data.sqlite3` plus optional `~/.kiro_sessions/*.json` archives (estimated tokens: CacheWrite = chars/4, CacheRead = accumulated context, Output = streamed chunks). If no CLI data exists, falls back to IDE `q-client.log` credit deltas as `kiro-credits`; legacy IDE `dev_data/devdata.sqlite` token telemetry is opt-in with `VIBE_USAGE_KIRO_LEGACY_TOKENS=1` |
67
67
  | Cline | `<host>/User/globalStorage/saoudrizwan.claude-dev/{state/taskHistory.json,tasks/<id>/ui_messages.json}` (walks all VSCode-fork hosts: Code, Cursor, Windsurf, VSCodium, Trae, ...) |
68
68
  | Roo Code | `<host>/User/globalStorage/rooveterinaryinc.roo-cline/{tasks/_index.json,tasks/<id>/{history_item,ui_messages}.json}` (walks all VSCode-fork hosts) |
69
69
  | Antigravity | `~/.gemini/antigravity/conversations/*.pb` to discover cascades, then reads token usage + sessions from the running language server via Connect RPC (process discovered with `ps`/`lsof` on macOS/Linux, PowerShell CIM with a `wmic` fallback on Windows) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -17,6 +17,8 @@ import { queryDbJson } from './sqlite.js';
17
17
  const KIRO_AGENT_RELATIVE = join('User', 'globalStorage', 'kiro.kiroagent');
18
18
  const KIRO_USER_RELATIVE = 'User';
19
19
  const CREDIT_MODEL = 'kiro-credits';
20
+ const ESTIMATE_MODEL = 'kiro-token-estimate';
21
+ const CHARS_PER_TOKEN = 4;
20
22
 
21
23
  function getDefaultAppPath() {
22
24
  if (process.platform === 'darwin') {
@@ -62,6 +64,34 @@ export function getKiroUserPath() {
62
64
  return existsSync(def) ? def : null;
63
65
  }
64
66
 
67
+ export function getKiroCliDbPath() {
68
+ const explicit = process.env.KIRO_CLI_DB_PATH?.trim();
69
+ if (explicit) {
70
+ const r = resolve(explicit);
71
+ return existsSync(r) ? r : null;
72
+ }
73
+ let def;
74
+ if (process.platform === 'darwin') {
75
+ def = join(homedir(), 'Library', 'Application Support', 'kiro-cli', 'data.sqlite3');
76
+ } else if (process.platform === 'win32') {
77
+ const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
78
+ def = join(appData, 'kiro-cli', 'data.sqlite3');
79
+ } else {
80
+ def = join(homedir(), '.local', 'share', 'kiro-cli', 'data.sqlite3');
81
+ }
82
+ return existsSync(def) ? def : null;
83
+ }
84
+
85
+ export function getKiroSessionsDir() {
86
+ const explicit = process.env.KIRO_SESSIONS_DIR?.trim();
87
+ if (explicit) {
88
+ const r = resolve(explicit);
89
+ return existsSync(r) ? r : null;
90
+ }
91
+ const def = join(homedir(), '.kiro_sessions');
92
+ return existsSync(def) ? def : null;
93
+ }
94
+
65
95
  function isLockError(err) {
66
96
  return err && typeof err.message === 'string' && /database is locked/i.test(err.message);
67
97
  }
@@ -70,32 +100,46 @@ function queryDb(dbPath, sql) {
70
100
  return queryDbJson(dbPath, sql);
71
101
  }
72
102
 
73
- const TOKENS_SQL =
74
- 'SELECT id, model, tokens_prompt, tokens_generated, timestamp ' +
75
- 'FROM tokens_generated ' +
76
- 'WHERE tokens_prompt > 0 OR tokens_generated > 0 ' +
77
- 'ORDER BY id ASC';
78
-
79
- function readLegacyDb(dbPath) {
103
+ function queryDbSnapshotOnLock(dbPath, sql) {
80
104
  try {
81
- return queryDb(dbPath, TOKENS_SQL);
105
+ return queryDb(dbPath, sql);
82
106
  } catch (err) {
83
107
  if (!isLockError(err)) throw err;
84
108
  const snapshotDir = mkdtempSync(join(tmpdir(), 'vibe-usage-kiro-'));
85
- const queryPath = join(snapshotDir, 'devdata.sqlite');
109
+ const queryPath = join(snapshotDir, 'data.sqlite3');
86
110
  copyFileSync(dbPath, queryPath);
87
111
  for (const suffix of ['-shm', '-wal']) {
88
112
  const companion = `${dbPath}${suffix}`;
89
113
  if (existsSync(companion)) copyFileSync(companion, `${queryPath}${suffix}`);
90
114
  }
91
115
  try {
92
- return queryDb(queryPath, TOKENS_SQL);
116
+ return queryDb(queryPath, sql);
93
117
  } finally {
94
118
  rmSync(snapshotDir, { recursive: true, force: true });
95
119
  }
96
120
  }
97
121
  }
98
122
 
123
+ function queryOptionalDb(dbPath, sql) {
124
+ try {
125
+ return queryDbSnapshotOnLock(dbPath, sql);
126
+ } catch (err) {
127
+ const msg = err && typeof err.message === 'string' ? err.message : '';
128
+ if (/no such table|no such column/i.test(msg)) return [];
129
+ throw err;
130
+ }
131
+ }
132
+
133
+ const TOKENS_SQL =
134
+ 'SELECT id, model, tokens_prompt, tokens_generated, timestamp ' +
135
+ 'FROM tokens_generated ' +
136
+ 'WHERE tokens_prompt > 0 OR tokens_generated > 0 ' +
137
+ 'ORDER BY id ASC';
138
+
139
+ function readLegacyDb(dbPath) {
140
+ return queryDbSnapshotOnLock(dbPath, TOKENS_SQL);
141
+ }
142
+
99
143
  // Legacy Kiro dev telemetry fallback. This is opt-in because recent Kiro builds
100
144
  // bill by server-side credits, while this table is often empty, estimated, or
101
145
  // populated with placeholder model names such as "agent".
@@ -113,7 +157,7 @@ function readLegacyJsonl(jsonlPath) {
113
157
  const obj = JSON.parse(lines[i]);
114
158
  rows.push({
115
159
  id: i + 1,
116
- model: obj.model || 'kiro-token-estimate',
160
+ model: obj.model || ESTIMATE_MODEL,
117
161
  tokens_prompt: obj.promptTokens || 0,
118
162
  tokens_generated: obj.generatedTokens || 0,
119
163
  timestamp: ts,
@@ -135,13 +179,13 @@ function parseDbTimestamp(value) {
135
179
 
136
180
  function normalizeLegacyModel(raw) {
137
181
  const model = typeof raw === 'string' ? raw.trim() : '';
138
- if (!model || model.toLowerCase() === 'agent') return 'kiro-token-estimate';
182
+ if (!model || model.toLowerCase() === 'agent') return ESTIMATE_MODEL;
139
183
  if (model === model.toLowerCase() && model.includes('-')) return model;
140
184
  return model
141
185
  .replace(/_\d{8}_V\d+_\d+$/i, '')
142
186
  .replace(/_V\d+$/i, '')
143
187
  .toLowerCase()
144
- .replace(/_/g, '-') || 'kiro-token-estimate';
188
+ .replace(/_/g, '-') || ESTIMATE_MODEL;
145
189
  }
146
190
 
147
191
  function rowsToLegacyEntries(rows) {
@@ -166,6 +210,196 @@ function rowsToLegacyEntries(rows) {
166
210
  return entries;
167
211
  }
168
212
 
213
+ function textLength(field) {
214
+ if (!field) return 0;
215
+ if (typeof field !== 'object' || Array.isArray(field)) return String(field).length;
216
+ let len = 0;
217
+ for (const [key, value] of Object.entries(field)) {
218
+ if (key === 'images') continue;
219
+ len += String(value ?? '').length;
220
+ }
221
+ return len;
222
+ }
223
+
224
+ function imageTokens(field) {
225
+ if (!field || typeof field !== 'object' || Array.isArray(field)) return 0;
226
+ const images = field.images;
227
+ if (!Array.isArray(images)) return 0;
228
+ let total = 0;
229
+ for (const image of images) {
230
+ const source = image && typeof image === 'object' ? image.source || {} : {};
231
+ const rawData = source.Bytes;
232
+ try {
233
+ let buf;
234
+ if (Array.isArray(rawData)) {
235
+ buf = Buffer.from(rawData);
236
+ } else if (typeof rawData === 'string') {
237
+ buf = Buffer.from(rawData, 'base64');
238
+ }
239
+ if (buf?.length >= 24 && buf.subarray(0, 4).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47]))) {
240
+ total += Math.floor((buf.readUInt32BE(16) * buf.readUInt32BE(20)) / 750);
241
+ continue;
242
+ }
243
+ } catch {
244
+ // fall back below
245
+ }
246
+ total += 1600;
247
+ }
248
+ return total;
249
+ }
250
+
251
+ function estimateTextTokens(field) {
252
+ return Math.floor(textLength(field) / CHARS_PER_TOKEN);
253
+ }
254
+
255
+ function normalizeCliModel(raw) {
256
+ const model = typeof raw === 'string' ? raw.trim() : '';
257
+ return model || ESTIMATE_MODEL;
258
+ }
259
+
260
+ function parseMsTimestamp(value) {
261
+ const n = Number(value);
262
+ if (!Number.isFinite(n) || n <= 0) return null;
263
+ const d = new Date(n);
264
+ return isNaN(d.getTime()) ? null : d;
265
+ }
266
+
267
+ function extractConversationTimes(data) {
268
+ const turns = Array.isArray(data?.history) ? data.history : [];
269
+ const first = turns[0]?.request_metadata?.request_start_timestamp_ms;
270
+ const last = turns[turns.length - 1]?.request_metadata?.request_start_timestamp_ms;
271
+ return {
272
+ createdAt: Number(first) || 0,
273
+ updatedAt: Number(last) || Number(first) || 0,
274
+ };
275
+ }
276
+
277
+ function conversationToEntries(conversation) {
278
+ const data = conversation?.value;
279
+ const turns = Array.isArray(data?.history) ? data.history : [];
280
+ const summary = data?.latest_summary || [];
281
+ const summaryTokens = summary ? Math.floor(String(summary).length / CHARS_PER_TOKEN) : 0;
282
+ let cumulative = summaryTokens;
283
+ let prevAssistantTokens = 0;
284
+ const entries = [];
285
+
286
+ for (let i = 0; i < turns.length; i++) {
287
+ const turn = turns[i] || {};
288
+ const meta = turn.request_metadata || {};
289
+ const timestamp = parseMsTimestamp(meta.request_start_timestamp_ms);
290
+ if (!timestamp) continue;
291
+
292
+ const userTokens = estimateTextTokens(turn.user) + imageTokens(turn.user);
293
+ const assistantTokens = estimateTextTokens(turn.assistant);
294
+ const outputTokens = Array.isArray(meta.time_between_chunks) ? meta.time_between_chunks.length : 0;
295
+ const cachedInputTokens = i > 0 ? cumulative : 0;
296
+ const inputTokens = userTokens + (i > 0 ? prevAssistantTokens : 0);
297
+
298
+ cumulative += userTokens + assistantTokens;
299
+ prevAssistantTokens = assistantTokens;
300
+
301
+ if (inputTokens === 0 && outputTokens === 0 && cachedInputTokens === 0) continue;
302
+ entries.push({
303
+ source: 'kiro',
304
+ model: normalizeCliModel(meta.model_id),
305
+ project: conversation.cwd || 'unknown',
306
+ timestamp,
307
+ inputTokens,
308
+ outputTokens,
309
+ cachedInputTokens,
310
+ reasoningOutputTokens: 0,
311
+ });
312
+ }
313
+
314
+ return entries;
315
+ }
316
+
317
+ export function conversationsToEstimateEntries(conversations) {
318
+ const entries = [];
319
+ const byId = new Map();
320
+ for (const conversation of conversations) {
321
+ const id = conversation?.conversation_id;
322
+ if (!id) continue;
323
+ const updatedAt = Number(conversation.updated_at) || 0;
324
+ const existing = byId.get(id);
325
+ if (!existing || updatedAt >= (Number(existing.updated_at) || 0)) {
326
+ byId.set(id, conversation);
327
+ }
328
+ }
329
+ for (const conversation of byId.values()) {
330
+ entries.push(...conversationToEntries(conversation));
331
+ }
332
+ return entries;
333
+ }
334
+
335
+ function readArchivedConversations(sessionsDir) {
336
+ if (!sessionsDir) return [];
337
+ let files;
338
+ try {
339
+ files = readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
340
+ } catch {
341
+ return [];
342
+ }
343
+ const conversations = [];
344
+ for (const file of files) {
345
+ try {
346
+ const obj = JSON.parse(readFileSync(join(sessionsDir, file), 'utf-8'));
347
+ if (obj?.conversation_id && obj?.value) conversations.push(obj);
348
+ } catch {
349
+ // skip malformed archives
350
+ }
351
+ }
352
+ return conversations;
353
+ }
354
+
355
+ function readCliDbConversations(dbPath) {
356
+ if (!dbPath) return [];
357
+ const conversations = [];
358
+ for (const row of queryOptionalDb(
359
+ dbPath,
360
+ 'SELECT conversation_id, key as cwd, created_at, updated_at, value FROM conversations_v2',
361
+ )) {
362
+ try {
363
+ conversations.push({
364
+ conversation_id: row.conversation_id,
365
+ cwd: row.cwd || 'unknown',
366
+ created_at: Number(row.created_at) || 0,
367
+ updated_at: Number(row.updated_at) || 0,
368
+ value: JSON.parse(row.value),
369
+ });
370
+ } catch {
371
+ // skip malformed conversations
372
+ }
373
+ }
374
+
375
+ for (const row of queryOptionalDb(dbPath, 'SELECT key as cwd, value FROM conversations')) {
376
+ try {
377
+ const data = JSON.parse(row.value);
378
+ const conversationId = data?.conversation_id;
379
+ if (!conversationId) continue;
380
+ const { createdAt, updatedAt } = extractConversationTimes(data);
381
+ conversations.push({
382
+ conversation_id: conversationId,
383
+ cwd: row.cwd || 'unknown',
384
+ created_at: createdAt,
385
+ updated_at: updatedAt,
386
+ value: data,
387
+ });
388
+ } catch {
389
+ // skip malformed conversations
390
+ }
391
+ }
392
+ return conversations;
393
+ }
394
+
395
+ function readCliEstimateEntries() {
396
+ const conversations = [
397
+ ...readArchivedConversations(getKiroSessionsDir()),
398
+ ...readCliDbConversations(getKiroCliDbPath()),
399
+ ];
400
+ return conversationsToEstimateEntries(conversations);
401
+ }
402
+
169
403
  function parseLogTimestamp(raw) {
170
404
  const d = new Date(String(raw).replace(' ', 'T'));
171
405
  return isNaN(d.getTime()) ? null : d;
@@ -340,6 +574,18 @@ function parseLegacyTokens(base) {
340
574
  }
341
575
 
342
576
  export async function parse() {
577
+ try {
578
+ const estimateEntries = readCliEstimateEntries();
579
+ if (estimateEntries.length > 0) {
580
+ return { buckets: aggregateToBuckets(estimateEntries), sessions: [] };
581
+ }
582
+ } catch (err) {
583
+ if (err && typeof err.message === 'string' && err.message.includes('ENOENT')) {
584
+ throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Kiro CLI data.');
585
+ }
586
+ throw err;
587
+ }
588
+
343
589
  const userPath = getKiroUserPath();
344
590
  if (!userPath) return { buckets: [], sessions: [] };
345
591