@geoqiao/pi-usage 0.1.0

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.
Files changed (62) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +222 -0
  3. package/bin/pi-usage.js +69 -0
  4. package/data/models.dev-LICENSE +21 -0
  5. package/data/prices.json +2678 -0
  6. package/extensions/usage-report.js +36 -0
  7. package/package.json +51 -0
  8. package/src/analytics.js +189 -0
  9. package/src/collect.js +34 -0
  10. package/src/network.js +25 -0
  11. package/src/report.js +43 -0
  12. package/vendor/vibe-usage/NOTICE.md +58 -0
  13. package/vendor/vibe-usage/src/cindy-roots.js +85 -0
  14. package/vendor/vibe-usage/src/claude-roots.js +165 -0
  15. package/vendor/vibe-usage/src/cline-roots.js +40 -0
  16. package/vendor/vibe-usage/src/codex-roots.js +46 -0
  17. package/vendor/vibe-usage/src/craft-roots.js +15 -0
  18. package/vendor/vibe-usage/src/extra-roots.js +312 -0
  19. package/vendor/vibe-usage/src/parsers/aggregate.js +196 -0
  20. package/vendor/vibe-usage/src/parsers/alma.js +94 -0
  21. package/vendor/vibe-usage/src/parsers/amp.js +156 -0
  22. package/vendor/vibe-usage/src/parsers/antigravity-db.js +359 -0
  23. package/vendor/vibe-usage/src/parsers/antigravity.js +530 -0
  24. package/vendor/vibe-usage/src/parsers/cindy-ledger.js +157 -0
  25. package/vendor/vibe-usage/src/parsers/claude-code.js +372 -0
  26. package/vendor/vibe-usage/src/parsers/cline.js +92 -0
  27. package/vendor/vibe-usage/src/parsers/codex-cache.js +138 -0
  28. package/vendor/vibe-usage/src/parsers/codex.js +1198 -0
  29. package/vendor/vibe-usage/src/parsers/contract.js +55 -0
  30. package/vendor/vibe-usage/src/parsers/copilot-cli.js +128 -0
  31. package/vendor/vibe-usage/src/parsers/craft-agent.js +21 -0
  32. package/vendor/vibe-usage/src/parsers/cursor.js +262 -0
  33. package/vendor/vibe-usage/src/parsers/dimagent.js +127 -0
  34. package/vendor/vibe-usage/src/parsers/droid.js +113 -0
  35. package/vendor/vibe-usage/src/parsers/dsh.js +563 -0
  36. package/vendor/vibe-usage/src/parsers/fs-utils.js +36 -0
  37. package/vendor/vibe-usage/src/parsers/gemini-cli.js +190 -0
  38. package/vendor/vibe-usage/src/parsers/grok.js +395 -0
  39. package/vendor/vibe-usage/src/parsers/hermes.js +123 -0
  40. package/vendor/vibe-usage/src/parsers/index.js +61 -0
  41. package/vendor/vibe-usage/src/parsers/kimi-code.js +467 -0
  42. package/vendor/vibe-usage/src/parsers/kiro.js +788 -0
  43. package/vendor/vibe-usage/src/parsers/mcode.js +182 -0
  44. package/vendor/vibe-usage/src/parsers/mimocode.js +88 -0
  45. package/vendor/vibe-usage/src/parsers/omp.js +10 -0
  46. package/vendor/vibe-usage/src/parsers/openclaw.js +142 -0
  47. package/vendor/vibe-usage/src/parsers/opencode.js +151 -0
  48. package/vendor/vibe-usage/src/parsers/pi-coding-agent.js +27 -0
  49. package/vendor/vibe-usage/src/parsers/pi-session-jsonl.js +166 -0
  50. package/vendor/vibe-usage/src/parsers/qwen-code.js +122 -0
  51. package/vendor/vibe-usage/src/parsers/roo-code.js +123 -0
  52. package/vendor/vibe-usage/src/parsers/sqlite.js +148 -0
  53. package/vendor/vibe-usage/src/parsers/trae-cli.js +171 -0
  54. package/vendor/vibe-usage/src/parsers/workbuddy.js +322 -0
  55. package/vendor/vibe-usage/src/parsers/zcode.js +115 -0
  56. package/vendor/vibe-usage/src/pi-roots.js +125 -0
  57. package/vendor/vibe-usage/src/tools.js +422 -0
  58. package/vendor/vibe-usage/src/workbuddy-roots.js +22 -0
  59. package/vendor/vibe-usage/upstream-files.json +48 -0
  60. package/web/report.css +10 -0
  61. package/web/report.html +81 -0
  62. package/web/report.js +310 -0
@@ -0,0 +1,359 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { queryDbJsonSnapshotOnLock, sqliteUnavailableError, isSqliteUnavailableError } from './sqlite.js';
4
+
5
+ /**
6
+ * Offline reader for Antigravity SQLite conversation stores.
7
+ *
8
+ * Antigravity 2.0 (standalone app) and the `agy` CLI both persist each cascade
9
+ * as a per-conversation SQLite `.db` file whose `gen_metadata` table holds one
10
+ * protobuf-encoded GeneratorMetadata per row. Unlike the legacy `.pb` files
11
+ * (which are encrypted/opaque and only decodable by a running language server),
12
+ * these blobs are plain protobuf, so we can extract token usage directly from
13
+ * disk — no running process, no RPC.
14
+ *
15
+ * The wire-format tag numbers below were cross-verified against the language
16
+ * server's GetCascadeTrajectory JSON for the same responseId:
17
+ *
18
+ * chatModel = field 1
19
+ * usage = field 4
20
+ * inputTokens = 4.2
21
+ * outputTokens = 4.3
22
+ * cacheReadTokens = 4.5
23
+ * thinkingOutputTokens = 4.9
24
+ * responseId = 4.11
25
+ * chatStartMetadata = field 9
26
+ * createdAt (Timestamp) = 9.4 → seconds = 9.4.1
27
+ * responseModel = field 19 ("gemini-3-flash-a" / "gemini-default")
28
+ * modelDisplayName = field 21 ("Gemini 3.5 Flash (High/Medium/Low)")
29
+ *
30
+ * Gemini 3.7 CLI blobs still have usage (4) and responseModel (19) but omit
31
+ * 9.4 and 21. Timestamps are recovered from steps.metadata field 1.1 at the
32
+ * same gen_metadata.idx; the model name falls back to responseModel.
33
+ */
34
+
35
+ // ── Minimal protobuf wire-format decoder (no dependency) ──────────────
36
+
37
+ /** Read a base-128 varint. Returns [value: number, newPos]. */
38
+ function readVarint(buf, pos) {
39
+ let result = 0n;
40
+ let shift = 0n;
41
+ while (pos < buf.length) {
42
+ const byte = buf[pos++];
43
+ result |= BigInt(byte & 0x7f) << shift;
44
+ if ((byte & 0x80) === 0) break;
45
+ shift += 7n;
46
+ }
47
+ return [Number(result), pos];
48
+ }
49
+
50
+ /**
51
+ * Decode one protobuf message into Map<fieldNumber, Array<{wireType, value}>>.
52
+ * Length-delimited values are kept as raw Buffers (caller decides string vs
53
+ * sub-message). Unknown wire types abort parsing of the rest of the message.
54
+ */
55
+ function decodeMessage(buf) {
56
+ const fields = new Map();
57
+ let pos = 0;
58
+ while (pos < buf.length) {
59
+ let tag;
60
+ [tag, pos] = readVarint(buf, pos);
61
+ const fieldNum = tag >> 3;
62
+ const wireType = tag & 0x7;
63
+ let value;
64
+ if (wireType === 0) {
65
+ [value, pos] = readVarint(buf, pos);
66
+ } else if (wireType === 2) {
67
+ let len;
68
+ [len, pos] = readVarint(buf, pos);
69
+ value = buf.subarray(pos, pos + len);
70
+ pos += len;
71
+ } else if (wireType === 5) {
72
+ value = buf.subarray(pos, pos + 4);
73
+ pos += 4;
74
+ } else if (wireType === 1) {
75
+ value = buf.subarray(pos, pos + 8);
76
+ pos += 8;
77
+ } else {
78
+ break; // group/unknown — stop
79
+ }
80
+ if (!fields.has(fieldNum)) fields.set(fieldNum, []);
81
+ fields.get(fieldNum).push({ wireType, value });
82
+ }
83
+ return fields;
84
+ }
85
+
86
+ function firstVarint(fields, num) {
87
+ const arr = fields.get(num);
88
+ const e = arr && arr.find((x) => x.wireType === 0);
89
+ return e ? e.value : undefined;
90
+ }
91
+
92
+ function firstBytes(fields, num) {
93
+ const arr = fields.get(num);
94
+ const e = arr && arr.find((x) => x.wireType === 2);
95
+ return e ? e.value : undefined;
96
+ }
97
+
98
+ function firstString(fields, num) {
99
+ const b = firstBytes(fields, num);
100
+ return b ? Buffer.from(b).toString('utf-8') : undefined;
101
+ }
102
+
103
+ function firstMessage(fields, num) {
104
+ const b = firstBytes(fields, num);
105
+ return b ? decodeMessage(b) : undefined;
106
+ }
107
+
108
+ // ── GeneratorMetadata parsing ─────────────────────────────────────────
109
+
110
+ /**
111
+ * Parse one gen_metadata blob into a normalized usage record, or null if it
112
+ * carries no token usage (error/planning placeholders have none).
113
+ *
114
+ * @param {Buffer} buf raw protobuf bytes of a GeneratorMetadata row
115
+ * @returns {{inputTokens, outputTokens, cacheReadTokens, thinkingOutputTokens,
116
+ * responseId, timestamp: Date|null, displayName, responseModel}|null}
117
+ */
118
+ export function parseGenMetadataBlob(buf) {
119
+ const chatModel = firstMessage(decodeMessage(buf), 1);
120
+ if (!chatModel) return null;
121
+
122
+ const usage = firstMessage(chatModel, 4);
123
+ if (!usage) return null;
124
+
125
+ const inputTokens = firstVarint(usage, 2) || 0;
126
+ const outputTokens = firstVarint(usage, 3) || 0;
127
+ const cacheReadTokens = firstVarint(usage, 5) || 0;
128
+ const thinkingOutputTokens = firstVarint(usage, 9) || 0;
129
+ const responseId = firstString(usage, 11) || '';
130
+
131
+ // Skip rows with no real usage (errors, planning-only steps).
132
+ if (!inputTokens && !outputTokens && !cacheReadTokens && !thinkingOutputTokens) {
133
+ return null;
134
+ }
135
+
136
+ const chatStartMetadata = firstMessage(chatModel, 9);
137
+ const createdAt = chatStartMetadata ? firstMessage(chatStartMetadata, 4) : undefined;
138
+ const seconds = createdAt ? firstVarint(createdAt, 1) : undefined;
139
+ const timestamp = seconds ? new Date(seconds * 1000) : null;
140
+
141
+ return {
142
+ inputTokens,
143
+ outputTokens,
144
+ cacheReadTokens,
145
+ thinkingOutputTokens,
146
+ responseId,
147
+ timestamp,
148
+ displayName: firstString(chatModel, 21) || '',
149
+ responseModel: firstString(chatModel, 19) || '',
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Extract createdAt seconds from a steps.metadata blob, regardless of step
155
+ * source. Gemini 3.7 CLI gen_metadata blobs no longer carry chatStartMetadata
156
+ * (field 9.4); the step timestamp at field 1.1 is the remaining clock.
157
+ *
158
+ * @param {Buffer} buf
159
+ * @returns {Date|null}
160
+ */
161
+ export function parseStepTimestamp(buf) {
162
+ const meta = decodeMessage(buf);
163
+ const createdAt = firstMessage(meta, 1);
164
+ const seconds = createdAt ? firstVarint(createdAt, 1) : undefined;
165
+ if (!seconds) return null;
166
+ const timestamp = new Date(seconds * 1000);
167
+ return Number.isNaN(timestamp.getTime()) ? null : timestamp;
168
+ }
169
+
170
+ /**
171
+ * Prefer the blob's own createdAt; if Gemini 3.7 omitted it, use the step
172
+ * with the same idx. idx-join is exact on current CLI stores (every
173
+ * gen_metadata idx exists in steps).
174
+ *
175
+ * @param {{timestamp: Date|null, idx?: number}} rec
176
+ * @param {Map<number, Date>} stepTimestampsByIdx
177
+ * @returns {Date|null}
178
+ */
179
+ export function resolveUsageTimestamp(rec, stepTimestampsByIdx) {
180
+ if (rec?.timestamp && !Number.isNaN(rec.timestamp.getTime())) return rec.timestamp;
181
+ if (rec?.idx == null || !stepTimestampsByIdx) return null;
182
+ const stepTs = stepTimestampsByIdx.get(rec.idx);
183
+ if (stepTs && !Number.isNaN(stepTs.getTime())) return stepTs;
184
+ return null;
185
+ }
186
+
187
+ // ── SQLite store reading ──────────────────────────────────────────────
188
+
189
+ function queryCascadeDb(conversationsDir, cascadeId, sql) {
190
+ const dbPath = join(conversationsDir, `${cascadeId}.db`);
191
+ try {
192
+ // The App can hold the live DB open; queryDbJsonSnapshotOnLock copies the
193
+ // WAL set to a temp dir and retries on "database is locked" so one active
194
+ // cascade does not make the whole Antigravity parser go empty.
195
+ return queryDbJsonSnapshotOnLock(dbPath, sql, { tempPrefix: 'vibe-usage-antigravity-' });
196
+ } catch (err) {
197
+ if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('Antigravity');
198
+ throw err;
199
+ }
200
+ }
201
+
202
+ /** List cascade IDs backed by a `.db` file in a conversations directory. */
203
+ export function listDbCascades(conversationsDir, { strict = false } = {}) {
204
+ try {
205
+ const out = [];
206
+ for (const f of readdirSync(conversationsDir)) {
207
+ if (f.endsWith('.db') && f !== 'db.sqlite') out.push(f.slice(0, -3));
208
+ }
209
+ return out;
210
+ } catch (err) {
211
+ if (strict) throw err;
212
+ return [];
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Read all gen_metadata blobs from one cascade `.db` and return parsed usage
218
+ * records. blob is fetched as hex text so it round-trips through both the
219
+ * node:sqlite and sqlite3-CLI backends uniformly.
220
+ */
221
+ export function readDbUsageRecords(conversationsDir, cascadeId, { strict = false } = {}) {
222
+ let rows;
223
+ try {
224
+ rows = queryCascadeDb(conversationsDir, cascadeId, 'SELECT idx, hex(data) AS h FROM gen_metadata ORDER BY idx');
225
+ } catch (err) {
226
+ if (isSqliteUnavailableError(err) || strict) throw err;
227
+ return [];
228
+ }
229
+ const records = [];
230
+ for (const row of rows) {
231
+ if (!row.h) continue;
232
+ let rec;
233
+ try {
234
+ rec = parseGenMetadataBlob(Buffer.from(row.h, 'hex'));
235
+ } catch {
236
+ continue; // one malformed blob must not kill the rest
237
+ }
238
+ if (rec) {
239
+ rec.idx = Number.isFinite(Number(row.idx)) ? Number(row.idx) : null;
240
+ records.push(rec);
241
+ }
242
+ }
243
+ return records;
244
+ }
245
+
246
+ /**
247
+ * Map steps.idx → createdAt for every step that has a timestamp, including
248
+ * system/tool steps that parseStepMetadata skips. Used to timestamp 3.7
249
+ * gen_metadata rows that no longer embed chatStartMetadata.
250
+ */
251
+ export function readDbStepTimestamps(conversationsDir, cascadeId, { strict = false } = {}) {
252
+ let rows;
253
+ try {
254
+ rows = queryCascadeDb(
255
+ conversationsDir,
256
+ cascadeId,
257
+ 'SELECT idx, hex(metadata) AS h FROM steps WHERE metadata IS NOT NULL ORDER BY idx',
258
+ );
259
+ } catch (err) {
260
+ if (isSqliteUnavailableError(err) || strict) throw err;
261
+ return new Map();
262
+ }
263
+ const byIdx = new Map();
264
+ for (const row of rows) {
265
+ if (!row.h) continue;
266
+ let ts;
267
+ try {
268
+ ts = parseStepTimestamp(Buffer.from(row.h, 'hex'));
269
+ } catch {
270
+ continue;
271
+ }
272
+ if (!ts) continue;
273
+ const idx = Number(row.idx);
274
+ if (Number.isFinite(idx)) byIdx.set(idx, ts);
275
+ }
276
+ return byIdx;
277
+ }
278
+
279
+ /**
280
+ * Read the workspace URI for a cascade from trajectory_metadata_blob.
281
+ * Structure: field 1 = workspaces[0], 1.1 = workspaceFolderAbsoluteUri.
282
+ * Returns the raw file:// URI, or null.
283
+ */
284
+ export function readDbWorkspaceUri(conversationsDir, cascadeId) {
285
+ let rows;
286
+ try {
287
+ rows = queryCascadeDb(conversationsDir, cascadeId, 'SELECT hex(data) AS h FROM trajectory_metadata_blob LIMIT 1');
288
+ } catch (err) {
289
+ if (isSqliteUnavailableError(err)) throw err;
290
+ return null;
291
+ }
292
+ if (!rows.length || !rows[0].h) return null;
293
+ try {
294
+ const meta = decodeMessage(Buffer.from(rows[0].h, 'hex'));
295
+ const ws0 = firstMessage(meta, 1);
296
+ return (ws0 && firstString(ws0, 1)) || null;
297
+ } catch {
298
+ return null;
299
+ }
300
+ }
301
+
302
+ // Step source enum in steps.metadata field 3 (behavior-verified against payload
303
+ // contents, since it mirrors the RPC's CORTEX_STEP_SOURCE_*): 4 = user turn,
304
+ // 2 = model turn. Everything else (system, tool, unspecified) is skipped.
305
+ const STEP_SOURCE_USER = 4;
306
+ const STEP_SOURCE_MODEL = 2;
307
+
308
+ /**
309
+ * Parse one steps.metadata blob into a session-timing event, or null if the
310
+ * step is neither a user nor a model turn (system/tool/unspecified). createdAt
311
+ * is a Timestamp at field 1 (seconds at 1.1); source enum is field 3.
312
+ *
313
+ * @param {Buffer} buf
314
+ * @returns {{role:'user'|'assistant', timestamp: Date}|null}
315
+ */
316
+ export function parseStepMetadata(buf) {
317
+ const meta = decodeMessage(buf);
318
+ const source = firstVarint(meta, 3);
319
+ let role;
320
+ if (source === STEP_SOURCE_USER) role = 'user';
321
+ else if (source === STEP_SOURCE_MODEL) role = 'assistant';
322
+ else return null;
323
+
324
+ const createdAt = firstMessage(meta, 1);
325
+ const seconds = createdAt ? firstVarint(createdAt, 1) : undefined;
326
+ if (!seconds) return null;
327
+
328
+ return { role, timestamp: new Date(seconds * 1000) };
329
+ }
330
+
331
+ /**
332
+ * Read session timing events (user/assistant turns) for a cascade from the
333
+ * steps table, chronological by idx.
334
+ */
335
+ export function readDbSessionEvents(conversationsDir, cascadeId, { strict = false } = {}) {
336
+ let rows;
337
+ try {
338
+ rows = queryCascadeDb(
339
+ conversationsDir,
340
+ cascadeId,
341
+ 'SELECT hex(metadata) AS h FROM steps WHERE metadata IS NOT NULL ORDER BY idx',
342
+ );
343
+ } catch (err) {
344
+ if (isSqliteUnavailableError(err) || strict) throw err;
345
+ return [];
346
+ }
347
+ const events = [];
348
+ for (const row of rows) {
349
+ if (!row.h) continue;
350
+ let ev;
351
+ try {
352
+ ev = parseStepMetadata(Buffer.from(row.h, 'hex'));
353
+ } catch {
354
+ continue;
355
+ }
356
+ if (ev) events.push(ev);
357
+ }
358
+ return events;
359
+ }