@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 +1 -1
- package/package.json +1 -1
- package/src/parsers/kiro.js +259 -13
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/
|
|
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
package/src/parsers/kiro.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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, '
|
|
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,
|
|
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 ||
|
|
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
|
|
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, '-') ||
|
|
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
|
|