@vibe-cafe/vibe-usage 0.9.4 → 0.9.5
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 +251 -117
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/
|
|
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` |
|
|
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.5",
|
|
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,30 @@ 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';
|
|
16
20
|
|
|
17
|
-
function
|
|
21
|
+
function getDefaultAppPath() {
|
|
18
22
|
if (process.platform === 'darwin') {
|
|
19
|
-
return join(homedir(), 'Library', 'Application Support', 'Kiro'
|
|
23
|
+
return join(homedir(), 'Library', 'Application Support', 'Kiro');
|
|
20
24
|
}
|
|
21
25
|
if (process.platform === 'win32') {
|
|
22
26
|
const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
|
|
23
|
-
return join(appData, 'Kiro'
|
|
27
|
+
return join(appData, 'Kiro');
|
|
24
28
|
}
|
|
25
29
|
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
|
|
26
|
-
return join(xdgConfigHome, 'Kiro'
|
|
30
|
+
return join(xdgConfigHome, 'Kiro');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getDefaultUserPath() {
|
|
34
|
+
return join(getDefaultAppPath(), KIRO_USER_RELATIVE);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
export function getKiroBasePath() {
|
|
@@ -32,7 +40,25 @@ export function getKiroBasePath() {
|
|
|
32
40
|
const r = resolve(explicit);
|
|
33
41
|
return existsSync(r) ? r : null;
|
|
34
42
|
}
|
|
35
|
-
const def =
|
|
43
|
+
const def = join(getDefaultAppPath(), KIRO_AGENT_RELATIVE);
|
|
44
|
+
return existsSync(def) ? def : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getKiroUserPath() {
|
|
48
|
+
const explicitUser = process.env.KIRO_USER_PATH?.trim();
|
|
49
|
+
if (explicitUser) {
|
|
50
|
+
const r = resolve(explicitUser);
|
|
51
|
+
return existsSync(r) ? r : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const explicitBase = process.env.KIRO_BASE_PATH?.trim();
|
|
55
|
+
if (explicitBase) {
|
|
56
|
+
const base = resolve(explicitBase);
|
|
57
|
+
const userPath = resolve(base, '..', '..');
|
|
58
|
+
return existsSync(userPath) ? userPath : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const def = getDefaultUserPath();
|
|
36
62
|
return existsSync(def) ? def : null;
|
|
37
63
|
}
|
|
38
64
|
|
|
@@ -50,12 +76,11 @@ const TOKENS_SQL =
|
|
|
50
76
|
'WHERE tokens_prompt > 0 OR tokens_generated > 0 ' +
|
|
51
77
|
'ORDER BY id ASC';
|
|
52
78
|
|
|
53
|
-
function
|
|
79
|
+
function readLegacyDb(dbPath) {
|
|
54
80
|
try {
|
|
55
81
|
return queryDb(dbPath, TOKENS_SQL);
|
|
56
82
|
} catch (err) {
|
|
57
83
|
if (!isLockError(err)) throw err;
|
|
58
|
-
// Kiro app holds a write lock; snapshot WAL set to a temp dir and retry.
|
|
59
84
|
const snapshotDir = mkdtempSync(join(tmpdir(), 'vibe-usage-kiro-'));
|
|
60
85
|
const queryPath = join(snapshotDir, 'devdata.sqlite');
|
|
61
86
|
copyFileSync(dbPath, queryPath);
|
|
@@ -71,10 +96,10 @@ function readDb(dbPath) {
|
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
function
|
|
99
|
+
// Legacy Kiro dev telemetry fallback. This is opt-in because recent Kiro builds
|
|
100
|
+
// bill by server-side credits, while this table is often empty, estimated, or
|
|
101
|
+
// populated with placeholder model names such as "agent".
|
|
102
|
+
function readLegacyJsonl(jsonlPath) {
|
|
78
103
|
let raw;
|
|
79
104
|
try { raw = readFileSync(jsonlPath, 'utf-8'); } catch { return []; }
|
|
80
105
|
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
|
|
@@ -88,7 +113,7 @@ function readJsonl(jsonlPath) {
|
|
|
88
113
|
const obj = JSON.parse(lines[i]);
|
|
89
114
|
rows.push({
|
|
90
115
|
id: i + 1,
|
|
91
|
-
model: obj.model || '
|
|
116
|
+
model: obj.model || 'kiro-token-estimate',
|
|
92
117
|
tokens_prompt: obj.promptTokens || 0,
|
|
93
118
|
tokens_generated: obj.generatedTokens || 0,
|
|
94
119
|
timestamp: ts,
|
|
@@ -100,102 +125,26 @@ function readJsonl(jsonlPath) {
|
|
|
100
125
|
return rows;
|
|
101
126
|
}
|
|
102
127
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
try { entries = readdirSync(base, { withFileTypes: true }); } catch { return timeline; }
|
|
110
|
-
for (const entry of entries) {
|
|
111
|
-
if (!entry.isDirectory() || entry.name === 'dev_data') continue;
|
|
112
|
-
const dirPath = join(base, entry.name);
|
|
113
|
-
let files;
|
|
114
|
-
try {
|
|
115
|
-
files = readdirSync(dirPath).filter(f => f.endsWith('.chat'));
|
|
116
|
-
} catch {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
for (const file of files) {
|
|
120
|
-
try {
|
|
121
|
-
const data = JSON.parse(readFileSync(join(dirPath, file), 'utf-8'));
|
|
122
|
-
const meta = data?.metadata;
|
|
123
|
-
if (!meta?.modelId || !meta?.startTime) continue;
|
|
124
|
-
const startMs = Number(meta.startTime);
|
|
125
|
-
const endMs = Number(meta.endTime || meta.startTime);
|
|
126
|
-
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) continue;
|
|
127
|
-
timeline.push({ startMs, endMs, model: String(meta.modelId) });
|
|
128
|
-
} catch {
|
|
129
|
-
// skip unreadable / malformed chat files
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
timeline.sort((a, b) => a.startMs - b.startMs);
|
|
134
|
-
return timeline;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function resolveModel(timeline, ts) {
|
|
138
|
-
if (!timeline.length || !ts) return null;
|
|
139
|
-
const t = ts.getTime();
|
|
140
|
-
if (!Number.isFinite(t)) return null;
|
|
141
|
-
let best = null;
|
|
142
|
-
let bestDist = Infinity;
|
|
143
|
-
for (const e of timeline) {
|
|
144
|
-
if (t >= e.startMs && t <= e.endMs) return e.model;
|
|
145
|
-
const d = Math.min(Math.abs(t - e.startMs), Math.abs(t - e.endMs));
|
|
146
|
-
if (d < bestDist) { bestDist = d; best = e.model; }
|
|
147
|
-
}
|
|
148
|
-
// 10-minute tolerance — beyond that, treat as no match.
|
|
149
|
-
return bestDist < 10 * 60 * 1000 ? best : null;
|
|
128
|
+
function parseDbTimestamp(value) {
|
|
129
|
+
if (!value) return null;
|
|
130
|
+
const s = String(value).trim();
|
|
131
|
+
const hasZone = /(?:Z|[+-]\d\d:?\d\d)$/.test(s);
|
|
132
|
+
const d = new Date(hasZone ? s.replace(' ', 'T') : `${s.replace(' ', 'T')}Z`);
|
|
133
|
+
return isNaN(d.getTime()) ? null : d;
|
|
150
134
|
}
|
|
151
135
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (!
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (trimmed === trimmed.toLowerCase() && trimmed.includes('-')) return trimmed;
|
|
158
|
-
const cleaned = trimmed
|
|
136
|
+
function normalizeLegacyModel(raw) {
|
|
137
|
+
const model = typeof raw === 'string' ? raw.trim() : '';
|
|
138
|
+
if (!model || model.toLowerCase() === 'agent') return 'kiro-token-estimate';
|
|
139
|
+
if (model === model.toLowerCase() && model.includes('-')) return model;
|
|
140
|
+
return model
|
|
159
141
|
.replace(/_\d{8}_V\d+_\d+$/i, '')
|
|
160
142
|
.replace(/_V\d+$/i, '')
|
|
161
143
|
.toLowerCase()
|
|
162
|
-
.replace(/_/g, '-');
|
|
163
|
-
return cleaned || null;
|
|
144
|
+
.replace(/_/g, '-') || 'kiro-token-estimate';
|
|
164
145
|
}
|
|
165
146
|
|
|
166
|
-
function
|
|
167
|
-
if (!value) return null;
|
|
168
|
-
// SQLite CURRENT_TIMESTAMP: "2026-01-09 15:25:30" (UTC, naive — append Z).
|
|
169
|
-
const d = new Date(String(value).trim().replace(' ', 'T') + 'Z');
|
|
170
|
-
return isNaN(d.getTime()) ? null : d;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export async function parse() {
|
|
174
|
-
const base = getKiroBasePath();
|
|
175
|
-
if (!base) return { buckets: [], sessions: [] };
|
|
176
|
-
|
|
177
|
-
const dbPath = join(base, 'dev_data', 'devdata.sqlite');
|
|
178
|
-
const jsonlPath = join(base, 'dev_data', 'tokens_generated.jsonl');
|
|
179
|
-
|
|
180
|
-
let rows;
|
|
181
|
-
try {
|
|
182
|
-
if (existsSync(dbPath)) {
|
|
183
|
-
rows = readDb(dbPath);
|
|
184
|
-
} else if (existsSync(jsonlPath)) {
|
|
185
|
-
rows = readJsonl(jsonlPath);
|
|
186
|
-
} else {
|
|
187
|
-
return { buckets: [], sessions: [] };
|
|
188
|
-
}
|
|
189
|
-
} catch (err) {
|
|
190
|
-
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.');
|
|
192
|
-
}
|
|
193
|
-
throw err;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (!rows.length) return { buckets: [], sessions: [] };
|
|
197
|
-
|
|
198
|
-
const timeline = buildModelTimeline(base);
|
|
147
|
+
function rowsToLegacyEntries(rows) {
|
|
199
148
|
const entries = [];
|
|
200
149
|
for (const row of rows) {
|
|
201
150
|
const inputTokens = Math.max(0, Number(row.tokens_prompt) || 0);
|
|
@@ -203,21 +152,9 @@ export async function parse() {
|
|
|
203
152
|
if (inputTokens === 0 && outputTokens === 0) continue;
|
|
204
153
|
const timestamp = parseDbTimestamp(row.timestamp);
|
|
205
154
|
if (!timestamp) continue;
|
|
206
|
-
|
|
207
|
-
// Prefer the .chat timeline; fall back to the row's literal model (skip
|
|
208
|
-
// the placeholder "agent"); then "kiro-agent".
|
|
209
|
-
let model = normalizeModelName(resolveModel(timeline, timestamp));
|
|
210
|
-
if (!model) {
|
|
211
|
-
const literal = (row.model || '').trim();
|
|
212
|
-
if (literal && literal.toLowerCase() !== 'agent') {
|
|
213
|
-
model = normalizeModelName(literal);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (!model) model = 'kiro-agent';
|
|
217
|
-
|
|
218
155
|
entries.push({
|
|
219
156
|
source: 'kiro',
|
|
220
|
-
model,
|
|
157
|
+
model: normalizeLegacyModel(row.model),
|
|
221
158
|
project: 'unknown',
|
|
222
159
|
timestamp,
|
|
223
160
|
inputTokens,
|
|
@@ -226,6 +163,203 @@ export async function parse() {
|
|
|
226
163
|
reasoningOutputTokens: 0,
|
|
227
164
|
});
|
|
228
165
|
}
|
|
166
|
+
return entries;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseLogTimestamp(raw) {
|
|
170
|
+
const d = new Date(String(raw).replace(' ', 'T'));
|
|
171
|
+
return isNaN(d.getTime()) ? null : d;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseLogLine(line) {
|
|
175
|
+
const match = /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{3}) \[[^\]]+\] (\{.*\})$/.exec(line);
|
|
176
|
+
if (!match) return null;
|
|
177
|
+
const timestamp = parseLogTimestamp(match[1]);
|
|
178
|
+
if (!timestamp) return null;
|
|
179
|
+
try {
|
|
180
|
+
return { timestamp, obj: JSON.parse(match[2]) };
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function usageBreakdownsFromCommand(obj) {
|
|
187
|
+
if (obj?.commandName !== 'GetUsageLimitsCommand') return [];
|
|
188
|
+
const out = obj.output || {};
|
|
189
|
+
if (Array.isArray(out.usageBreakdownList)) return out.usageBreakdownList;
|
|
190
|
+
if (Array.isArray(out.usageBreakdowns)) return out.usageBreakdowns;
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function numberFrom(...values) {
|
|
195
|
+
for (const value of values) {
|
|
196
|
+
const n = Number(value);
|
|
197
|
+
if (Number.isFinite(n)) return n;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function maxNumberFrom(...values) {
|
|
203
|
+
const nums = values
|
|
204
|
+
.map(value => Number(value))
|
|
205
|
+
.filter(Number.isFinite);
|
|
206
|
+
if (nums.length === 0) return null;
|
|
207
|
+
return Math.max(...nums);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function snapshotFromBreakdown(timestamp, breakdown) {
|
|
211
|
+
const type = String(breakdown.resourceType || breakdown.type || '').toUpperCase();
|
|
212
|
+
const unit = String(breakdown.unit || '').toUpperCase();
|
|
213
|
+
if (type !== 'CREDIT' || unit !== 'INVOCATIONS') return null;
|
|
214
|
+
|
|
215
|
+
const currentUsage = maxNumberFrom(
|
|
216
|
+
breakdown.currentUsageWithPrecision,
|
|
217
|
+
breakdown.currentUsage,
|
|
218
|
+
breakdown.freeTrialInfo?.currentUsageWithPrecision,
|
|
219
|
+
breakdown.freeTrialInfo?.currentUsage,
|
|
220
|
+
breakdown.freeTrialUsage?.currentUsage,
|
|
221
|
+
);
|
|
222
|
+
if (currentUsage === null) return null;
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
timestamp,
|
|
226
|
+
currentUsage,
|
|
227
|
+
resetDate: String(breakdown.nextDateReset || breakdown.resetDate || ''),
|
|
228
|
+
usageLimit: numberFrom(
|
|
229
|
+
breakdown.usageLimitWithPrecision,
|
|
230
|
+
breakdown.usageLimit,
|
|
231
|
+
breakdown.freeTrialInfo?.usageLimitWithPrecision,
|
|
232
|
+
breakdown.freeTrialInfo?.usageLimit,
|
|
233
|
+
breakdown.freeTrialUsage?.usageLimit,
|
|
234
|
+
),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function findQClientLogs(logsRoot) {
|
|
239
|
+
const files = [];
|
|
240
|
+
const stack = [logsRoot];
|
|
241
|
+
while (stack.length) {
|
|
242
|
+
const dir = stack.pop();
|
|
243
|
+
let entries;
|
|
244
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
const p = join(dir, entry.name);
|
|
247
|
+
if (entry.isDirectory()) {
|
|
248
|
+
stack.push(p);
|
|
249
|
+
} else if (entry.isFile() && /^q-client\.log(?:\.\d+)?$/.test(entry.name)) {
|
|
250
|
+
files.push(p);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return files.sort();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function readLogSnapshots(logPath) {
|
|
258
|
+
const snapshots = [];
|
|
259
|
+
const rl = createInterface({
|
|
260
|
+
input: createReadStream(logPath, { encoding: 'utf-8' }),
|
|
261
|
+
crlfDelay: Infinity,
|
|
262
|
+
});
|
|
263
|
+
for await (const line of rl) {
|
|
264
|
+
const parsed = parseLogLine(line);
|
|
265
|
+
if (!parsed) continue;
|
|
266
|
+
for (const breakdown of usageBreakdownsFromCommand(parsed.obj)) {
|
|
267
|
+
const snapshot = snapshotFromBreakdown(parsed.timestamp, breakdown);
|
|
268
|
+
if (snapshot) snapshots.push(snapshot);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return snapshots;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function readUsageSnapshots(userPath) {
|
|
275
|
+
const appPath = dirname(userPath);
|
|
276
|
+
const logsRoot = join(appPath, 'logs');
|
|
277
|
+
const files = findQClientLogs(logsRoot);
|
|
278
|
+
const snapshots = [];
|
|
279
|
+
for (const file of files) {
|
|
280
|
+
try {
|
|
281
|
+
snapshots.push(...await readLogSnapshots(file));
|
|
282
|
+
} catch {
|
|
283
|
+
// skip unreadable / concurrently rotated logs
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return snapshots;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function dedupeSnapshots(snapshots) {
|
|
290
|
+
const map = new Map();
|
|
291
|
+
for (const s of snapshots) {
|
|
292
|
+
const key = `${s.timestamp.toISOString()}|${s.resetDate}|${s.currentUsage}`;
|
|
293
|
+
map.set(key, s);
|
|
294
|
+
}
|
|
295
|
+
return Array.from(map.values()).sort((a, b) => a.timestamp - b.timestamp);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function snapshotsToCreditEntries(snapshots) {
|
|
299
|
+
const ordered = dedupeSnapshots(snapshots);
|
|
300
|
+
const entries = [];
|
|
301
|
+
let prev = null;
|
|
302
|
+
|
|
303
|
+
for (const snapshot of ordered) {
|
|
304
|
+
if (!prev || snapshot.resetDate !== prev.resetDate || snapshot.currentUsage < prev.currentUsage) {
|
|
305
|
+
prev = snapshot;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const delta = Number((snapshot.currentUsage - prev.currentUsage).toFixed(4));
|
|
310
|
+
if (delta > 0) {
|
|
311
|
+
entries.push({
|
|
312
|
+
source: 'kiro',
|
|
313
|
+
model: CREDIT_MODEL,
|
|
314
|
+
project: 'unknown',
|
|
315
|
+
timestamp: snapshot.timestamp,
|
|
316
|
+
inputTokens: 0,
|
|
317
|
+
outputTokens: delta,
|
|
318
|
+
cachedInputTokens: 0,
|
|
319
|
+
reasoningOutputTokens: 0,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
prev = snapshot;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return entries;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function parseLegacyTokens(base) {
|
|
329
|
+
const dbPath = join(base, 'dev_data', 'devdata.sqlite');
|
|
330
|
+
const jsonlPath = join(base, 'dev_data', 'tokens_generated.jsonl');
|
|
331
|
+
let rows;
|
|
332
|
+
if (existsSync(dbPath)) {
|
|
333
|
+
rows = readLegacyDb(dbPath);
|
|
334
|
+
} else if (existsSync(jsonlPath)) {
|
|
335
|
+
rows = readLegacyJsonl(jsonlPath);
|
|
336
|
+
} else {
|
|
337
|
+
rows = [];
|
|
338
|
+
}
|
|
339
|
+
return rowsToLegacyEntries(rows);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function parse() {
|
|
343
|
+
const userPath = getKiroUserPath();
|
|
344
|
+
if (!userPath) return { buckets: [], sessions: [] };
|
|
345
|
+
|
|
346
|
+
const snapshots = await readUsageSnapshots(userPath);
|
|
347
|
+
const entries = snapshotsToCreditEntries(snapshots);
|
|
348
|
+
if (entries.length > 0) {
|
|
349
|
+
return { buckets: aggregateToBuckets(entries), sessions: [] };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Keep old token telemetry available for explicit debugging, but do not use
|
|
353
|
+
// it by default: it is not Kiro's billing source and causes false model rows.
|
|
354
|
+
if (process.env.VIBE_USAGE_KIRO_LEGACY_TOKENS === '1') {
|
|
355
|
+
const base = getKiroBasePath();
|
|
356
|
+
if (!base) return { buckets: [], sessions: [] };
|
|
357
|
+
const legacyEntries = parseLegacyTokens(base);
|
|
358
|
+
return { buckets: aggregateToBuckets(legacyEntries), sessions: [] };
|
|
359
|
+
}
|
|
229
360
|
|
|
230
|
-
|
|
361
|
+
// state.vscdb contains only the latest cumulative credit snapshot. The parser
|
|
362
|
+
// stays stateless, so a single cumulative point cannot be uploaded as a bucket
|
|
363
|
+
// without double-counting on later syncs.
|
|
364
|
+
return { buckets: [], sessions: [] };
|
|
231
365
|
}
|