@vibe-cafe/vibe-usage 0.9.4 → 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 +4 -1
- package/src/parsers/kiro.js +500 -120
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
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-cafe/vibe-usage",
|
|
3
|
-
"version": "0.9.
|
|
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
|
+
"scripts": {
|
|
7
|
+
"test": "node --test"
|
|
8
|
+
},
|
|
6
9
|
"bin": {
|
|
7
10
|
"vibe-usage": "bin/vibe-usage.js"
|
|
8
11
|
},
|
package/src/parsers/kiro.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
copyFileSync,
|
|
3
|
+
createReadStream,
|
|
3
4
|
existsSync,
|
|
4
5
|
mkdtempSync,
|
|
5
6
|
readFileSync,
|
|
@@ -7,23 +8,32 @@ import {
|
|
|
7
8
|
rmSync,
|
|
8
9
|
statSync,
|
|
9
10
|
} from 'node:fs';
|
|
10
|
-
import {
|
|
11
|
+
import { createInterface } from 'node:readline';
|
|
12
|
+
import { dirname, join, resolve } from 'node:path';
|
|
11
13
|
import { homedir, tmpdir } from 'node:os';
|
|
12
14
|
import { aggregateToBuckets } from './index.js';
|
|
13
15
|
import { queryDbJson } from './sqlite.js';
|
|
14
16
|
|
|
15
|
-
const
|
|
17
|
+
const KIRO_AGENT_RELATIVE = join('User', 'globalStorage', 'kiro.kiroagent');
|
|
18
|
+
const KIRO_USER_RELATIVE = 'User';
|
|
19
|
+
const CREDIT_MODEL = 'kiro-credits';
|
|
20
|
+
const ESTIMATE_MODEL = 'kiro-token-estimate';
|
|
21
|
+
const CHARS_PER_TOKEN = 4;
|
|
16
22
|
|
|
17
|
-
function
|
|
23
|
+
function getDefaultAppPath() {
|
|
18
24
|
if (process.platform === 'darwin') {
|
|
19
|
-
return join(homedir(), 'Library', 'Application Support', 'Kiro'
|
|
25
|
+
return join(homedir(), 'Library', 'Application Support', 'Kiro');
|
|
20
26
|
}
|
|
21
27
|
if (process.platform === 'win32') {
|
|
22
28
|
const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
|
|
23
|
-
return join(appData, 'Kiro'
|
|
29
|
+
return join(appData, 'Kiro');
|
|
24
30
|
}
|
|
25
31
|
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
|
|
26
|
-
return join(xdgConfigHome, 'Kiro'
|
|
32
|
+
return join(xdgConfigHome, 'Kiro');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getDefaultUserPath() {
|
|
36
|
+
return join(getDefaultAppPath(), KIRO_USER_RELATIVE);
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
export function getKiroBasePath() {
|
|
@@ -32,7 +42,53 @@ export function getKiroBasePath() {
|
|
|
32
42
|
const r = resolve(explicit);
|
|
33
43
|
return existsSync(r) ? r : null;
|
|
34
44
|
}
|
|
35
|
-
const def =
|
|
45
|
+
const def = join(getDefaultAppPath(), KIRO_AGENT_RELATIVE);
|
|
46
|
+
return existsSync(def) ? def : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getKiroUserPath() {
|
|
50
|
+
const explicitUser = process.env.KIRO_USER_PATH?.trim();
|
|
51
|
+
if (explicitUser) {
|
|
52
|
+
const r = resolve(explicitUser);
|
|
53
|
+
return existsSync(r) ? r : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const explicitBase = process.env.KIRO_BASE_PATH?.trim();
|
|
57
|
+
if (explicitBase) {
|
|
58
|
+
const base = resolve(explicitBase);
|
|
59
|
+
const userPath = resolve(base, '..', '..');
|
|
60
|
+
return existsSync(userPath) ? userPath : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const def = getDefaultUserPath();
|
|
64
|
+
return existsSync(def) ? def : null;
|
|
65
|
+
}
|
|
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');
|
|
36
92
|
return existsSync(def) ? def : null;
|
|
37
93
|
}
|
|
38
94
|
|
|
@@ -44,37 +100,50 @@ function queryDb(dbPath, sql) {
|
|
|
44
100
|
return queryDbJson(dbPath, sql);
|
|
45
101
|
}
|
|
46
102
|
|
|
47
|
-
|
|
48
|
-
'SELECT id, model, tokens_prompt, tokens_generated, timestamp ' +
|
|
49
|
-
'FROM tokens_generated ' +
|
|
50
|
-
'WHERE tokens_prompt > 0 OR tokens_generated > 0 ' +
|
|
51
|
-
'ORDER BY id ASC';
|
|
52
|
-
|
|
53
|
-
function readDb(dbPath) {
|
|
103
|
+
function queryDbSnapshotOnLock(dbPath, sql) {
|
|
54
104
|
try {
|
|
55
|
-
return queryDb(dbPath,
|
|
105
|
+
return queryDb(dbPath, sql);
|
|
56
106
|
} catch (err) {
|
|
57
107
|
if (!isLockError(err)) throw err;
|
|
58
|
-
// Kiro app holds a write lock; snapshot WAL set to a temp dir and retry.
|
|
59
108
|
const snapshotDir = mkdtempSync(join(tmpdir(), 'vibe-usage-kiro-'));
|
|
60
|
-
const queryPath = join(snapshotDir, '
|
|
109
|
+
const queryPath = join(snapshotDir, 'data.sqlite3');
|
|
61
110
|
copyFileSync(dbPath, queryPath);
|
|
62
111
|
for (const suffix of ['-shm', '-wal']) {
|
|
63
112
|
const companion = `${dbPath}${suffix}`;
|
|
64
113
|
if (existsSync(companion)) copyFileSync(companion, `${queryPath}${suffix}`);
|
|
65
114
|
}
|
|
66
115
|
try {
|
|
67
|
-
return queryDb(queryPath,
|
|
116
|
+
return queryDb(queryPath, sql);
|
|
68
117
|
} finally {
|
|
69
118
|
rmSync(snapshotDir, { recursive: true, force: true });
|
|
70
119
|
}
|
|
71
120
|
}
|
|
72
121
|
}
|
|
73
122
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
|
|
143
|
+
// Legacy Kiro dev telemetry fallback. This is opt-in because recent Kiro builds
|
|
144
|
+
// bill by server-side credits, while this table is often empty, estimated, or
|
|
145
|
+
// populated with placeholder model names such as "agent".
|
|
146
|
+
function readLegacyJsonl(jsonlPath) {
|
|
78
147
|
let raw;
|
|
79
148
|
try { raw = readFileSync(jsonlPath, 'utf-8'); } catch { return []; }
|
|
80
149
|
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
|
|
@@ -88,7 +157,7 @@ function readJsonl(jsonlPath) {
|
|
|
88
157
|
const obj = JSON.parse(lines[i]);
|
|
89
158
|
rows.push({
|
|
90
159
|
id: i + 1,
|
|
91
|
-
model: obj.model ||
|
|
160
|
+
model: obj.model || ESTIMATE_MODEL,
|
|
92
161
|
tokens_prompt: obj.promptTokens || 0,
|
|
93
162
|
tokens_generated: obj.generatedTokens || 0,
|
|
94
163
|
timestamp: ts,
|
|
@@ -100,132 +169,443 @@ function readJsonl(jsonlPath) {
|
|
|
100
169
|
return rows;
|
|
101
170
|
}
|
|
102
171
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
172
|
+
function parseDbTimestamp(value) {
|
|
173
|
+
if (!value) return null;
|
|
174
|
+
const s = String(value).trim();
|
|
175
|
+
const hasZone = /(?:Z|[+-]\d\d:?\d\d)$/.test(s);
|
|
176
|
+
const d = new Date(hasZone ? s.replace(' ', 'T') : `${s.replace(' ', 'T')}Z`);
|
|
177
|
+
return isNaN(d.getTime()) ? null : d;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizeLegacyModel(raw) {
|
|
181
|
+
const model = typeof raw === 'string' ? raw.trim() : '';
|
|
182
|
+
if (!model || model.toLowerCase() === 'agent') return ESTIMATE_MODEL;
|
|
183
|
+
if (model === model.toLowerCase() && model.includes('-')) return model;
|
|
184
|
+
return model
|
|
185
|
+
.replace(/_\d{8}_V\d+_\d+$/i, '')
|
|
186
|
+
.replace(/_V\d+$/i, '')
|
|
187
|
+
.toLowerCase()
|
|
188
|
+
.replace(/_/g, '-') || ESTIMATE_MODEL;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function rowsToLegacyEntries(rows) {
|
|
192
|
+
const entries = [];
|
|
193
|
+
for (const row of rows) {
|
|
194
|
+
const inputTokens = Math.max(0, Number(row.tokens_prompt) || 0);
|
|
195
|
+
const outputTokens = Math.max(0, Number(row.tokens_generated) || 0);
|
|
196
|
+
if (inputTokens === 0 && outputTokens === 0) continue;
|
|
197
|
+
const timestamp = parseDbTimestamp(row.timestamp);
|
|
198
|
+
if (!timestamp) continue;
|
|
199
|
+
entries.push({
|
|
200
|
+
source: 'kiro',
|
|
201
|
+
model: normalizeLegacyModel(row.model),
|
|
202
|
+
project: 'unknown',
|
|
203
|
+
timestamp,
|
|
204
|
+
inputTokens,
|
|
205
|
+
outputTokens,
|
|
206
|
+
cachedInputTokens: 0,
|
|
207
|
+
reasoningOutputTokens: 0,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return entries;
|
|
211
|
+
}
|
|
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;
|
|
114
232
|
try {
|
|
115
|
-
|
|
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
|
+
}
|
|
116
243
|
} catch {
|
|
117
|
-
|
|
244
|
+
// fall back below
|
|
118
245
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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);
|
|
131
327
|
}
|
|
132
328
|
}
|
|
133
|
-
|
|
134
|
-
|
|
329
|
+
for (const conversation of byId.values()) {
|
|
330
|
+
entries.push(...conversationToEntries(conversation));
|
|
331
|
+
}
|
|
332
|
+
return entries;
|
|
135
333
|
}
|
|
136
334
|
|
|
137
|
-
function
|
|
138
|
-
if (!
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
}
|
|
147
351
|
}
|
|
148
|
-
|
|
149
|
-
return bestDist < 10 * 60 * 1000 ? best : null;
|
|
352
|
+
return conversations;
|
|
150
353
|
}
|
|
151
354
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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;
|
|
164
393
|
}
|
|
165
394
|
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
395
|
+
function readCliEstimateEntries() {
|
|
396
|
+
const conversations = [
|
|
397
|
+
...readArchivedConversations(getKiroSessionsDir()),
|
|
398
|
+
...readCliDbConversations(getKiroCliDbPath()),
|
|
399
|
+
];
|
|
400
|
+
return conversationsToEstimateEntries(conversations);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function parseLogTimestamp(raw) {
|
|
404
|
+
const d = new Date(String(raw).replace(' ', 'T'));
|
|
170
405
|
return isNaN(d.getTime()) ? null : d;
|
|
171
406
|
}
|
|
172
407
|
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
if (!
|
|
408
|
+
function parseLogLine(line) {
|
|
409
|
+
const match = /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{3}) \[[^\]]+\] (\{.*\})$/.exec(line);
|
|
410
|
+
if (!match) return null;
|
|
411
|
+
const timestamp = parseLogTimestamp(match[1]);
|
|
412
|
+
if (!timestamp) return null;
|
|
413
|
+
try {
|
|
414
|
+
return { timestamp, obj: JSON.parse(match[2]) };
|
|
415
|
+
} catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function usageBreakdownsFromCommand(obj) {
|
|
421
|
+
if (obj?.commandName !== 'GetUsageLimitsCommand') return [];
|
|
422
|
+
const out = obj.output || {};
|
|
423
|
+
if (Array.isArray(out.usageBreakdownList)) return out.usageBreakdownList;
|
|
424
|
+
if (Array.isArray(out.usageBreakdowns)) return out.usageBreakdowns;
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function numberFrom(...values) {
|
|
429
|
+
for (const value of values) {
|
|
430
|
+
const n = Number(value);
|
|
431
|
+
if (Number.isFinite(n)) return n;
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function maxNumberFrom(...values) {
|
|
437
|
+
const nums = values
|
|
438
|
+
.map(value => Number(value))
|
|
439
|
+
.filter(Number.isFinite);
|
|
440
|
+
if (nums.length === 0) return null;
|
|
441
|
+
return Math.max(...nums);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function snapshotFromBreakdown(timestamp, breakdown) {
|
|
445
|
+
const type = String(breakdown.resourceType || breakdown.type || '').toUpperCase();
|
|
446
|
+
const unit = String(breakdown.unit || '').toUpperCase();
|
|
447
|
+
if (type !== 'CREDIT' || unit !== 'INVOCATIONS') return null;
|
|
176
448
|
|
|
449
|
+
const currentUsage = maxNumberFrom(
|
|
450
|
+
breakdown.currentUsageWithPrecision,
|
|
451
|
+
breakdown.currentUsage,
|
|
452
|
+
breakdown.freeTrialInfo?.currentUsageWithPrecision,
|
|
453
|
+
breakdown.freeTrialInfo?.currentUsage,
|
|
454
|
+
breakdown.freeTrialUsage?.currentUsage,
|
|
455
|
+
);
|
|
456
|
+
if (currentUsage === null) return null;
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
timestamp,
|
|
460
|
+
currentUsage,
|
|
461
|
+
resetDate: String(breakdown.nextDateReset || breakdown.resetDate || ''),
|
|
462
|
+
usageLimit: numberFrom(
|
|
463
|
+
breakdown.usageLimitWithPrecision,
|
|
464
|
+
breakdown.usageLimit,
|
|
465
|
+
breakdown.freeTrialInfo?.usageLimitWithPrecision,
|
|
466
|
+
breakdown.freeTrialInfo?.usageLimit,
|
|
467
|
+
breakdown.freeTrialUsage?.usageLimit,
|
|
468
|
+
),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function findQClientLogs(logsRoot) {
|
|
473
|
+
const files = [];
|
|
474
|
+
const stack = [logsRoot];
|
|
475
|
+
while (stack.length) {
|
|
476
|
+
const dir = stack.pop();
|
|
477
|
+
let entries;
|
|
478
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
479
|
+
for (const entry of entries) {
|
|
480
|
+
const p = join(dir, entry.name);
|
|
481
|
+
if (entry.isDirectory()) {
|
|
482
|
+
stack.push(p);
|
|
483
|
+
} else if (entry.isFile() && /^q-client\.log(?:\.\d+)?$/.test(entry.name)) {
|
|
484
|
+
files.push(p);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return files.sort();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function readLogSnapshots(logPath) {
|
|
492
|
+
const snapshots = [];
|
|
493
|
+
const rl = createInterface({
|
|
494
|
+
input: createReadStream(logPath, { encoding: 'utf-8' }),
|
|
495
|
+
crlfDelay: Infinity,
|
|
496
|
+
});
|
|
497
|
+
for await (const line of rl) {
|
|
498
|
+
const parsed = parseLogLine(line);
|
|
499
|
+
if (!parsed) continue;
|
|
500
|
+
for (const breakdown of usageBreakdownsFromCommand(parsed.obj)) {
|
|
501
|
+
const snapshot = snapshotFromBreakdown(parsed.timestamp, breakdown);
|
|
502
|
+
if (snapshot) snapshots.push(snapshot);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return snapshots;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function readUsageSnapshots(userPath) {
|
|
509
|
+
const appPath = dirname(userPath);
|
|
510
|
+
const logsRoot = join(appPath, 'logs');
|
|
511
|
+
const files = findQClientLogs(logsRoot);
|
|
512
|
+
const snapshots = [];
|
|
513
|
+
for (const file of files) {
|
|
514
|
+
try {
|
|
515
|
+
snapshots.push(...await readLogSnapshots(file));
|
|
516
|
+
} catch {
|
|
517
|
+
// skip unreadable / concurrently rotated logs
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return snapshots;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function dedupeSnapshots(snapshots) {
|
|
524
|
+
const map = new Map();
|
|
525
|
+
for (const s of snapshots) {
|
|
526
|
+
const key = `${s.timestamp.toISOString()}|${s.resetDate}|${s.currentUsage}`;
|
|
527
|
+
map.set(key, s);
|
|
528
|
+
}
|
|
529
|
+
return Array.from(map.values()).sort((a, b) => a.timestamp - b.timestamp);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function snapshotsToCreditEntries(snapshots) {
|
|
533
|
+
const ordered = dedupeSnapshots(snapshots);
|
|
534
|
+
const entries = [];
|
|
535
|
+
let prev = null;
|
|
536
|
+
|
|
537
|
+
for (const snapshot of ordered) {
|
|
538
|
+
if (!prev || snapshot.resetDate !== prev.resetDate || snapshot.currentUsage < prev.currentUsage) {
|
|
539
|
+
prev = snapshot;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const delta = Number((snapshot.currentUsage - prev.currentUsage).toFixed(4));
|
|
544
|
+
if (delta > 0) {
|
|
545
|
+
entries.push({
|
|
546
|
+
source: 'kiro',
|
|
547
|
+
model: CREDIT_MODEL,
|
|
548
|
+
project: 'unknown',
|
|
549
|
+
timestamp: snapshot.timestamp,
|
|
550
|
+
inputTokens: 0,
|
|
551
|
+
outputTokens: delta,
|
|
552
|
+
cachedInputTokens: 0,
|
|
553
|
+
reasoningOutputTokens: 0,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
prev = snapshot;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return entries;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function parseLegacyTokens(base) {
|
|
177
563
|
const dbPath = join(base, 'dev_data', 'devdata.sqlite');
|
|
178
564
|
const jsonlPath = join(base, 'dev_data', 'tokens_generated.jsonl');
|
|
179
|
-
|
|
180
565
|
let rows;
|
|
566
|
+
if (existsSync(dbPath)) {
|
|
567
|
+
rows = readLegacyDb(dbPath);
|
|
568
|
+
} else if (existsSync(jsonlPath)) {
|
|
569
|
+
rows = readLegacyJsonl(jsonlPath);
|
|
570
|
+
} else {
|
|
571
|
+
rows = [];
|
|
572
|
+
}
|
|
573
|
+
return rowsToLegacyEntries(rows);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export async function parse() {
|
|
181
577
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
rows = readJsonl(jsonlPath);
|
|
186
|
-
} else {
|
|
187
|
-
return { buckets: [], sessions: [] };
|
|
578
|
+
const estimateEntries = readCliEstimateEntries();
|
|
579
|
+
if (estimateEntries.length > 0) {
|
|
580
|
+
return { buckets: aggregateToBuckets(estimateEntries), sessions: [] };
|
|
188
581
|
}
|
|
189
582
|
} catch (err) {
|
|
190
583
|
if (err && typeof err.message === 'string' && err.message.includes('ENOENT')) {
|
|
191
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Kiro data.');
|
|
584
|
+
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Kiro CLI data.');
|
|
192
585
|
}
|
|
193
586
|
throw err;
|
|
194
587
|
}
|
|
195
588
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const timeline = buildModelTimeline(base);
|
|
199
|
-
const entries = [];
|
|
200
|
-
for (const row of rows) {
|
|
201
|
-
const inputTokens = Math.max(0, Number(row.tokens_prompt) || 0);
|
|
202
|
-
const outputTokens = Math.max(0, Number(row.tokens_generated) || 0);
|
|
203
|
-
if (inputTokens === 0 && outputTokens === 0) continue;
|
|
204
|
-
const timestamp = parseDbTimestamp(row.timestamp);
|
|
205
|
-
if (!timestamp) continue;
|
|
589
|
+
const userPath = getKiroUserPath();
|
|
590
|
+
if (!userPath) return { buckets: [], sessions: [] };
|
|
206
591
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (literal && literal.toLowerCase() !== 'agent') {
|
|
213
|
-
model = normalizeModelName(literal);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (!model) model = 'kiro-agent';
|
|
592
|
+
const snapshots = await readUsageSnapshots(userPath);
|
|
593
|
+
const entries = snapshotsToCreditEntries(snapshots);
|
|
594
|
+
if (entries.length > 0) {
|
|
595
|
+
return { buckets: aggregateToBuckets(entries), sessions: [] };
|
|
596
|
+
}
|
|
217
597
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
cachedInputTokens: 0,
|
|
226
|
-
reasoningOutputTokens: 0,
|
|
227
|
-
});
|
|
598
|
+
// Keep old token telemetry available for explicit debugging, but do not use
|
|
599
|
+
// it by default: it is not Kiro's billing source and causes false model rows.
|
|
600
|
+
if (process.env.VIBE_USAGE_KIRO_LEGACY_TOKENS === '1') {
|
|
601
|
+
const base = getKiroBasePath();
|
|
602
|
+
if (!base) return { buckets: [], sessions: [] };
|
|
603
|
+
const legacyEntries = parseLegacyTokens(base);
|
|
604
|
+
return { buckets: aggregateToBuckets(legacyEntries), sessions: [] };
|
|
228
605
|
}
|
|
229
606
|
|
|
230
|
-
|
|
607
|
+
// state.vscdb contains only the latest cumulative credit snapshot. The parser
|
|
608
|
+
// stays stateless, so a single cumulative point cannot be uploaded as a bucket
|
|
609
|
+
// without double-counting on later syncs.
|
|
610
|
+
return { buckets: [], sessions: [] };
|
|
231
611
|
}
|