@vibe-cafe/vibe-usage 0.9.5 → 0.9.7
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 +464 -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 |
|
|
66
|
+
| Kiro | Kiro CLI native event streams `~/.kiro/sessions/cli/*.jsonl` (estimated tokens from message text: input = prompt + tool results, output = reply + tool calls, reasoning = thinking, cacheRead = re-sent context; thinking-block signatures excluded). Falls back to `~/Library/Application Support/kiro-cli/data.sqlite3` / `~/.local/share/kiro-cli/data.sqlite3` + optional `~/.kiro_sessions/*.json` archives, then 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,27 @@ 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;
|
|
22
|
+
const IMAGE_TOKENS = 1600;
|
|
23
|
+
|
|
24
|
+
// The official Kiro CLI persists each conversation as a `{version, kind, data}`
|
|
25
|
+
// event stream under ~/.kiro/sessions/cli/<uuid>.jsonl (companion <uuid>.json
|
|
26
|
+
// holds cwd + model). It carries NO token counts, so tokens are estimated from
|
|
27
|
+
// text length (chars/CHARS_PER_TOKEN), the same heuristic used elsewhere here.
|
|
28
|
+
//
|
|
29
|
+
// String values that are NOT linguistic tokens are excluded from the count.
|
|
30
|
+
// The big one is `signature`: extended-thinking blocks carry a ~500-char crypto
|
|
31
|
+
// signature; counting it as text inflates "output" by well over 100%.
|
|
32
|
+
const NON_TEXT_KEYS = new Set([
|
|
33
|
+
'signature', 'redactedContent', 'toolUseId', 'modelId', 'message_id', 'format', 'id',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// System prompt + tool JSON schemas are injected on every request but never
|
|
37
|
+
// written to the session log, so the stream alone counts none of them. They are
|
|
38
|
+
// a near-constant per-call overhead. Keep the default at 0 so the parser reports
|
|
39
|
+
// only observed log text unless the user explicitly opts into an estimate.
|
|
40
|
+
const DEFAULT_KIRO_CLI_SYSTEM_OVERHEAD_TOKENS = 0;
|
|
20
41
|
|
|
21
42
|
function getDefaultAppPath() {
|
|
22
43
|
if (process.platform === 'darwin') {
|
|
@@ -62,6 +83,45 @@ export function getKiroUserPath() {
|
|
|
62
83
|
return existsSync(def) ? def : null;
|
|
63
84
|
}
|
|
64
85
|
|
|
86
|
+
export function getKiroCliDbPath() {
|
|
87
|
+
const explicit = process.env.KIRO_CLI_DB_PATH?.trim();
|
|
88
|
+
if (explicit) {
|
|
89
|
+
const r = resolve(explicit);
|
|
90
|
+
return existsSync(r) ? r : null;
|
|
91
|
+
}
|
|
92
|
+
let def;
|
|
93
|
+
if (process.platform === 'darwin') {
|
|
94
|
+
def = join(homedir(), 'Library', 'Application Support', 'kiro-cli', 'data.sqlite3');
|
|
95
|
+
} else if (process.platform === 'win32') {
|
|
96
|
+
const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
|
|
97
|
+
def = join(appData, 'kiro-cli', 'data.sqlite3');
|
|
98
|
+
} else {
|
|
99
|
+
def = join(homedir(), '.local', 'share', 'kiro-cli', 'data.sqlite3');
|
|
100
|
+
}
|
|
101
|
+
return existsSync(def) ? def : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getKiroSessionsDir() {
|
|
105
|
+
const explicit = process.env.KIRO_SESSIONS_DIR?.trim();
|
|
106
|
+
if (explicit) {
|
|
107
|
+
const r = resolve(explicit);
|
|
108
|
+
return existsSync(r) ? r : null;
|
|
109
|
+
}
|
|
110
|
+
const def = join(homedir(), '.kiro_sessions');
|
|
111
|
+
return existsSync(def) ? def : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Native Kiro CLI session event streams: ~/.kiro/sessions/cli/*.jsonl
|
|
115
|
+
export function getKiroCliSessionsDir() {
|
|
116
|
+
const explicit = process.env.KIRO_CLI_SESSIONS_DIR?.trim();
|
|
117
|
+
if (explicit) {
|
|
118
|
+
const r = resolve(explicit);
|
|
119
|
+
return existsSync(r) ? r : null;
|
|
120
|
+
}
|
|
121
|
+
const def = join(homedir(), '.kiro', 'sessions', 'cli');
|
|
122
|
+
return existsSync(def) ? def : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
65
125
|
function isLockError(err) {
|
|
66
126
|
return err && typeof err.message === 'string' && /database is locked/i.test(err.message);
|
|
67
127
|
}
|
|
@@ -70,32 +130,46 @@ function queryDb(dbPath, sql) {
|
|
|
70
130
|
return queryDbJson(dbPath, sql);
|
|
71
131
|
}
|
|
72
132
|
|
|
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) {
|
|
133
|
+
function queryDbSnapshotOnLock(dbPath, sql) {
|
|
80
134
|
try {
|
|
81
|
-
return queryDb(dbPath,
|
|
135
|
+
return queryDb(dbPath, sql);
|
|
82
136
|
} catch (err) {
|
|
83
137
|
if (!isLockError(err)) throw err;
|
|
84
138
|
const snapshotDir = mkdtempSync(join(tmpdir(), 'vibe-usage-kiro-'));
|
|
85
|
-
const queryPath = join(snapshotDir, '
|
|
139
|
+
const queryPath = join(snapshotDir, 'data.sqlite3');
|
|
86
140
|
copyFileSync(dbPath, queryPath);
|
|
87
141
|
for (const suffix of ['-shm', '-wal']) {
|
|
88
142
|
const companion = `${dbPath}${suffix}`;
|
|
89
143
|
if (existsSync(companion)) copyFileSync(companion, `${queryPath}${suffix}`);
|
|
90
144
|
}
|
|
91
145
|
try {
|
|
92
|
-
return queryDb(queryPath,
|
|
146
|
+
return queryDb(queryPath, sql);
|
|
93
147
|
} finally {
|
|
94
148
|
rmSync(snapshotDir, { recursive: true, force: true });
|
|
95
149
|
}
|
|
96
150
|
}
|
|
97
151
|
}
|
|
98
152
|
|
|
153
|
+
function queryOptionalDb(dbPath, sql) {
|
|
154
|
+
try {
|
|
155
|
+
return queryDbSnapshotOnLock(dbPath, sql);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const msg = err && typeof err.message === 'string' ? err.message : '';
|
|
158
|
+
if (/no such table|no such column/i.test(msg)) return [];
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const TOKENS_SQL =
|
|
164
|
+
'SELECT id, model, tokens_prompt, tokens_generated, timestamp ' +
|
|
165
|
+
'FROM tokens_generated ' +
|
|
166
|
+
'WHERE tokens_prompt > 0 OR tokens_generated > 0 ' +
|
|
167
|
+
'ORDER BY id ASC';
|
|
168
|
+
|
|
169
|
+
function readLegacyDb(dbPath) {
|
|
170
|
+
return queryDbSnapshotOnLock(dbPath, TOKENS_SQL);
|
|
171
|
+
}
|
|
172
|
+
|
|
99
173
|
// Legacy Kiro dev telemetry fallback. This is opt-in because recent Kiro builds
|
|
100
174
|
// bill by server-side credits, while this table is often empty, estimated, or
|
|
101
175
|
// populated with placeholder model names such as "agent".
|
|
@@ -113,7 +187,7 @@ function readLegacyJsonl(jsonlPath) {
|
|
|
113
187
|
const obj = JSON.parse(lines[i]);
|
|
114
188
|
rows.push({
|
|
115
189
|
id: i + 1,
|
|
116
|
-
model: obj.model ||
|
|
190
|
+
model: obj.model || ESTIMATE_MODEL,
|
|
117
191
|
tokens_prompt: obj.promptTokens || 0,
|
|
118
192
|
tokens_generated: obj.generatedTokens || 0,
|
|
119
193
|
timestamp: ts,
|
|
@@ -135,13 +209,13 @@ function parseDbTimestamp(value) {
|
|
|
135
209
|
|
|
136
210
|
function normalizeLegacyModel(raw) {
|
|
137
211
|
const model = typeof raw === 'string' ? raw.trim() : '';
|
|
138
|
-
if (!model || model.toLowerCase() === 'agent') return
|
|
212
|
+
if (!model || model.toLowerCase() === 'agent') return ESTIMATE_MODEL;
|
|
139
213
|
if (model === model.toLowerCase() && model.includes('-')) return model;
|
|
140
214
|
return model
|
|
141
215
|
.replace(/_\d{8}_V\d+_\d+$/i, '')
|
|
142
216
|
.replace(/_V\d+$/i, '')
|
|
143
217
|
.toLowerCase()
|
|
144
|
-
.replace(/_/g, '-') ||
|
|
218
|
+
.replace(/_/g, '-') || ESTIMATE_MODEL;
|
|
145
219
|
}
|
|
146
220
|
|
|
147
221
|
function rowsToLegacyEntries(rows) {
|
|
@@ -166,6 +240,362 @@ function rowsToLegacyEntries(rows) {
|
|
|
166
240
|
return entries;
|
|
167
241
|
}
|
|
168
242
|
|
|
243
|
+
function textLength(field) {
|
|
244
|
+
if (!field) return 0;
|
|
245
|
+
if (typeof field !== 'object' || Array.isArray(field)) return String(field).length;
|
|
246
|
+
let len = 0;
|
|
247
|
+
for (const [key, value] of Object.entries(field)) {
|
|
248
|
+
if (key === 'images') continue;
|
|
249
|
+
len += String(value ?? '').length;
|
|
250
|
+
}
|
|
251
|
+
return len;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function imageTokens(field) {
|
|
255
|
+
if (!field || typeof field !== 'object' || Array.isArray(field)) return 0;
|
|
256
|
+
const images = field.images;
|
|
257
|
+
if (!Array.isArray(images)) return 0;
|
|
258
|
+
let total = 0;
|
|
259
|
+
for (const image of images) {
|
|
260
|
+
const source = image && typeof image === 'object' ? image.source || {} : {};
|
|
261
|
+
const rawData = source.Bytes;
|
|
262
|
+
try {
|
|
263
|
+
let buf;
|
|
264
|
+
if (Array.isArray(rawData)) {
|
|
265
|
+
buf = Buffer.from(rawData);
|
|
266
|
+
} else if (typeof rawData === 'string') {
|
|
267
|
+
buf = Buffer.from(rawData, 'base64');
|
|
268
|
+
}
|
|
269
|
+
if (buf?.length >= 24 && buf.subarray(0, 4).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47]))) {
|
|
270
|
+
total += Math.floor((buf.readUInt32BE(16) * buf.readUInt32BE(20)) / 750);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// fall back below
|
|
275
|
+
}
|
|
276
|
+
total += 1600;
|
|
277
|
+
}
|
|
278
|
+
return total;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function estimateTextTokens(field) {
|
|
282
|
+
return Math.floor(textLength(field) / CHARS_PER_TOKEN);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function normalizeCliModel(raw) {
|
|
286
|
+
const model = typeof raw === 'string' ? raw.trim() : '';
|
|
287
|
+
return model || ESTIMATE_MODEL;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function parseMsTimestamp(value) {
|
|
291
|
+
const n = Number(value);
|
|
292
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
293
|
+
const d = new Date(n);
|
|
294
|
+
return isNaN(d.getTime()) ? null : d;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function extractConversationTimes(data) {
|
|
298
|
+
const turns = Array.isArray(data?.history) ? data.history : [];
|
|
299
|
+
const first = turns[0]?.request_metadata?.request_start_timestamp_ms;
|
|
300
|
+
const last = turns[turns.length - 1]?.request_metadata?.request_start_timestamp_ms;
|
|
301
|
+
return {
|
|
302
|
+
createdAt: Number(first) || 0,
|
|
303
|
+
updatedAt: Number(last) || Number(first) || 0,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function conversationToEntries(conversation) {
|
|
308
|
+
const data = conversation?.value;
|
|
309
|
+
const turns = Array.isArray(data?.history) ? data.history : [];
|
|
310
|
+
const summary = data?.latest_summary || [];
|
|
311
|
+
const summaryTokens = summary ? Math.floor(String(summary).length / CHARS_PER_TOKEN) : 0;
|
|
312
|
+
let cumulative = summaryTokens;
|
|
313
|
+
let prevAssistantTokens = 0;
|
|
314
|
+
const entries = [];
|
|
315
|
+
|
|
316
|
+
for (let i = 0; i < turns.length; i++) {
|
|
317
|
+
const turn = turns[i] || {};
|
|
318
|
+
const meta = turn.request_metadata || {};
|
|
319
|
+
const timestamp = parseMsTimestamp(meta.request_start_timestamp_ms);
|
|
320
|
+
if (!timestamp) continue;
|
|
321
|
+
|
|
322
|
+
const userTokens = estimateTextTokens(turn.user) + imageTokens(turn.user);
|
|
323
|
+
const assistantTokens = estimateTextTokens(turn.assistant);
|
|
324
|
+
const outputTokens = Array.isArray(meta.time_between_chunks) ? meta.time_between_chunks.length : 0;
|
|
325
|
+
const cachedInputTokens = i > 0 ? cumulative : 0;
|
|
326
|
+
const inputTokens = userTokens + (i > 0 ? prevAssistantTokens : 0);
|
|
327
|
+
|
|
328
|
+
cumulative += userTokens + assistantTokens;
|
|
329
|
+
prevAssistantTokens = assistantTokens;
|
|
330
|
+
|
|
331
|
+
if (inputTokens === 0 && outputTokens === 0 && cachedInputTokens === 0) continue;
|
|
332
|
+
entries.push({
|
|
333
|
+
source: 'kiro',
|
|
334
|
+
model: normalizeCliModel(meta.model_id),
|
|
335
|
+
project: conversation.cwd || 'unknown',
|
|
336
|
+
timestamp,
|
|
337
|
+
inputTokens,
|
|
338
|
+
outputTokens,
|
|
339
|
+
cachedInputTokens,
|
|
340
|
+
reasoningOutputTokens: 0,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return entries;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function conversationsToEstimateEntries(conversations) {
|
|
348
|
+
const entries = [];
|
|
349
|
+
const byId = new Map();
|
|
350
|
+
for (const conversation of conversations) {
|
|
351
|
+
const id = conversation?.conversation_id;
|
|
352
|
+
if (!id) continue;
|
|
353
|
+
const updatedAt = Number(conversation.updated_at) || 0;
|
|
354
|
+
const existing = byId.get(id);
|
|
355
|
+
if (!existing || updatedAt >= (Number(existing.updated_at) || 0)) {
|
|
356
|
+
byId.set(id, conversation);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const conversation of byId.values()) {
|
|
360
|
+
entries.push(...conversationToEntries(conversation));
|
|
361
|
+
}
|
|
362
|
+
return entries;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function readArchivedConversations(sessionsDir) {
|
|
366
|
+
if (!sessionsDir) return [];
|
|
367
|
+
let files;
|
|
368
|
+
try {
|
|
369
|
+
files = readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
|
|
370
|
+
} catch {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
const conversations = [];
|
|
374
|
+
for (const file of files) {
|
|
375
|
+
try {
|
|
376
|
+
const obj = JSON.parse(readFileSync(join(sessionsDir, file), 'utf-8'));
|
|
377
|
+
if (obj?.conversation_id && obj?.value) conversations.push(obj);
|
|
378
|
+
} catch {
|
|
379
|
+
// skip malformed archives
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return conversations;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function readCliDbConversations(dbPath) {
|
|
386
|
+
if (!dbPath) return [];
|
|
387
|
+
const conversations = [];
|
|
388
|
+
for (const row of queryOptionalDb(
|
|
389
|
+
dbPath,
|
|
390
|
+
'SELECT conversation_id, key as cwd, created_at, updated_at, value FROM conversations_v2',
|
|
391
|
+
)) {
|
|
392
|
+
try {
|
|
393
|
+
conversations.push({
|
|
394
|
+
conversation_id: row.conversation_id,
|
|
395
|
+
cwd: row.cwd || 'unknown',
|
|
396
|
+
created_at: Number(row.created_at) || 0,
|
|
397
|
+
updated_at: Number(row.updated_at) || 0,
|
|
398
|
+
value: JSON.parse(row.value),
|
|
399
|
+
});
|
|
400
|
+
} catch {
|
|
401
|
+
// skip malformed conversations
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const row of queryOptionalDb(dbPath, 'SELECT key as cwd, value FROM conversations')) {
|
|
406
|
+
try {
|
|
407
|
+
const data = JSON.parse(row.value);
|
|
408
|
+
const conversationId = data?.conversation_id;
|
|
409
|
+
if (!conversationId) continue;
|
|
410
|
+
const { createdAt, updatedAt } = extractConversationTimes(data);
|
|
411
|
+
conversations.push({
|
|
412
|
+
conversation_id: conversationId,
|
|
413
|
+
cwd: row.cwd || 'unknown',
|
|
414
|
+
created_at: createdAt,
|
|
415
|
+
updated_at: updatedAt,
|
|
416
|
+
value: data,
|
|
417
|
+
});
|
|
418
|
+
} catch {
|
|
419
|
+
// skip malformed conversations
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return conversations;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function readCliEstimateEntries() {
|
|
426
|
+
const conversations = [
|
|
427
|
+
...readArchivedConversations(getKiroSessionsDir()),
|
|
428
|
+
...readCliDbConversations(getKiroCliDbPath()),
|
|
429
|
+
];
|
|
430
|
+
return conversationsToEstimateEntries(conversations);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// Native Kiro CLI event-stream sessions: ~/.kiro/sessions/cli/<uuid>.jsonl
|
|
435
|
+
// Each line is a {version, kind, data} event. Relevant kinds:
|
|
436
|
+
// Prompt user turn; data.content[] (text/image); data.meta.timestamp
|
|
437
|
+
// is the ONLY timestamp (epoch SECONDS).
|
|
438
|
+
// AssistantMessage data.content[] items: text (reply), thinking (reasoning +
|
|
439
|
+
// modelId + crypto signature), toolUse (name + input).
|
|
440
|
+
// ToolResults tool output fed back as context for the next turn.
|
|
441
|
+
// Compaction context was summarized; resets the running context size.
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
// Sum of string-leaf character lengths, skipping non-linguistic keys.
|
|
445
|
+
function textLeafChars(value) {
|
|
446
|
+
if (typeof value === 'string') return value.length;
|
|
447
|
+
if (Array.isArray(value)) {
|
|
448
|
+
let n = 0;
|
|
449
|
+
for (const v of value) n += textLeafChars(v);
|
|
450
|
+
return n;
|
|
451
|
+
}
|
|
452
|
+
if (value && typeof value === 'object') {
|
|
453
|
+
let n = 0;
|
|
454
|
+
for (const [k, v] of Object.entries(value)) {
|
|
455
|
+
if (NON_TEXT_KEYS.has(k)) continue;
|
|
456
|
+
n += textLeafChars(v);
|
|
457
|
+
}
|
|
458
|
+
return n;
|
|
459
|
+
}
|
|
460
|
+
return 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function estTokens(value) {
|
|
464
|
+
return Math.floor(textLeafChars(value) / CHARS_PER_TOKEN);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function getKiroCliSystemOverheadTokens() {
|
|
468
|
+
const raw = process.env.KIRO_CLI_SYSTEM_OVERHEAD_TOKENS;
|
|
469
|
+
if (raw === undefined || raw.trim() === '') return DEFAULT_KIRO_CLI_SYSTEM_OVERHEAD_TOKENS;
|
|
470
|
+
const n = Number(raw);
|
|
471
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function loadCliSessionMeta(sessionsDir, sessionId) {
|
|
475
|
+
try {
|
|
476
|
+
const d = JSON.parse(readFileSync(join(sessionsDir, `${sessionId}.json`), 'utf-8'));
|
|
477
|
+
return {
|
|
478
|
+
cwd: (d && typeof d.cwd === 'string' && d.cwd) || 'unknown',
|
|
479
|
+
model: d?.session_state?.rts_model_state?.model_info?.model_id || null,
|
|
480
|
+
};
|
|
481
|
+
} catch {
|
|
482
|
+
return { cwd: 'unknown', model: null };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function readCliSessionEntries(sessionsDir, fileName) {
|
|
487
|
+
const sessionId = fileName.replace(/\.jsonl$/, '');
|
|
488
|
+
const { cwd, model: metaModel } = loadCliSessionMeta(sessionsDir, sessionId);
|
|
489
|
+
const filePath = join(sessionsDir, fileName);
|
|
490
|
+
|
|
491
|
+
let mtime;
|
|
492
|
+
try { mtime = statSync(filePath).mtime; } catch { mtime = new Date(); }
|
|
493
|
+
|
|
494
|
+
const events = [];
|
|
495
|
+
const rl = createInterface({
|
|
496
|
+
input: createReadStream(filePath, { encoding: 'utf-8' }),
|
|
497
|
+
crlfDelay: Infinity,
|
|
498
|
+
});
|
|
499
|
+
for await (const raw of rl) {
|
|
500
|
+
const line = raw.trim();
|
|
501
|
+
if (!line) continue;
|
|
502
|
+
try { events.push(JSON.parse(line)); } catch { /* skip malformed line */ }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return cliEventsToEntries(events, { cwd, model: metaModel, fallbackTimestamp: mtime });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Pure state machine over a Kiro CLI session's ordered events. Exposed for tests.
|
|
509
|
+
export function cliEventsToEntries(events, { cwd = 'unknown', model = null, fallbackTimestamp = null } = {}) {
|
|
510
|
+
const entries = [];
|
|
511
|
+
let curTs = null; // Date from the enclosing Prompt (epoch seconds)
|
|
512
|
+
let cumulative = 0; // running conversation context, re-sent every turn
|
|
513
|
+
let pendingInput = 0; // fresh input accumulated since the last assistant turn
|
|
514
|
+
let curModel = model;
|
|
515
|
+
|
|
516
|
+
for (const ev of events) {
|
|
517
|
+
const data = ev?.data;
|
|
518
|
+
if (!data || typeof data !== 'object') continue;
|
|
519
|
+
const content = Array.isArray(data.content) ? data.content : [];
|
|
520
|
+
|
|
521
|
+
if (ev.kind === 'Prompt') {
|
|
522
|
+
const ts = data.meta?.timestamp;
|
|
523
|
+
if (typeof ts === 'number' && ts > 0) curTs = new Date(ts * 1000);
|
|
524
|
+
for (const item of content) {
|
|
525
|
+
pendingInput += item?.kind === 'image' ? IMAGE_TOKENS : estTokens(item?.data);
|
|
526
|
+
}
|
|
527
|
+
} else if (ev.kind === 'ToolResults') {
|
|
528
|
+
for (const item of content) pendingInput += estTokens(item?.data);
|
|
529
|
+
} else if (ev.kind === 'AssistantMessage') {
|
|
530
|
+
let output = 0;
|
|
531
|
+
let reasoning = 0;
|
|
532
|
+
let signatureTokens = 0;
|
|
533
|
+
for (const item of content) {
|
|
534
|
+
const cd = item?.data;
|
|
535
|
+
if (cd && typeof cd === 'object' && typeof cd.modelId === 'string' && cd.modelId) {
|
|
536
|
+
curModel = cd.modelId;
|
|
537
|
+
}
|
|
538
|
+
if (item?.kind === 'thinking' && cd && typeof cd === 'object') {
|
|
539
|
+
reasoning += Math.floor(String(cd.text ?? '').length / CHARS_PER_TOKEN);
|
|
540
|
+
// Signature persists in history and is re-sent as context on every
|
|
541
|
+
// later turn, but it is NOT model output — keep it out of output.
|
|
542
|
+
signatureTokens += Math.floor(String(cd.signature ?? '').length / CHARS_PER_TOKEN);
|
|
543
|
+
} else {
|
|
544
|
+
output += estTokens(cd);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const inputTokens = pendingInput;
|
|
549
|
+
// Each request re-sends the whole prior conversation (cache read) plus the
|
|
550
|
+
// optional per-call system-prompt/tool-schema overhead.
|
|
551
|
+
const cachedInputTokens = cumulative + getKiroCliSystemOverheadTokens();
|
|
552
|
+
|
|
553
|
+
if (inputTokens > 0 || output > 0 || reasoning > 0) {
|
|
554
|
+
entries.push({
|
|
555
|
+
source: 'kiro',
|
|
556
|
+
model: curModel || ESTIMATE_MODEL,
|
|
557
|
+
project: cwd,
|
|
558
|
+
timestamp: curTs || fallbackTimestamp || new Date(),
|
|
559
|
+
inputTokens,
|
|
560
|
+
outputTokens: output,
|
|
561
|
+
cachedInputTokens,
|
|
562
|
+
reasoningOutputTokens: reasoning,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Grow the running context: fresh input + everything generated this turn,
|
|
567
|
+
// including the thinking signature (it persists in history and is re-sent).
|
|
568
|
+
cumulative += inputTokens + output + reasoning + signatureTokens;
|
|
569
|
+
pendingInput = 0;
|
|
570
|
+
} else if (ev.kind === 'Compaction') {
|
|
571
|
+
cumulative = estTokens(data.summary);
|
|
572
|
+
pendingInput = 0;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return entries;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function readCliSessionStreamEntries() {
|
|
580
|
+
const dir = getKiroCliSessionsDir();
|
|
581
|
+
if (!dir) return [];
|
|
582
|
+
let files;
|
|
583
|
+
try {
|
|
584
|
+
files = readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
585
|
+
} catch {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
const entries = [];
|
|
589
|
+
for (const file of files) {
|
|
590
|
+
try {
|
|
591
|
+
entries.push(...await readCliSessionEntries(dir, file));
|
|
592
|
+
} catch {
|
|
593
|
+
// skip unreadable / concurrently rotated session
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return entries;
|
|
597
|
+
}
|
|
598
|
+
|
|
169
599
|
function parseLogTimestamp(raw) {
|
|
170
600
|
const d = new Date(String(raw).replace(' ', 'T'));
|
|
171
601
|
return isNaN(d.getTime()) ? null : d;
|
|
@@ -340,6 +770,27 @@ function parseLegacyTokens(base) {
|
|
|
340
770
|
}
|
|
341
771
|
|
|
342
772
|
export async function parse() {
|
|
773
|
+
// 1. Official Kiro CLI native event streams (~/.kiro/sessions/cli/*.jsonl).
|
|
774
|
+
// This is where the shipping CLI actually records conversations; the
|
|
775
|
+
// conversations_v2 DB path below is empty on those installs.
|
|
776
|
+
const streamEntries = await readCliSessionStreamEntries();
|
|
777
|
+
if (streamEntries.length > 0) {
|
|
778
|
+
return { buckets: aggregateToBuckets(streamEntries), sessions: [] };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 2. Kiro CLI conversations DB / archived snapshots (other CLI variants).
|
|
782
|
+
try {
|
|
783
|
+
const estimateEntries = readCliEstimateEntries();
|
|
784
|
+
if (estimateEntries.length > 0) {
|
|
785
|
+
return { buckets: aggregateToBuckets(estimateEntries), sessions: [] };
|
|
786
|
+
}
|
|
787
|
+
} catch (err) {
|
|
788
|
+
if (err && typeof err.message === 'string' && err.message.includes('ENOENT')) {
|
|
789
|
+
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Kiro CLI data.');
|
|
790
|
+
}
|
|
791
|
+
throw err;
|
|
792
|
+
}
|
|
793
|
+
|
|
343
794
|
const userPath = getKiroUserPath();
|
|
344
795
|
if (!userPath) return { buckets: [], sessions: [] };
|
|
345
796
|
|