@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.
- package/LICENSE +22 -0
- package/README.md +222 -0
- package/bin/pi-usage.js +69 -0
- package/data/models.dev-LICENSE +21 -0
- package/data/prices.json +2678 -0
- package/extensions/usage-report.js +36 -0
- package/package.json +51 -0
- package/src/analytics.js +189 -0
- package/src/collect.js +34 -0
- package/src/network.js +25 -0
- package/src/report.js +43 -0
- package/vendor/vibe-usage/NOTICE.md +58 -0
- package/vendor/vibe-usage/src/cindy-roots.js +85 -0
- package/vendor/vibe-usage/src/claude-roots.js +165 -0
- package/vendor/vibe-usage/src/cline-roots.js +40 -0
- package/vendor/vibe-usage/src/codex-roots.js +46 -0
- package/vendor/vibe-usage/src/craft-roots.js +15 -0
- package/vendor/vibe-usage/src/extra-roots.js +312 -0
- package/vendor/vibe-usage/src/parsers/aggregate.js +196 -0
- package/vendor/vibe-usage/src/parsers/alma.js +94 -0
- package/vendor/vibe-usage/src/parsers/amp.js +156 -0
- package/vendor/vibe-usage/src/parsers/antigravity-db.js +359 -0
- package/vendor/vibe-usage/src/parsers/antigravity.js +530 -0
- package/vendor/vibe-usage/src/parsers/cindy-ledger.js +157 -0
- package/vendor/vibe-usage/src/parsers/claude-code.js +372 -0
- package/vendor/vibe-usage/src/parsers/cline.js +92 -0
- package/vendor/vibe-usage/src/parsers/codex-cache.js +138 -0
- package/vendor/vibe-usage/src/parsers/codex.js +1198 -0
- package/vendor/vibe-usage/src/parsers/contract.js +55 -0
- package/vendor/vibe-usage/src/parsers/copilot-cli.js +128 -0
- package/vendor/vibe-usage/src/parsers/craft-agent.js +21 -0
- package/vendor/vibe-usage/src/parsers/cursor.js +262 -0
- package/vendor/vibe-usage/src/parsers/dimagent.js +127 -0
- package/vendor/vibe-usage/src/parsers/droid.js +113 -0
- package/vendor/vibe-usage/src/parsers/dsh.js +563 -0
- package/vendor/vibe-usage/src/parsers/fs-utils.js +36 -0
- package/vendor/vibe-usage/src/parsers/gemini-cli.js +190 -0
- package/vendor/vibe-usage/src/parsers/grok.js +395 -0
- package/vendor/vibe-usage/src/parsers/hermes.js +123 -0
- package/vendor/vibe-usage/src/parsers/index.js +61 -0
- package/vendor/vibe-usage/src/parsers/kimi-code.js +467 -0
- package/vendor/vibe-usage/src/parsers/kiro.js +788 -0
- package/vendor/vibe-usage/src/parsers/mcode.js +182 -0
- package/vendor/vibe-usage/src/parsers/mimocode.js +88 -0
- package/vendor/vibe-usage/src/parsers/omp.js +10 -0
- package/vendor/vibe-usage/src/parsers/openclaw.js +142 -0
- package/vendor/vibe-usage/src/parsers/opencode.js +151 -0
- package/vendor/vibe-usage/src/parsers/pi-coding-agent.js +27 -0
- package/vendor/vibe-usage/src/parsers/pi-session-jsonl.js +166 -0
- package/vendor/vibe-usage/src/parsers/qwen-code.js +122 -0
- package/vendor/vibe-usage/src/parsers/roo-code.js +123 -0
- package/vendor/vibe-usage/src/parsers/sqlite.js +148 -0
- package/vendor/vibe-usage/src/parsers/trae-cli.js +171 -0
- package/vendor/vibe-usage/src/parsers/workbuddy.js +322 -0
- package/vendor/vibe-usage/src/parsers/zcode.js +115 -0
- package/vendor/vibe-usage/src/pi-roots.js +125 -0
- package/vendor/vibe-usage/src/tools.js +422 -0
- package/vendor/vibe-usage/src/workbuddy-roots.js +22 -0
- package/vendor/vibe-usage/upstream-files.json +48 -0
- package/web/report.css +10 -0
- package/web/report.html +81 -0
- 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
|
+
}
|