@meterbility/collector 0.3.1

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/dist/index.js ADDED
@@ -0,0 +1,1475 @@
1
+ // src/store.ts
2
+ import { mkdir as mkdir2 } from "fs/promises";
3
+ import { mkdirSync } from "fs";
4
+ import { dirname as dirname2 } from "path";
5
+ import Database from "better-sqlite3";
6
+ import { dbPath, meterHome } from "@meterbility/shared";
7
+
8
+ // src/schema.ts
9
+ var SCHEMA_VERSION = 5;
10
+ function ensureSchema(db) {
11
+ db.pragma("journal_mode = WAL");
12
+ db.pragma("synchronous = NORMAL");
13
+ db.pragma("foreign_keys = ON");
14
+ db.exec(`
15
+ CREATE TABLE IF NOT EXISTS meta (
16
+ key TEXT PRIMARY KEY,
17
+ value TEXT NOT NULL
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS projects (
21
+ project_id TEXT PRIMARY KEY,
22
+ name TEXT NOT NULL,
23
+ cwd TEXT NOT NULL UNIQUE,
24
+ created_at TEXT NOT NULL
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS agents (
28
+ agent_id TEXT PRIMARY KEY,
29
+ project_id TEXT NOT NULL REFERENCES projects(project_id),
30
+ name TEXT NOT NULL,
31
+ created_at TEXT NOT NULL,
32
+ UNIQUE(project_id, name)
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS runs (
36
+ run_id TEXT PRIMARY KEY,
37
+ agent_id TEXT NOT NULL REFERENCES agents(agent_id),
38
+ project_id TEXT NOT NULL REFERENCES projects(project_id),
39
+ source_session_id TEXT,
40
+ source_runtime TEXT NOT NULL,
41
+ title TEXT,
42
+ status TEXT NOT NULL,
43
+ started_at TEXT NOT NULL,
44
+ ended_at TEXT,
45
+ git_branch TEXT,
46
+ cwd TEXT,
47
+ fork_origin_run_id TEXT REFERENCES runs(run_id),
48
+ fork_origin_step_id TEXT,
49
+ tokens_total_input INTEGER NOT NULL DEFAULT 0,
50
+ tokens_total_output INTEGER NOT NULL DEFAULT 0,
51
+ tokens_total_cached INTEGER NOT NULL DEFAULT 0,
52
+ cost_cents REAL NOT NULL DEFAULT 0,
53
+ step_count INTEGER NOT NULL DEFAULT 0,
54
+ tags TEXT NOT NULL DEFAULT '[]'
55
+ );
56
+
57
+ CREATE INDEX IF NOT EXISTS idx_runs_project_started
58
+ ON runs(project_id, started_at DESC);
59
+ CREATE INDEX IF NOT EXISTS idx_runs_session
60
+ ON runs(source_session_id);
61
+ CREATE INDEX IF NOT EXISTS idx_runs_fork_origin
62
+ ON runs(fork_origin_run_id);
63
+
64
+ CREATE TABLE IF NOT EXISTS steps (
65
+ step_id TEXT PRIMARY KEY,
66
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
67
+ parent_step_id TEXT,
68
+ fork_origin_id TEXT,
69
+ sequence INTEGER NOT NULL,
70
+ timestamp TEXT NOT NULL,
71
+ model TEXT NOT NULL,
72
+ context_snapshot_id TEXT NOT NULL,
73
+ decision_ref TEXT NOT NULL,
74
+ action_json TEXT NOT NULL,
75
+ outcome_json TEXT NOT NULL,
76
+ tokens_input INTEGER NOT NULL DEFAULT 0,
77
+ tokens_output INTEGER NOT NULL DEFAULT 0,
78
+ tokens_cached_read INTEGER NOT NULL DEFAULT 0,
79
+ tokens_cache_creation INTEGER NOT NULL DEFAULT 0,
80
+ tokens_cache_creation_1h INTEGER NOT NULL DEFAULT 0,
81
+ tokens_reasoning INTEGER,
82
+ latency_ms INTEGER NOT NULL DEFAULT 0,
83
+ cost_cents REAL NOT NULL DEFAULT 0,
84
+ status TEXT NOT NULL,
85
+ tags TEXT NOT NULL DEFAULT '[]',
86
+ UNIQUE(run_id, sequence)
87
+ );
88
+
89
+ CREATE INDEX IF NOT EXISTS idx_steps_run
90
+ ON steps(run_id, sequence);
91
+ CREATE INDEX IF NOT EXISTS idx_steps_context
92
+ ON steps(context_snapshot_id);
93
+
94
+ -- Stored context snapshots. The 'id' is the SHA256 of the canonicalized
95
+ -- snapshot JSON; the snapshot JSON itself lives in the blob store.
96
+ CREATE TABLE IF NOT EXISTS context_snapshots (
97
+ snapshot_id TEXT PRIMARY KEY,
98
+ blob_ref TEXT NOT NULL,
99
+ component_count INTEGER NOT NULL DEFAULT 0,
100
+ created_at TEXT NOT NULL
101
+ );
102
+
103
+ CREATE TABLE IF NOT EXISTS forks (
104
+ fork_id TEXT PRIMARY KEY,
105
+ origin_run_id TEXT NOT NULL REFERENCES runs(run_id),
106
+ origin_step_id TEXT NOT NULL,
107
+ fork_run_id TEXT NOT NULL REFERENCES runs(run_id),
108
+ edit_type TEXT NOT NULL,
109
+ edit_payload_json TEXT NOT NULL,
110
+ created_at TEXT NOT NULL
111
+ );
112
+
113
+ CREATE INDEX IF NOT EXISTS idx_forks_origin
114
+ ON forks(origin_run_id);
115
+
116
+ CREATE TABLE IF NOT EXISTS annotations (
117
+ annotation_id TEXT PRIMARY KEY,
118
+ target_kind TEXT NOT NULL,
119
+ target_id TEXT NOT NULL,
120
+ author TEXT NOT NULL,
121
+ verdict TEXT,
122
+ note TEXT,
123
+ kind TEXT NOT NULL DEFAULT 'comment'
124
+ CHECK (kind IN ('comment', 'probe_pause', 'probe_edit', 'capture_skipped')),
125
+ created_at TEXT NOT NULL
126
+ );
127
+
128
+ CREATE INDEX IF NOT EXISTS idx_annotations_target
129
+ ON annotations(target_kind, target_id);
130
+ -- idx_annotations_kind is created AFTER ensureColumn below so
131
+ -- legacy v4 DBs (which lack the kind column at executescript
132
+ -- time) don't trip on referencing a column that doesn't exist
133
+ -- yet. For fresh DBs the column is created above so the index
134
+ -- still fires on the same call path, just a few statements later.
135
+
136
+ -- Idempotency aid for the Claude Code adapter: remember the last byte
137
+ -- offset we ingested per session file so we can resume cheaply.
138
+ CREATE TABLE IF NOT EXISTS ingest_progress (
139
+ source_runtime TEXT NOT NULL,
140
+ source_path TEXT NOT NULL,
141
+ last_offset INTEGER NOT NULL,
142
+ last_ingested_at TEXT NOT NULL,
143
+ PRIMARY KEY (source_runtime, source_path)
144
+ );
145
+
146
+ CREATE TABLE IF NOT EXISTS settings (
147
+ key TEXT PRIMARY KEY,
148
+ value TEXT NOT NULL,
149
+ updated_at TEXT NOT NULL
150
+ );
151
+
152
+ CREATE TABLE IF NOT EXISTS redaction_log (
153
+ blob_ref TEXT NOT NULL,
154
+ rule TEXT NOT NULL,
155
+ count INTEGER NOT NULL,
156
+ created_at TEXT NOT NULL
157
+ );
158
+ CREATE INDEX IF NOT EXISTS idx_redaction_blob
159
+ ON redaction_log(blob_ref);
160
+
161
+ -- \u2500\u2500\u2500 v0.3 Track A: per-step file change capture \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
162
+ -- One row per file mutation attributed to a Step. Vendor-neutral \u2014
163
+ -- Claude Code hook adapter writes v0.3; Codex / SDK / proxy / file
164
+ -- watcher fill in additional source paths in v0.4. Path-keyed
165
+ -- queries are core (per v0.3 \xA73.1.2: "show me every change to
166
+ -- src/auth.ts across this run"), so indexes cover (run_id, path)
167
+ -- as well as the step join.
168
+ --
169
+ -- before_blob_ref / after_blob_ref are content addresses into the
170
+ -- blob store. NULL sentinels carry meaning: NULL before = 'create'
171
+ -- (or partial); NULL after = 'delete' (or partial). The
172
+ -- partial_diff flag distinguishes "we didn't capture this" from
173
+ -- "this side genuinely doesn't exist" so the UI can render the
174
+ -- right affordance.
175
+ --
176
+ -- CHECK constraints declare the full enum even where v0.3 only
177
+ -- writes a subset \u2014 see the additive-only note above the schema
178
+ -- version constant.
179
+ CREATE TABLE IF NOT EXISTS file_change (
180
+ file_change_id TEXT PRIMARY KEY,
181
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
182
+ step_id TEXT NOT NULL REFERENCES steps(step_id) ON DELETE CASCADE,
183
+ sequence INTEGER NOT NULL,
184
+ tool_call_id TEXT,
185
+ derived_from TEXT NOT NULL
186
+ CHECK (derived_from IN ('tool_call','filesystem_watch','git_diff')),
187
+ path TEXT NOT NULL,
188
+ old_path TEXT,
189
+ op TEXT NOT NULL
190
+ CHECK (op IN ('create','modify','delete','rename','chmod')),
191
+ before_blob_ref TEXT,
192
+ after_blob_ref TEXT,
193
+ partial_diff INTEGER NOT NULL DEFAULT 0,
194
+ gitignored INTEGER NOT NULL DEFAULT 0,
195
+ patch_text TEXT,
196
+ patch_format TEXT
197
+ CHECK (patch_format IN ('unified','binary','notebook_cell')
198
+ OR patch_format IS NULL),
199
+ encoding TEXT,
200
+ bom INTEGER NOT NULL DEFAULT 0,
201
+ line_endings TEXT,
202
+ mime TEXT,
203
+ language TEXT,
204
+ size_before INTEGER,
205
+ size_after INTEGER,
206
+ line_count_before INTEGER,
207
+ line_count_after INTEGER,
208
+ lines_added INTEGER NOT NULL DEFAULT 0,
209
+ lines_removed INTEGER NOT NULL DEFAULT 0,
210
+ mode_before INTEGER,
211
+ mode_after INTEGER,
212
+ source_tool_name TEXT,
213
+ source_tool_input TEXT,
214
+ redacted INTEGER NOT NULL DEFAULT 0,
215
+ normalizer_notes TEXT,
216
+ created_at TEXT NOT NULL,
217
+ UNIQUE(step_id, sequence)
218
+ );
219
+ CREATE INDEX IF NOT EXISTS idx_fc_run_step
220
+ ON file_change(run_id, step_id);
221
+ CREATE INDEX IF NOT EXISTS idx_fc_run_path
222
+ ON file_change(run_id, path);
223
+ CREATE INDEX IF NOT EXISTS idx_fc_step
224
+ ON file_change(step_id);
225
+ CREATE INDEX IF NOT EXISTS idx_fc_path_seq
226
+ ON file_change(run_id, path, step_id);
227
+
228
+ -- \u2500\u2500\u2500 v0.3 Track A: baseline working tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
229
+ -- Content-addressed snapshot of the project's files at Run start,
230
+ -- captured lazily on the first FileChange (per v0.3 \xA73.5). The
231
+ -- replay algorithm (\xA73.6) layers FileChange rows on top of this to
232
+ -- compute working_tree_at(run, step) in O(touched_paths) instead of
233
+ -- O(total_files_in_repo).
234
+ --
235
+ -- manifest_blob_ref points at a sorted, NUL-separated, newline-
236
+ -- delimited blob (each record is path + NUL + mode + NUL + blob_ref
237
+ -- followed by a newline). Sortedness gives byte-identical manifests
238
+ -- for identical trees, which dedups via SHA naturally \u2014 two runs
239
+ -- against the same git HEAD share one baseline blob.
240
+ --
241
+ -- git_head / git_dirty are advisory metadata only. Meterbility never
242
+ -- depends on git being present.
243
+ CREATE TABLE IF NOT EXISTS baseline_tree (
244
+ baseline_tree_id TEXT PRIMARY KEY,
245
+ project_id TEXT NOT NULL REFERENCES projects(project_id),
246
+ manifest_blob_ref TEXT NOT NULL,
247
+ git_head TEXT,
248
+ git_dirty INTEGER NOT NULL DEFAULT 0,
249
+ captured_at TEXT NOT NULL
250
+ );
251
+ CREATE INDEX IF NOT EXISTS idx_bt_project
252
+ ON baseline_tree(project_id);
253
+ CREATE INDEX IF NOT EXISTS idx_bt_git
254
+ ON baseline_tree(project_id, git_head);
255
+
256
+ -- Regression suite (v0.1). A test = name + assertion list.
257
+ -- Optionally references a canonical run that originated the test.
258
+ CREATE TABLE IF NOT EXISTS regression_tests (
259
+ test_id TEXT PRIMARY KEY,
260
+ name TEXT NOT NULL UNIQUE,
261
+ description TEXT,
262
+ assertions_json TEXT NOT NULL,
263
+ canonical_run_id TEXT REFERENCES runs(run_id),
264
+ created_at TEXT NOT NULL
265
+ );
266
+
267
+ CREATE TABLE IF NOT EXISTS regression_results (
268
+ result_id TEXT PRIMARY KEY,
269
+ test_id TEXT NOT NULL REFERENCES regression_tests(test_id) ON DELETE CASCADE,
270
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
271
+ passed INTEGER NOT NULL,
272
+ details_json TEXT NOT NULL,
273
+ created_at TEXT NOT NULL
274
+ );
275
+ CREATE INDEX IF NOT EXISTS idx_regression_results_test
276
+ ON regression_results(test_id, created_at DESC);
277
+ `);
278
+ ensureColumn(
279
+ db,
280
+ "steps",
281
+ "tokens_cache_creation_1h",
282
+ "INTEGER NOT NULL DEFAULT 0"
283
+ );
284
+ ensureColumn(db, "runs", "baseline_tree_id", "TEXT");
285
+ ensureColumn(db, "runs", "probe_state", "TEXT");
286
+ ensureColumn(
287
+ db,
288
+ "annotations",
289
+ "kind",
290
+ "TEXT NOT NULL DEFAULT 'comment'"
291
+ );
292
+ db.exec(
293
+ "CREATE INDEX IF NOT EXISTS idx_annotations_kind ON annotations(kind)"
294
+ );
295
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
296
+ if (!row) {
297
+ db.prepare("INSERT INTO meta(key,value) VALUES(?,?)").run(
298
+ "schema_version",
299
+ String(SCHEMA_VERSION)
300
+ );
301
+ } else if (Number(row.value) < SCHEMA_VERSION) {
302
+ db.prepare("UPDATE meta SET value = ? WHERE key = 'schema_version'").run(
303
+ String(SCHEMA_VERSION)
304
+ );
305
+ }
306
+ }
307
+ function ensureColumn(db, table, column, ddl) {
308
+ const cols = db.pragma(`table_info(${table})`);
309
+ if (cols.find((c) => c.name === column)) return;
310
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddl}`);
311
+ }
312
+
313
+ // src/blobs.ts
314
+ import { mkdir, writeFile, readFile, stat } from "fs/promises";
315
+ import { dirname } from "path";
316
+ import {
317
+ blobPath,
318
+ blobRoot,
319
+ hashJson,
320
+ redactBuffer,
321
+ sha256
322
+ } from "@meterbility/shared";
323
+ function isProbablyText(buf) {
324
+ for (let i = 0; i < buf.length; i++) {
325
+ if (buf[i] === 0) return false;
326
+ }
327
+ if (buf.length > 0 && Buffer.byteLength(buf.toString("utf-8"), "utf-8") !== buf.length) {
328
+ return false;
329
+ }
330
+ return true;
331
+ }
332
+ var BlobStore = class {
333
+ constructor(db) {
334
+ this.db = db;
335
+ }
336
+ db;
337
+ async putString(content, opts) {
338
+ return this.putBuffer(Buffer.from(content, "utf-8"), opts);
339
+ }
340
+ async putJson(value, opts) {
341
+ return this.putString(JSON.stringify(value), opts);
342
+ }
343
+ /**
344
+ * Hash without persisting — useful when you want a snapshot id before
345
+ * deciding whether to write.
346
+ */
347
+ hashJson(value) {
348
+ return hashJson(value);
349
+ }
350
+ async putBuffer(buf, opts) {
351
+ const shouldSkip = opts?.skipRedact === true || !isProbablyText(buf);
352
+ const { buffer: scrubbed, redactions } = shouldSkip ? { buffer: buf, redactions: [] } : redactBuffer(buf);
353
+ const hash = sha256(scrubbed);
354
+ const path = blobPath(hash);
355
+ try {
356
+ await stat(path);
357
+ return hash;
358
+ } catch {
359
+ }
360
+ await mkdir(dirname(path), { recursive: true });
361
+ await writeFile(path, scrubbed);
362
+ if (redactions.length) {
363
+ const stmt = this.db.prepare(
364
+ "INSERT INTO redaction_log(blob_ref, rule, count, created_at) VALUES (?,?,?,?)"
365
+ );
366
+ const now = (/* @__PURE__ */ new Date()).toISOString();
367
+ for (const r of redactions) stmt.run(hash, r.rule, r.count, now);
368
+ }
369
+ return hash;
370
+ }
371
+ async getString(hash) {
372
+ const buf = await this.getBuffer(hash);
373
+ return buf.toString("utf-8");
374
+ }
375
+ async getJson(hash) {
376
+ const text = await this.getString(hash);
377
+ return JSON.parse(text);
378
+ }
379
+ async getBuffer(hash) {
380
+ return readFile(blobPath(hash));
381
+ }
382
+ async tryGetString(hash) {
383
+ try {
384
+ return await this.getString(hash);
385
+ } catch {
386
+ return void 0;
387
+ }
388
+ }
389
+ /**
390
+ * Binary-safe variant of `tryGetString`. Used by /api/blob/:hash/render
391
+ * which has to look at raw bytes for MIME sniffing before deciding
392
+ * whether to render text or serve as image/octet-stream.
393
+ */
394
+ async tryGetBuffer(hash) {
395
+ try {
396
+ return await this.getBuffer(hash);
397
+ } catch {
398
+ return void 0;
399
+ }
400
+ }
401
+ /**
402
+ * Write pre-computed HTML (or other text) under a caller-chosen
403
+ * synthetic key, bypassing the content-addressed hash. Used by the
404
+ * blob_render cache to store rendered HTML under a key derived
405
+ * from `sha(blob_hash + lang + RENDER_VERSION)` rather than the
406
+ * content's own hash — this way two different renders of the same
407
+ * source code (e.g. lang=auto vs lang=typescript) cache separately
408
+ * but predictably.
409
+ *
410
+ * Skips redaction (renders are derived from already-stored blobs
411
+ * that went through redaction at put time).
412
+ */
413
+ async putWithKey(content, key) {
414
+ const path = blobPath(key);
415
+ try {
416
+ await stat(path);
417
+ return;
418
+ } catch {
419
+ }
420
+ await mkdir(dirname(path), { recursive: true });
421
+ await writeFile(path, Buffer.from(content, "utf-8"));
422
+ }
423
+ rootDir() {
424
+ return blobRoot();
425
+ }
426
+ };
427
+
428
+ // src/store.ts
429
+ var Store = class _Store {
430
+ db;
431
+ blobs;
432
+ constructor(db) {
433
+ this.db = db;
434
+ this.blobs = new BlobStore(db);
435
+ }
436
+ static open(opts) {
437
+ const path = opts?.path ?? dbPath();
438
+ mkdirSync(meterHome(), { recursive: true });
439
+ mkdirSync(dirname2(path), { recursive: true });
440
+ const db = new Database(path);
441
+ ensureSchema(db);
442
+ return new _Store(db);
443
+ }
444
+ static async openAsync(opts) {
445
+ const path = opts?.path ?? dbPath();
446
+ await mkdir2(meterHome(), { recursive: true });
447
+ await mkdir2(dirname2(path), { recursive: true });
448
+ return this.open(opts);
449
+ }
450
+ close() {
451
+ this.db.close();
452
+ }
453
+ };
454
+
455
+ // src/queries.ts
456
+ import { randomUUID } from "crypto";
457
+ function rowToRun(row) {
458
+ return {
459
+ run_id: row.run_id,
460
+ agent_id: row.agent_id,
461
+ project_id: row.project_id,
462
+ source_session_id: row.source_session_id ?? void 0,
463
+ source_runtime: row.source_runtime,
464
+ title: row.title ?? void 0,
465
+ status: row.status,
466
+ started_at: row.started_at,
467
+ ended_at: row.ended_at ?? void 0,
468
+ git_branch: row.git_branch ?? void 0,
469
+ cwd: row.cwd ?? void 0,
470
+ fork_origin_run_id: row.fork_origin_run_id ?? void 0,
471
+ fork_origin_step_id: row.fork_origin_step_id ?? void 0,
472
+ tokens_total_input: row.tokens_total_input,
473
+ tokens_total_output: row.tokens_total_output,
474
+ tokens_total_cached: row.tokens_total_cached,
475
+ cost_cents: row.cost_cents,
476
+ step_count: row.step_count,
477
+ tags: JSON.parse(row.tags),
478
+ baseline_tree_id: row.baseline_tree_id ?? void 0,
479
+ probe_state: row.probe_state === "paused" || row.probe_state === "resumed" ? row.probe_state : void 0
480
+ };
481
+ }
482
+ function rowToStep(row) {
483
+ return {
484
+ step_id: row.step_id,
485
+ run_id: row.run_id,
486
+ parent_step_id: row.parent_step_id ?? void 0,
487
+ fork_origin_id: row.fork_origin_id ?? void 0,
488
+ sequence: row.sequence,
489
+ timestamp: row.timestamp,
490
+ model: row.model,
491
+ context_snapshot_id: row.context_snapshot_id,
492
+ decision_ref: row.decision_ref,
493
+ action: JSON.parse(row.action_json),
494
+ outcome: JSON.parse(row.outcome_json),
495
+ tokens: {
496
+ input: row.tokens_input,
497
+ output: row.tokens_output,
498
+ cached_read: row.tokens_cached_read,
499
+ cache_creation: row.tokens_cache_creation,
500
+ cache_creation_1h: row.tokens_cache_creation_1h ?? 0,
501
+ reasoning: row.tokens_reasoning ?? void 0
502
+ },
503
+ latency_ms: row.latency_ms,
504
+ cost_cents: row.cost_cents,
505
+ tags: JSON.parse(row.tags),
506
+ status: row.status
507
+ };
508
+ }
509
+ function upsertProjectByCwd(store, cwd, name) {
510
+ const existing = store.db.prepare("SELECT * FROM projects WHERE cwd = ?").get(cwd);
511
+ if (existing) return existing;
512
+ const project = {
513
+ project_id: `prj_${randomUUID()}`,
514
+ cwd,
515
+ name: name ?? cwd.split("/").pop() ?? cwd,
516
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
517
+ };
518
+ store.db.prepare(
519
+ "INSERT INTO projects(project_id, name, cwd, created_at) VALUES (?,?,?,?)"
520
+ ).run(project.project_id, project.name, project.cwd, project.created_at);
521
+ return project;
522
+ }
523
+ function upsertAgent(store, projectId, name) {
524
+ const existing = store.db.prepare("SELECT * FROM agents WHERE project_id = ? AND name = ?").get(projectId, name);
525
+ if (existing) return existing;
526
+ const agent = {
527
+ agent_id: `agt_${randomUUID()}`,
528
+ project_id: projectId,
529
+ name,
530
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
531
+ };
532
+ store.db.prepare("INSERT INTO agents(agent_id, project_id, name, created_at) VALUES (?,?,?,?)").run(agent.agent_id, agent.project_id, agent.name, agent.created_at);
533
+ return agent;
534
+ }
535
+ function insertRun(store, run) {
536
+ store.db.prepare(
537
+ `INSERT INTO runs(
538
+ run_id, agent_id, project_id, source_session_id, source_runtime,
539
+ title, status, started_at, ended_at, git_branch, cwd,
540
+ fork_origin_run_id, fork_origin_step_id,
541
+ tokens_total_input, tokens_total_output, tokens_total_cached,
542
+ cost_cents, step_count, tags,
543
+ baseline_tree_id, probe_state
544
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
545
+ ).run(
546
+ run.run_id,
547
+ run.agent_id,
548
+ run.project_id,
549
+ run.source_session_id ?? null,
550
+ run.source_runtime,
551
+ run.title ?? null,
552
+ run.status,
553
+ run.started_at,
554
+ run.ended_at ?? null,
555
+ run.git_branch ?? null,
556
+ run.cwd ?? null,
557
+ run.fork_origin_run_id ?? null,
558
+ run.fork_origin_step_id ?? null,
559
+ run.tokens_total_input,
560
+ run.tokens_total_output,
561
+ run.tokens_total_cached,
562
+ run.cost_cents,
563
+ run.step_count,
564
+ JSON.stringify(run.tags ?? []),
565
+ run.baseline_tree_id ?? null,
566
+ run.probe_state ?? null
567
+ );
568
+ }
569
+ function updateRunTotals(store, runId) {
570
+ store.db.prepare(
571
+ `UPDATE runs SET
572
+ step_count = (SELECT COUNT(*) FROM steps WHERE run_id = runs.run_id),
573
+ tokens_total_input = COALESCE(
574
+ (SELECT SUM(tokens_input) FROM steps WHERE run_id = runs.run_id), 0),
575
+ tokens_total_output = COALESCE(
576
+ (SELECT SUM(tokens_output) FROM steps WHERE run_id = runs.run_id), 0),
577
+ tokens_total_cached = COALESCE(
578
+ (SELECT SUM(tokens_cached_read + tokens_cache_creation + tokens_cache_creation_1h)
579
+ FROM steps WHERE run_id = runs.run_id), 0),
580
+ cost_cents = COALESCE(
581
+ (SELECT SUM(cost_cents) FROM steps WHERE run_id = runs.run_id), 0)
582
+ WHERE run_id = ?`
583
+ ).run(runId);
584
+ }
585
+ function setRunStatus(store, runId, status, endedAt) {
586
+ store.db.prepare("UPDATE runs SET status = ?, ended_at = ? WHERE run_id = ?").run(status, endedAt ?? null, runId);
587
+ }
588
+ function insertStep(store, step) {
589
+ store.db.prepare(
590
+ `INSERT OR REPLACE INTO steps(
591
+ step_id, run_id, parent_step_id, fork_origin_id, sequence, timestamp,
592
+ model, context_snapshot_id, decision_ref, action_json, outcome_json,
593
+ tokens_input, tokens_output, tokens_cached_read, tokens_cache_creation,
594
+ tokens_cache_creation_1h, tokens_reasoning, latency_ms, cost_cents,
595
+ status, tags
596
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
597
+ ).run(
598
+ step.step_id,
599
+ step.run_id,
600
+ step.parent_step_id ?? null,
601
+ step.fork_origin_id ?? null,
602
+ step.sequence,
603
+ step.timestamp,
604
+ step.model,
605
+ step.context_snapshot_id,
606
+ step.decision_ref,
607
+ JSON.stringify(step.action),
608
+ JSON.stringify(step.outcome),
609
+ step.tokens.input,
610
+ step.tokens.output,
611
+ step.tokens.cached_read,
612
+ step.tokens.cache_creation,
613
+ step.tokens.cache_creation_1h ?? 0,
614
+ step.tokens.reasoning ?? null,
615
+ step.latency_ms,
616
+ step.cost_cents,
617
+ step.status,
618
+ JSON.stringify(step.tags ?? [])
619
+ );
620
+ }
621
+ function recordContextSnapshot(store, snapshotId, blobRef, componentCount) {
622
+ store.db.prepare(
623
+ `INSERT OR IGNORE INTO context_snapshots(snapshot_id, blob_ref, component_count, created_at)
624
+ VALUES (?,?,?,?)`
625
+ ).run(snapshotId, blobRef, componentCount, (/* @__PURE__ */ new Date()).toISOString());
626
+ }
627
+ function resolveSnapshotBlobRef(store, snapshotId) {
628
+ const row = store.db.prepare("SELECT blob_ref FROM context_snapshots WHERE snapshot_id = ?").get(snapshotId);
629
+ return row?.blob_ref ?? snapshotId;
630
+ }
631
+ function getRun(store, runId) {
632
+ const exact = store.db.prepare("SELECT * FROM runs WHERE run_id = ?").get(runId);
633
+ if (exact) return rowToRun(exact);
634
+ if (runId.length < 6) return void 0;
635
+ const prefix = store.db.prepare("SELECT * FROM runs WHERE run_id LIKE ? LIMIT 2").all(`${runId}%`);
636
+ if (prefix.length === 1) return rowToRun(prefix[0]);
637
+ return void 0;
638
+ }
639
+ function listRuns(store, opts = {}) {
640
+ const params = [];
641
+ let where = "1=1";
642
+ if (opts.projectId) {
643
+ where += " AND project_id = ?";
644
+ params.push(opts.projectId);
645
+ }
646
+ if (opts.agentId) {
647
+ where += " AND agent_id = ?";
648
+ params.push(opts.agentId);
649
+ }
650
+ if (opts.status) {
651
+ where += " AND status = ?";
652
+ params.push(opts.status);
653
+ }
654
+ if (opts.containsTool) {
655
+ where += " AND run_id IN (SELECT run_id FROM steps WHERE action_json LIKE ?)";
656
+ params.push(`%"tool_name":"${opts.containsTool}"%`);
657
+ }
658
+ const limit = opts.limit ?? 50;
659
+ const rows = store.db.prepare(
660
+ `SELECT * FROM runs WHERE ${where} ORDER BY started_at DESC LIMIT ?`
661
+ ).all(...params, limit);
662
+ return rows.map(rowToRun);
663
+ }
664
+ function listSteps(store, runId) {
665
+ const rows = store.db.prepare("SELECT * FROM steps WHERE run_id = ? ORDER BY sequence ASC").all(runId);
666
+ return rows.map(rowToStep);
667
+ }
668
+ function getStep(store, stepId) {
669
+ const exact = store.db.prepare("SELECT * FROM steps WHERE step_id = ?").get(stepId);
670
+ if (exact) return rowToStep(exact);
671
+ if (stepId.length < 6) return void 0;
672
+ const prefix = store.db.prepare("SELECT * FROM steps WHERE step_id LIKE ? LIMIT 2").all(`${stepId}%`);
673
+ if (prefix.length === 1) return rowToStep(prefix[0]);
674
+ return void 0;
675
+ }
676
+ function getStepBySequence(store, runId, sequence) {
677
+ const row = store.db.prepare("SELECT * FROM steps WHERE run_id = ? AND sequence = ?").get(runId, sequence);
678
+ return row ? rowToStep(row) : void 0;
679
+ }
680
+ function insertFork(store, args) {
681
+ const fork_id = `frk_${randomUUID()}`;
682
+ store.db.prepare(
683
+ `INSERT INTO forks(fork_id, origin_run_id, origin_step_id, fork_run_id,
684
+ edit_type, edit_payload_json, created_at)
685
+ VALUES (?,?,?,?,?,?,?)`
686
+ ).run(
687
+ fork_id,
688
+ args.originRunId,
689
+ args.originStepId,
690
+ args.forkRunId,
691
+ args.edit.type,
692
+ JSON.stringify(args.edit.payload ?? null),
693
+ (/* @__PURE__ */ new Date()).toISOString()
694
+ );
695
+ return fork_id;
696
+ }
697
+ function listForks(store, originRunId) {
698
+ return store.db.prepare(
699
+ "SELECT fork_id, origin_run_id, origin_step_id, fork_run_id, edit_type, created_at FROM forks WHERE origin_run_id = ? ORDER BY created_at"
700
+ ).all(originRunId);
701
+ }
702
+ function insertAnnotation(store, args) {
703
+ const ann = {
704
+ annotation_id: `ann_${randomUUID()}`,
705
+ target_kind: args.targetKind,
706
+ target_id: args.targetId,
707
+ author: args.author,
708
+ kind: args.kind ?? "comment",
709
+ verdict: args.verdict,
710
+ note: args.note,
711
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
712
+ };
713
+ store.db.prepare(
714
+ `INSERT INTO annotations(annotation_id, target_kind, target_id, author, kind, verdict, note, created_at)
715
+ VALUES (?,?,?,?,?,?,?,?)`
716
+ ).run(
717
+ ann.annotation_id,
718
+ ann.target_kind,
719
+ ann.target_id,
720
+ ann.author,
721
+ ann.kind,
722
+ ann.verdict ?? null,
723
+ ann.note ?? null,
724
+ ann.created_at
725
+ );
726
+ return ann;
727
+ }
728
+ function listAnnotations(store, targetKind, targetId) {
729
+ const rows = store.db.prepare(
730
+ "SELECT * FROM annotations WHERE target_kind = ? AND target_id = ? ORDER BY created_at"
731
+ ).all(targetKind, targetId);
732
+ return rows.map((r) => ({
733
+ annotation_id: r.annotation_id,
734
+ target_kind: r.target_kind,
735
+ target_id: r.target_id,
736
+ author: r.author,
737
+ kind: r.kind,
738
+ verdict: r.verdict ?? void 0,
739
+ note: r.note ?? void 0,
740
+ created_at: r.created_at
741
+ }));
742
+ }
743
+ function getIngestOffset(store, source_runtime, source_path) {
744
+ const row = store.db.prepare(
745
+ "SELECT last_offset FROM ingest_progress WHERE source_runtime = ? AND source_path = ?"
746
+ ).get(source_runtime, source_path);
747
+ return row?.last_offset ?? 0;
748
+ }
749
+ function setIngestOffset(store, source_runtime, source_path, offset) {
750
+ store.db.prepare(
751
+ `INSERT INTO ingest_progress(source_runtime, source_path, last_offset, last_ingested_at)
752
+ VALUES(?,?,?,?)
753
+ ON CONFLICT(source_runtime, source_path) DO UPDATE SET
754
+ last_offset = excluded.last_offset,
755
+ last_ingested_at = excluded.last_ingested_at`
756
+ ).run(source_runtime, source_path, offset, (/* @__PURE__ */ new Date()).toISOString());
757
+ }
758
+ function getRunBySessionId(store, sourceSessionId) {
759
+ const row = store.db.prepare("SELECT * FROM runs WHERE source_session_id = ? ORDER BY started_at DESC LIMIT 1").get(sourceSessionId);
760
+ return row ? rowToRun(row) : void 0;
761
+ }
762
+ function aggregateTokens(steps) {
763
+ return steps.reduce(
764
+ (acc, s) => ({
765
+ input: acc.input + s.tokens.input,
766
+ output: acc.output + s.tokens.output,
767
+ cached_read: acc.cached_read + s.tokens.cached_read,
768
+ cache_creation: acc.cache_creation + s.tokens.cache_creation,
769
+ cache_creation_1h: (acc.cache_creation_1h ?? 0) + (s.tokens.cache_creation_1h ?? 0)
770
+ }),
771
+ {
772
+ input: 0,
773
+ output: 0,
774
+ cached_read: 0,
775
+ cache_creation: 0,
776
+ cache_creation_1h: 0
777
+ }
778
+ );
779
+ }
780
+ function rowToFileChange(row) {
781
+ return {
782
+ file_change_id: row.file_change_id,
783
+ run_id: row.run_id,
784
+ step_id: row.step_id,
785
+ sequence: row.sequence,
786
+ tool_call_id: row.tool_call_id ?? void 0,
787
+ derived_from: row.derived_from,
788
+ path: row.path,
789
+ old_path: row.old_path ?? void 0,
790
+ op: row.op,
791
+ before_blob_ref: row.before_blob_ref ?? void 0,
792
+ after_blob_ref: row.after_blob_ref ?? void 0,
793
+ partial_diff: row.partial_diff === 1,
794
+ gitignored: row.gitignored === 1,
795
+ patch_text: row.patch_text ?? void 0,
796
+ patch_format: row.patch_format ?? void 0,
797
+ encoding: row.encoding ?? void 0,
798
+ bom: row.bom === 1,
799
+ line_endings: row.line_endings ?? void 0,
800
+ mime: row.mime ?? void 0,
801
+ language: row.language ?? void 0,
802
+ size_before: row.size_before ?? void 0,
803
+ size_after: row.size_after ?? void 0,
804
+ line_count_before: row.line_count_before ?? void 0,
805
+ line_count_after: row.line_count_after ?? void 0,
806
+ lines_added: row.lines_added,
807
+ lines_removed: row.lines_removed,
808
+ mode_before: row.mode_before ?? void 0,
809
+ mode_after: row.mode_after ?? void 0,
810
+ source_tool_name: row.source_tool_name ?? void 0,
811
+ source_tool_input: row.source_tool_input ? safeJsonParse(row.source_tool_input) : void 0,
812
+ redacted: row.redacted === 1,
813
+ normalizer_notes: row.normalizer_notes ? safeJsonParse(row.normalizer_notes) : void 0,
814
+ created_at: row.created_at
815
+ };
816
+ }
817
+ function safeJsonParse(s) {
818
+ try {
819
+ return JSON.parse(s);
820
+ } catch {
821
+ return s;
822
+ }
823
+ }
824
+ var FileChangeInvariantError = class extends Error {
825
+ constructor(message) {
826
+ super(`FileChange invariant violation: ${message}`);
827
+ this.name = "FileChangeInvariantError";
828
+ }
829
+ };
830
+ function assertFileChangeInvariants(fc) {
831
+ if (fc.op === "chmod") return;
832
+ if (fc.partial_diff) {
833
+ if (fc.before_blob_ref !== void 0) {
834
+ throw new FileChangeInvariantError(
835
+ `partial_diff=true requires before_blob_ref to be null (got ${JSON.stringify(fc.before_blob_ref)})`
836
+ );
837
+ }
838
+ if (fc.after_blob_ref !== void 0) {
839
+ throw new FileChangeInvariantError(
840
+ `partial_diff=true requires after_blob_ref to be null (got ${JSON.stringify(fc.after_blob_ref)})`
841
+ );
842
+ }
843
+ return;
844
+ }
845
+ if (fc.op === "create") {
846
+ if (fc.before_blob_ref !== void 0) {
847
+ throw new FileChangeInvariantError(
848
+ "op='create' requires before_blob_ref to be null"
849
+ );
850
+ }
851
+ if (fc.after_blob_ref === void 0) {
852
+ throw new FileChangeInvariantError(
853
+ "op='create' requires after_blob_ref to be set"
854
+ );
855
+ }
856
+ return;
857
+ }
858
+ if (fc.op === "delete") {
859
+ if (fc.before_blob_ref === void 0) {
860
+ throw new FileChangeInvariantError(
861
+ "op='delete' requires before_blob_ref to be set"
862
+ );
863
+ }
864
+ if (fc.after_blob_ref !== void 0) {
865
+ throw new FileChangeInvariantError(
866
+ "op='delete' requires after_blob_ref to be null"
867
+ );
868
+ }
869
+ return;
870
+ }
871
+ if (fc.before_blob_ref === void 0) {
872
+ throw new FileChangeInvariantError(
873
+ `op='${fc.op}' requires before_blob_ref to be set (use partial_diff=true if not captured)`
874
+ );
875
+ }
876
+ if (fc.after_blob_ref === void 0) {
877
+ throw new FileChangeInvariantError(
878
+ `op='${fc.op}' requires after_blob_ref to be set (use partial_diff=true if not captured)`
879
+ );
880
+ }
881
+ if (fc.op === "rename" && fc.old_path === void 0) {
882
+ throw new FileChangeInvariantError("op='rename' requires old_path to be set");
883
+ }
884
+ }
885
+ function insertFileChange(store, fc) {
886
+ assertFileChangeInvariants(fc);
887
+ const id = fc.file_change_id ?? `fc_${randomUUID()}`;
888
+ const created = fc.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
889
+ store.db.prepare(
890
+ `INSERT INTO file_change(
891
+ file_change_id, run_id, step_id, sequence,
892
+ tool_call_id, derived_from, path, old_path, op,
893
+ before_blob_ref, after_blob_ref,
894
+ partial_diff, gitignored,
895
+ patch_text, patch_format,
896
+ encoding, bom, line_endings, mime, language,
897
+ size_before, size_after, line_count_before, line_count_after,
898
+ lines_added, lines_removed,
899
+ mode_before, mode_after,
900
+ source_tool_name, source_tool_input,
901
+ redacted, normalizer_notes, created_at
902
+ ) VALUES (
903
+ ?, ?, ?, ?,
904
+ ?, ?, ?, ?, ?,
905
+ ?, ?,
906
+ ?, ?,
907
+ ?, ?,
908
+ ?, ?, ?, ?, ?,
909
+ ?, ?, ?, ?,
910
+ ?, ?,
911
+ ?, ?,
912
+ ?, ?,
913
+ ?, ?, ?
914
+ )`
915
+ ).run(
916
+ id,
917
+ fc.run_id,
918
+ fc.step_id,
919
+ fc.sequence,
920
+ fc.tool_call_id ?? null,
921
+ fc.derived_from,
922
+ fc.path,
923
+ fc.old_path ?? null,
924
+ fc.op,
925
+ fc.before_blob_ref ?? null,
926
+ fc.after_blob_ref ?? null,
927
+ fc.partial_diff ? 1 : 0,
928
+ fc.gitignored ? 1 : 0,
929
+ fc.patch_text ?? null,
930
+ fc.patch_format ?? null,
931
+ fc.encoding ?? null,
932
+ fc.bom ? 1 : 0,
933
+ fc.line_endings ?? null,
934
+ fc.mime ?? null,
935
+ fc.language ?? null,
936
+ fc.size_before ?? null,
937
+ fc.size_after ?? null,
938
+ fc.line_count_before ?? null,
939
+ fc.line_count_after ?? null,
940
+ fc.lines_added,
941
+ fc.lines_removed,
942
+ fc.mode_before ?? null,
943
+ fc.mode_after ?? null,
944
+ fc.source_tool_name ?? null,
945
+ fc.source_tool_input !== void 0 ? JSON.stringify(fc.source_tool_input) : null,
946
+ fc.redacted ? 1 : 0,
947
+ fc.normalizer_notes !== void 0 ? JSON.stringify(fc.normalizer_notes) : null,
948
+ created
949
+ );
950
+ return {
951
+ file_change_id: id,
952
+ run_id: fc.run_id,
953
+ step_id: fc.step_id,
954
+ sequence: fc.sequence,
955
+ tool_call_id: fc.tool_call_id,
956
+ derived_from: fc.derived_from,
957
+ path: fc.path,
958
+ old_path: fc.old_path,
959
+ op: fc.op,
960
+ before_blob_ref: fc.before_blob_ref,
961
+ after_blob_ref: fc.after_blob_ref,
962
+ partial_diff: fc.partial_diff,
963
+ gitignored: fc.gitignored,
964
+ patch_text: fc.patch_text,
965
+ patch_format: fc.patch_format,
966
+ encoding: fc.encoding,
967
+ bom: fc.bom,
968
+ line_endings: fc.line_endings,
969
+ mime: fc.mime,
970
+ language: fc.language,
971
+ size_before: fc.size_before,
972
+ size_after: fc.size_after,
973
+ line_count_before: fc.line_count_before,
974
+ line_count_after: fc.line_count_after,
975
+ lines_added: fc.lines_added,
976
+ lines_removed: fc.lines_removed,
977
+ mode_before: fc.mode_before,
978
+ mode_after: fc.mode_after,
979
+ source_tool_name: fc.source_tool_name,
980
+ source_tool_input: fc.source_tool_input,
981
+ redacted: fc.redacted,
982
+ normalizer_notes: fc.normalizer_notes,
983
+ created_at: created
984
+ };
985
+ }
986
+ function listFileChanges(store, opts = {}) {
987
+ const params = [];
988
+ let where = "1=1";
989
+ if (opts.runId) {
990
+ where += " AND fc.run_id = ?";
991
+ params.push(opts.runId);
992
+ }
993
+ if (opts.stepId) {
994
+ where += " AND fc.step_id = ?";
995
+ params.push(opts.stepId);
996
+ }
997
+ if (opts.path) {
998
+ where += " AND (fc.path = ? OR fc.old_path = ?)";
999
+ params.push(opts.path, opts.path);
1000
+ }
1001
+ if (opts.maxStepSeqExclusive !== void 0) {
1002
+ where += " AND s.sequence < ?";
1003
+ params.push(opts.maxStepSeqExclusive);
1004
+ }
1005
+ const rows = store.db.prepare(
1006
+ `SELECT fc.*
1007
+ FROM file_change fc
1008
+ JOIN steps s ON s.step_id = fc.step_id
1009
+ WHERE ${where}
1010
+ ORDER BY s.sequence ASC, fc.sequence ASC`
1011
+ ).all(...params);
1012
+ return rows.map(rowToFileChange);
1013
+ }
1014
+ function getFileChange(store, fileChangeId) {
1015
+ const row = store.db.prepare("SELECT * FROM file_change WHERE file_change_id = ?").get(fileChangeId);
1016
+ return row ? rowToFileChange(row) : void 0;
1017
+ }
1018
+ function rowToBaselineTree(row) {
1019
+ return {
1020
+ baseline_tree_id: row.baseline_tree_id,
1021
+ project_id: row.project_id,
1022
+ manifest_blob_ref: row.manifest_blob_ref,
1023
+ git_head: row.git_head ?? void 0,
1024
+ git_dirty: row.git_dirty === 1,
1025
+ captured_at: row.captured_at
1026
+ };
1027
+ }
1028
+ function insertBaselineTree(store, bt) {
1029
+ const id = bt.baseline_tree_id ?? `bt_${randomUUID()}`;
1030
+ const captured = bt.captured_at ?? (/* @__PURE__ */ new Date()).toISOString();
1031
+ store.db.prepare(
1032
+ `INSERT INTO baseline_tree(
1033
+ baseline_tree_id, project_id, manifest_blob_ref,
1034
+ git_head, git_dirty, captured_at
1035
+ ) VALUES (?, ?, ?, ?, ?, ?)`
1036
+ ).run(
1037
+ id,
1038
+ bt.project_id,
1039
+ bt.manifest_blob_ref,
1040
+ bt.git_head ?? null,
1041
+ bt.git_dirty ? 1 : 0,
1042
+ captured
1043
+ );
1044
+ return {
1045
+ baseline_tree_id: id,
1046
+ project_id: bt.project_id,
1047
+ manifest_blob_ref: bt.manifest_blob_ref,
1048
+ git_head: bt.git_head,
1049
+ git_dirty: bt.git_dirty,
1050
+ captured_at: captured
1051
+ };
1052
+ }
1053
+ function getBaselineTree(store, baselineTreeId) {
1054
+ const row = store.db.prepare("SELECT * FROM baseline_tree WHERE baseline_tree_id = ?").get(baselineTreeId);
1055
+ return row ? rowToBaselineTree(row) : void 0;
1056
+ }
1057
+ function findBaselineByManifest(store, projectId, manifestBlobRef) {
1058
+ const row = store.db.prepare(
1059
+ "SELECT * FROM baseline_tree WHERE project_id = ? AND manifest_blob_ref = ? LIMIT 1"
1060
+ ).get(projectId, manifestBlobRef);
1061
+ return row ? rowToBaselineTree(row) : void 0;
1062
+ }
1063
+ function setRunBaselineTree(store, runId, baselineTreeId) {
1064
+ store.db.prepare("UPDATE runs SET baseline_tree_id = ? WHERE run_id = ?").run(baselineTreeId, runId);
1065
+ }
1066
+ function setRunProbeState(store, runId, state) {
1067
+ store.db.prepare("UPDATE runs SET probe_state = ? WHERE run_id = ?").run(state, runId);
1068
+ }
1069
+
1070
+ // src/settings.ts
1071
+ function getSetting(store, key) {
1072
+ const row = store.db.prepare("SELECT value FROM settings WHERE key = ?").get(key);
1073
+ return row?.value;
1074
+ }
1075
+ function setSetting(store, key, value) {
1076
+ store.db.prepare(
1077
+ `INSERT INTO settings(key, value, updated_at) VALUES (?, ?, ?)
1078
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
1079
+ ).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
1080
+ }
1081
+ function deleteSetting(store, key) {
1082
+ store.db.prepare("DELETE FROM settings WHERE key = ?").run(key);
1083
+ }
1084
+ function listSettings(store) {
1085
+ return store.db.prepare("SELECT key, value, updated_at FROM settings ORDER BY key").all();
1086
+ }
1087
+ function resolveSetting(store, key, envVar) {
1088
+ return process.env[envVar] ?? getSetting(store, key);
1089
+ }
1090
+ function isSecret(key) {
1091
+ return /api_key|webhook|password|token|secret|url$/i.test(key);
1092
+ }
1093
+ function maskSecret(value) {
1094
+ if (value.length <= 8) return "\u2022".repeat(value.length);
1095
+ return `${value.slice(0, 7)}${"\u2022".repeat(8)}${value.slice(-4)}`;
1096
+ }
1097
+
1098
+ // src/replay.ts
1099
+ var FIELD_SEP = 0;
1100
+ var RECORD_SEP = 10;
1101
+ function serializeManifest(entries) {
1102
+ const sorted = [...entries].sort((a, b) => {
1103
+ const ab = Buffer.from(a.path, "utf-8");
1104
+ const bb = Buffer.from(b.path, "utf-8");
1105
+ return Buffer.compare(ab, bb);
1106
+ });
1107
+ const parts = [];
1108
+ for (const e of sorted) {
1109
+ const pathBuf = Buffer.from(e.path, "utf-8");
1110
+ if (pathBuf.includes(FIELD_SEP) || pathBuf.includes(RECORD_SEP)) {
1111
+ throw new Error(
1112
+ `serializeManifest: path contains illegal byte (NUL or newline): ${JSON.stringify(e.path)}`
1113
+ );
1114
+ }
1115
+ const refBuf = Buffer.from(e.blob_ref, "utf-8");
1116
+ if (refBuf.includes(FIELD_SEP) || refBuf.includes(RECORD_SEP)) {
1117
+ throw new Error(
1118
+ `serializeManifest: blob_ref contains illegal byte: ${JSON.stringify(e.blob_ref)}`
1119
+ );
1120
+ }
1121
+ parts.push(pathBuf);
1122
+ parts.push(Buffer.from([FIELD_SEP]));
1123
+ parts.push(Buffer.from(String(e.mode), "utf-8"));
1124
+ parts.push(Buffer.from([FIELD_SEP]));
1125
+ parts.push(refBuf);
1126
+ parts.push(Buffer.from([RECORD_SEP]));
1127
+ }
1128
+ return Buffer.concat(parts);
1129
+ }
1130
+ function parseManifest(buf) {
1131
+ if (buf.length === 0) return [];
1132
+ const entries = [];
1133
+ let cursor = 0;
1134
+ while (cursor < buf.length) {
1135
+ const nl = buf.indexOf(RECORD_SEP, cursor);
1136
+ const end = nl === -1 ? buf.length : nl;
1137
+ if (end === cursor) {
1138
+ cursor = end + 1;
1139
+ continue;
1140
+ }
1141
+ const record = buf.subarray(cursor, end);
1142
+ const sep1 = record.indexOf(FIELD_SEP);
1143
+ if (sep1 === -1) {
1144
+ throw new Error(
1145
+ `parseManifest: malformed record at offset ${cursor} (no NUL separator)`
1146
+ );
1147
+ }
1148
+ const sep2 = record.indexOf(FIELD_SEP, sep1 + 1);
1149
+ if (sep2 === -1) {
1150
+ throw new Error(
1151
+ `parseManifest: malformed record at offset ${cursor} (missing second NUL)`
1152
+ );
1153
+ }
1154
+ const path = record.subarray(0, sep1).toString("utf-8");
1155
+ const modeStr = record.subarray(sep1 + 1, sep2).toString("utf-8");
1156
+ const blob_ref = record.subarray(sep2 + 1).toString("utf-8");
1157
+ const mode = Number.parseInt(modeStr, 10);
1158
+ if (!Number.isFinite(mode)) {
1159
+ throw new Error(
1160
+ `parseManifest: invalid mode "${modeStr}" at offset ${cursor}`
1161
+ );
1162
+ }
1163
+ entries.push({ path, mode, blob_ref });
1164
+ if (nl === -1) break;
1165
+ cursor = nl + 1;
1166
+ }
1167
+ return entries;
1168
+ }
1169
+ async function loadBaselineTree(store, baseline) {
1170
+ const tree = /* @__PURE__ */ new Map();
1171
+ const buf = await store.blobs.getBuffer(baseline.manifest_blob_ref).catch(
1172
+ () => void 0
1173
+ );
1174
+ if (!buf) return tree;
1175
+ for (const e of parseManifest(buf)) {
1176
+ tree.set(e.path, { blob_ref: e.blob_ref, mode: e.mode });
1177
+ }
1178
+ return tree;
1179
+ }
1180
+ async function workingTreeAt(store, runId, opts = {}) {
1181
+ const run = getRun(store, runId);
1182
+ if (!run) return /* @__PURE__ */ new Map();
1183
+ const tree = run.baseline_tree_id ? await (async () => {
1184
+ const bt = getBaselineTree(store, run.baseline_tree_id);
1185
+ return bt ? await loadBaselineTree(store, bt) : /* @__PURE__ */ new Map();
1186
+ })() : /* @__PURE__ */ new Map();
1187
+ const fcs = listFileChanges(store, {
1188
+ runId: run.run_id,
1189
+ maxStepSeqExclusive: opts.stepSeq
1190
+ });
1191
+ for (const fc of fcs) {
1192
+ applyFileChange(tree, fc);
1193
+ }
1194
+ return tree;
1195
+ }
1196
+ function applyFileChange(tree, fc) {
1197
+ if (fc.partial_diff) return;
1198
+ if (fc.op === "delete") {
1199
+ tree.delete(fc.path);
1200
+ return;
1201
+ }
1202
+ if (fc.op === "rename") {
1203
+ if (fc.old_path) tree.delete(fc.old_path);
1204
+ if (fc.after_blob_ref) {
1205
+ const existing = tree.get(fc.path);
1206
+ tree.set(fc.path, {
1207
+ blob_ref: fc.after_blob_ref,
1208
+ mode: fc.mode_after ?? existing?.mode ?? 33188
1209
+ });
1210
+ }
1211
+ return;
1212
+ }
1213
+ if (fc.op === "chmod") {
1214
+ const existing = tree.get(fc.path);
1215
+ if (existing && fc.mode_after !== void 0) {
1216
+ tree.set(fc.path, { ...existing, mode: fc.mode_after });
1217
+ }
1218
+ return;
1219
+ }
1220
+ if (fc.after_blob_ref) {
1221
+ const existing = tree.get(fc.path);
1222
+ tree.set(fc.path, {
1223
+ blob_ref: fc.after_blob_ref,
1224
+ mode: fc.mode_after ?? existing?.mode ?? 33188
1225
+ });
1226
+ }
1227
+ }
1228
+
1229
+ // src/baseline.ts
1230
+ import { readdir, readFile as readFile2, stat as stat2 } from "fs/promises";
1231
+ import { spawnSync } from "child_process";
1232
+ import { availableParallelism } from "os";
1233
+ import { join, relative, sep } from "path";
1234
+ import { IgnoreMatcher } from "@meterbility/shared";
1235
+ var MAX_BASELINE_FILE_BYTES = 5 * 1024 * 1024;
1236
+ var HASH_CONCURRENCY = Math.min(8, Math.max(1, availableParallelism()));
1237
+ async function captureBaseline(store, projectId, cwd, opts = {}) {
1238
+ try {
1239
+ const s = await stat2(cwd);
1240
+ if (!s.isDirectory()) return void 0;
1241
+ } catch {
1242
+ return void 0;
1243
+ }
1244
+ const matcher = opts.matcher ?? await buildDefaultMatcher(cwd);
1245
+ const maxBytes = opts.maxFileBytes ?? MAX_BASELINE_FILE_BYTES;
1246
+ const candidates = await walk(cwd, matcher);
1247
+ const entries = [];
1248
+ let skipped = 0;
1249
+ const queue = [...candidates];
1250
+ await Promise.all(
1251
+ Array.from({ length: HASH_CONCURRENCY }, async () => {
1252
+ for (; ; ) {
1253
+ const c = queue.shift();
1254
+ if (!c) return;
1255
+ const absPath = join(cwd, ...c.repoPath.split("/"));
1256
+ try {
1257
+ const buf = await readFile2(absPath);
1258
+ if (buf.length > maxBytes) {
1259
+ skipped += 1;
1260
+ continue;
1261
+ }
1262
+ const blob_ref = await store.blobs.putBuffer(buf);
1263
+ entries.push({ path: c.repoPath, mode: c.mode, blob_ref });
1264
+ } catch {
1265
+ skipped += 1;
1266
+ }
1267
+ }
1268
+ })
1269
+ );
1270
+ const manifestBuf = serializeManifest(entries);
1271
+ const manifest_blob_ref = await store.blobs.putBuffer(manifestBuf, {
1272
+ skipRedact: true
1273
+ });
1274
+ const existing = findBaselineByManifest(store, projectId, manifest_blob_ref);
1275
+ const { git_head, git_dirty } = readGitAdvisory(cwd);
1276
+ if (existing) {
1277
+ return {
1278
+ baseline_tree_id: existing.baseline_tree_id,
1279
+ manifest_blob_ref,
1280
+ file_count: entries.length,
1281
+ skipped,
1282
+ reused_existing: true,
1283
+ git_head: existing.git_head ?? git_head,
1284
+ git_dirty: existing.git_dirty
1285
+ };
1286
+ }
1287
+ const row = insertBaselineTree(store, {
1288
+ project_id: projectId,
1289
+ manifest_blob_ref,
1290
+ git_head,
1291
+ git_dirty
1292
+ });
1293
+ return {
1294
+ baseline_tree_id: row.baseline_tree_id,
1295
+ manifest_blob_ref,
1296
+ file_count: entries.length,
1297
+ skipped,
1298
+ reused_existing: false,
1299
+ git_head,
1300
+ git_dirty
1301
+ };
1302
+ }
1303
+ async function walk(rootCwd, matcher) {
1304
+ const out = [];
1305
+ const stack = [rootCwd];
1306
+ while (stack.length > 0) {
1307
+ const dir = stack.pop();
1308
+ let entries;
1309
+ try {
1310
+ entries = await readdir(dir, { withFileTypes: true });
1311
+ } catch {
1312
+ continue;
1313
+ }
1314
+ for (const entry of entries) {
1315
+ const abs = join(dir, entry.name);
1316
+ const repoRel = toRepoRelative(rootCwd, abs);
1317
+ const isDir = entry.isDirectory();
1318
+ if (matcher.matches(repoRel, isDir)) continue;
1319
+ if (isDir) {
1320
+ stack.push(abs);
1321
+ } else if (entry.isFile()) {
1322
+ let mode = 33188;
1323
+ try {
1324
+ const s = await stat2(abs);
1325
+ mode = s.mode;
1326
+ } catch {
1327
+ }
1328
+ out.push({ repoPath: repoRel, mode });
1329
+ }
1330
+ }
1331
+ }
1332
+ return out;
1333
+ }
1334
+ function toRepoRelative(rootCwd, abs) {
1335
+ return relative(rootCwd, abs).split(sep).join("/");
1336
+ }
1337
+ async function buildDefaultMatcher(cwd) {
1338
+ const userMeterbility = await readLines(join(cwd, ".meterbilityignore"));
1339
+ const gitignore = await readLines(join(cwd, ".gitignore"));
1340
+ return IgnoreMatcher.fromDefaultsPlus(userMeterbility, gitignore);
1341
+ }
1342
+ async function readLines(path) {
1343
+ try {
1344
+ const text = await readFile2(path, "utf-8");
1345
+ return text.split(/\r?\n/);
1346
+ } catch {
1347
+ return void 0;
1348
+ }
1349
+ }
1350
+ function readGitAdvisory(cwd) {
1351
+ const head = spawnSync("git", ["rev-parse", "HEAD"], {
1352
+ cwd,
1353
+ encoding: "utf-8",
1354
+ // Suppress noisy "not a git repository" output from stderr.
1355
+ stdio: ["ignore", "pipe", "ignore"]
1356
+ });
1357
+ const git_head = head.status === 0 ? head.stdout.trim() || void 0 : void 0;
1358
+ if (!git_head) return { git_head: void 0, git_dirty: false };
1359
+ const status = spawnSync("git", ["status", "--porcelain"], {
1360
+ cwd,
1361
+ encoding: "utf-8",
1362
+ stdio: ["ignore", "pipe", "ignore"]
1363
+ });
1364
+ const git_dirty = status.status === 0 && status.stdout.trim().length > 0;
1365
+ return { git_head, git_dirty };
1366
+ }
1367
+
1368
+ // src/file_size_policy.ts
1369
+ var DEFAULT_MAX_PARTIAL_BYTES = 5e6;
1370
+ var DEFAULT_MAX_SKIP_BYTES = 5e7;
1371
+ function enforceFileSizePolicy(input) {
1372
+ const maxPartial = input.settings?.max_partial_bytes ?? DEFAULT_MAX_PARTIAL_BYTES;
1373
+ const maxSkip = input.settings?.max_skip_bytes ?? DEFAULT_MAX_SKIP_BYTES;
1374
+ if (maxSkip <= maxPartial) {
1375
+ throw new Error(
1376
+ `file_size_policy: misconfig \u2014 max_skip_bytes (${maxSkip}) must exceed max_partial_bytes (${maxPartial})`
1377
+ );
1378
+ }
1379
+ const sizeBefore = input.beforeBuf?.length;
1380
+ const sizeAfter = input.afterBuf?.length;
1381
+ const largerSize = Math.max(sizeBefore ?? 0, sizeAfter ?? 0);
1382
+ if (sizeBefore === void 0 && sizeAfter === void 0) {
1383
+ return {
1384
+ beforeBuf: void 0,
1385
+ afterBuf: void 0,
1386
+ partial_diff: false,
1387
+ redacted: false,
1388
+ size_before: void 0,
1389
+ size_after: void 0
1390
+ };
1391
+ }
1392
+ if (largerSize > maxSkip) {
1393
+ return {
1394
+ beforeBuf: void 0,
1395
+ afterBuf: void 0,
1396
+ partial_diff: false,
1397
+ redacted: true,
1398
+ size_before: sizeBefore,
1399
+ size_after: sizeAfter
1400
+ };
1401
+ }
1402
+ if (largerSize > maxPartial) {
1403
+ const dropBefore = (sizeBefore ?? 0) >= (sizeAfter ?? 0);
1404
+ return {
1405
+ beforeBuf: dropBefore ? void 0 : input.beforeBuf,
1406
+ afterBuf: dropBefore ? input.afterBuf : void 0,
1407
+ partial_diff: true,
1408
+ redacted: false,
1409
+ size_before: sizeBefore,
1410
+ size_after: sizeAfter
1411
+ };
1412
+ }
1413
+ return {
1414
+ beforeBuf: input.beforeBuf,
1415
+ afterBuf: input.afterBuf,
1416
+ partial_diff: false,
1417
+ redacted: false,
1418
+ size_before: sizeBefore,
1419
+ size_after: sizeAfter
1420
+ };
1421
+ }
1422
+ export {
1423
+ BlobStore,
1424
+ DEFAULT_MAX_PARTIAL_BYTES,
1425
+ DEFAULT_MAX_SKIP_BYTES,
1426
+ FileChangeInvariantError,
1427
+ SCHEMA_VERSION,
1428
+ Store,
1429
+ aggregateTokens,
1430
+ applyFileChange,
1431
+ assertFileChangeInvariants,
1432
+ captureBaseline,
1433
+ deleteSetting,
1434
+ enforceFileSizePolicy,
1435
+ ensureSchema,
1436
+ findBaselineByManifest,
1437
+ getBaselineTree,
1438
+ getFileChange,
1439
+ getIngestOffset,
1440
+ getRun,
1441
+ getRunBySessionId,
1442
+ getSetting,
1443
+ getStep,
1444
+ getStepBySequence,
1445
+ insertAnnotation,
1446
+ insertBaselineTree,
1447
+ insertFileChange,
1448
+ insertFork,
1449
+ insertRun,
1450
+ insertStep,
1451
+ isProbablyText,
1452
+ isSecret,
1453
+ listAnnotations,
1454
+ listFileChanges,
1455
+ listForks,
1456
+ listRuns,
1457
+ listSettings,
1458
+ listSteps,
1459
+ loadBaselineTree,
1460
+ maskSecret,
1461
+ parseManifest,
1462
+ recordContextSnapshot,
1463
+ resolveSetting,
1464
+ resolveSnapshotBlobRef,
1465
+ serializeManifest,
1466
+ setIngestOffset,
1467
+ setRunBaselineTree,
1468
+ setRunProbeState,
1469
+ setRunStatus,
1470
+ setSetting,
1471
+ updateRunTotals,
1472
+ upsertAgent,
1473
+ upsertProjectByCwd,
1474
+ workingTreeAt
1475
+ };