@meterbility/cli 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.
Files changed (4) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +11 -0
  3. package/dist/index.js +3018 -0
  4. package/package.json +56 -0
package/dist/index.js ADDED
@@ -0,0 +1,3018 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import pc23 from "picocolors";
6
+
7
+ // src/commands/ingest.ts
8
+ import { existsSync } from "fs";
9
+ import pc2 from "picocolors";
10
+ import {
11
+ discoverSessions,
12
+ ingestSession
13
+ } from "@meterbility/claude-code-adapter";
14
+ import {
15
+ discoverCodexSessions,
16
+ ingestCodexSession
17
+ } from "@meterbility/codex-cli-adapter";
18
+ import {
19
+ defaultGlobalDbPath,
20
+ discoverCursorWorkspaces,
21
+ ingestCursorGlobal
22
+ } from "@meterbility/cursor-adapter";
23
+
24
+ // src/util.ts
25
+ import pc from "picocolors";
26
+ import { Store } from "@meterbility/collector";
27
+ import { fmtCents, fmtTokens } from "@meterbility/shared";
28
+ function openStore() {
29
+ return Store.open();
30
+ }
31
+ function statusColor(status) {
32
+ switch (status) {
33
+ case "ok":
34
+ return pc.green;
35
+ case "error":
36
+ return pc.red;
37
+ case "in_progress":
38
+ return pc.yellow;
39
+ case "abandoned":
40
+ return pc.gray;
41
+ default:
42
+ return (s) => s;
43
+ }
44
+ }
45
+ function actionLabel(step) {
46
+ switch (step.action.kind) {
47
+ case "tool_call":
48
+ return `${step.action.tool_name ?? "tool"}`;
49
+ case "message":
50
+ return "message";
51
+ case "thinking_only":
52
+ return "thinking";
53
+ case "sub_agent_dispatch":
54
+ return `sub_agent(${step.action.sub_agent ?? "?"})`;
55
+ case "none":
56
+ return "\u2014";
57
+ }
58
+ }
59
+ function shortId(id, len = 12) {
60
+ return id.length > len ? id.slice(0, len) : id;
61
+ }
62
+ function runSummaryLine(r) {
63
+ const status = statusColor(r.status)(r.status.padEnd(11));
64
+ const cost = fmtCents(r.cost_cents).padStart(8);
65
+ return `${pc.dim(shortId(r.run_id))} ${status} ${String(r.step_count).padStart(4)} steps ${cost} ${pc.cyan(
66
+ shortId(r.git_branch ?? "", 16).padEnd(16)
67
+ )} ${r.title ?? ""}`;
68
+ }
69
+
70
+ // src/commands/ingest.ts
71
+ function registerIngestCommand(program2) {
72
+ const ingest = program2.command("ingest").description("Capture agent runs into the local store");
73
+ ingest.command("claude-code [path]").description(
74
+ "Import Claude Code sessions. With no path, ingests every session under ~/.claude/projects (only new bytes per file)."
75
+ ).option("--cwd <dir>", "Restrict to a project by working directory").option("--limit <n>", "Cap number of sessions ingested", (v) => parseInt(v, 10)).action(async (path, opts) => {
76
+ const store = openStore();
77
+ try {
78
+ let paths = [];
79
+ if (path) {
80
+ if (!existsSync(path)) throw new Error(`not found: ${path}`);
81
+ paths = [path];
82
+ } else {
83
+ const sessions = await discoverSessions({ cwd: opts.cwd });
84
+ paths = sessions.map((s) => s.path);
85
+ if (opts.limit) paths = paths.slice(0, opts.limit);
86
+ }
87
+ if (paths.length === 0) {
88
+ console.log(pc2.dim("no sessions to ingest"));
89
+ return;
90
+ }
91
+ let totalSteps = 0;
92
+ let totalBytes = 0;
93
+ let runs = 0;
94
+ for (const p of paths) {
95
+ const r = await ingestSession(store, p);
96
+ if (r.status === "empty") {
97
+ console.log(`${pc2.dim("skip")} ${p} ${pc2.dim("(no new bytes)")}`);
98
+ continue;
99
+ }
100
+ runs += 1;
101
+ totalSteps += r.steps_added;
102
+ totalBytes += r.bytes_read;
103
+ console.log(
104
+ `${pc2.green("ingested")} ${p} ${pc2.dim(
105
+ `run=${r.run_id.slice(0, 12)} steps=${r.steps_added} bytes=${r.bytes_read}`
106
+ )}`
107
+ );
108
+ }
109
+ console.log(
110
+ pc2.bold(
111
+ `
112
+ ${runs} run(s) updated \xB7 ${totalSteps} step(s) \xB7 ${(totalBytes / 1024).toFixed(1)}KB read`
113
+ )
114
+ );
115
+ } finally {
116
+ store.close();
117
+ }
118
+ });
119
+ ingest.command("codex-cli [path]").description(
120
+ "Import Codex / Codex Desktop sessions. With no path, ingests every rollout under ~/.codex/sessions."
121
+ ).option("--limit <n>", "Cap number of sessions ingested", (v) => parseInt(v, 10)).action(async (path, opts) => {
122
+ const store = openStore();
123
+ try {
124
+ let paths = [];
125
+ if (path) {
126
+ if (!existsSync(path)) throw new Error(`not found: ${path}`);
127
+ paths = [path];
128
+ } else {
129
+ const sessions = await discoverCodexSessions();
130
+ paths = sessions.map((s) => s.path);
131
+ if (opts.limit) paths = paths.slice(0, opts.limit);
132
+ }
133
+ if (paths.length === 0) {
134
+ console.log(pc2.dim("no Codex sessions to ingest"));
135
+ return;
136
+ }
137
+ let runs = 0;
138
+ let totalSteps = 0;
139
+ let totalBytes = 0;
140
+ for (const p of paths) {
141
+ const r = await ingestCodexSession(store, p);
142
+ if (r.status === "empty") {
143
+ console.log(`${pc2.dim("skip")} ${p} ${pc2.dim("(no new bytes)")}`);
144
+ continue;
145
+ }
146
+ runs += 1;
147
+ totalSteps += r.steps_added;
148
+ totalBytes += r.bytes_read;
149
+ console.log(
150
+ `${pc2.green("ingested")} ${p} ${pc2.dim(
151
+ `run=${r.run_id.slice(0, 12)} steps=${r.steps_added} bytes=${r.bytes_read}`
152
+ )}`
153
+ );
154
+ }
155
+ console.log(
156
+ pc2.bold(
157
+ `
158
+ ${runs} run(s) updated \xB7 ${totalSteps} step(s) \xB7 ${(totalBytes / 1024).toFixed(1)}KB read`
159
+ )
160
+ );
161
+ } finally {
162
+ store.close();
163
+ }
164
+ });
165
+ ingest.command("cursor").description(
166
+ "Import Cursor composer (Agents/Composer window) conversations from the global state.vscdb."
167
+ ).option("--db <path>", "Override path to Cursor's state.vscdb").option("--composer <id>", "Restrict to one composer id").option(
168
+ "--limit <n>",
169
+ "Cap composers ingested (newest-first)",
170
+ (v) => parseInt(v, 10)
171
+ ).option(
172
+ "--since <iso>",
173
+ "Skip composers older than this ISO timestamp"
174
+ ).option(
175
+ "--cwd <dir>",
176
+ "Project cwd to attribute the runs to (defaults to '(cursor)')"
177
+ ).option(
178
+ "--list-workspaces",
179
+ "Just print discovered Cursor workspaces and exit"
180
+ ).action(async (opts) => {
181
+ if (opts.listWorkspaces) {
182
+ const ws = await discoverCursorWorkspaces();
183
+ if (ws.length === 0) {
184
+ console.log(pc2.dim("no Cursor workspace storage found"));
185
+ return;
186
+ }
187
+ console.log(pc2.bold("Cursor workspaces (newest first):"));
188
+ for (const w of ws.slice(0, 20)) {
189
+ console.log(
190
+ ` ${pc2.cyan(w.workspace_id.slice(0, 12))} ${w.mtime.toISOString()} ${pc2.dim(w.path)}`
191
+ );
192
+ }
193
+ return;
194
+ }
195
+ const store = openStore();
196
+ try {
197
+ const sinceMs = opts.since ? Date.parse(opts.since) : void 0;
198
+ const dbPath2 = opts.db ?? defaultGlobalDbPath();
199
+ const r = await ingestCursorGlobal(store, {
200
+ dbPath: dbPath2,
201
+ composerId: opts.composer,
202
+ limit: opts.limit,
203
+ sinceMs: Number.isFinite(sinceMs) ? sinceMs : void 0,
204
+ cwd: opts.cwd
205
+ });
206
+ if (r.status !== "ok") {
207
+ console.log(`${pc2.yellow(r.status)} ${r.reason ?? ""}`);
208
+ return;
209
+ }
210
+ console.log(
211
+ pc2.bold(
212
+ `${r.composers_ingested}/${r.composers_seen} composer(s) \xB7 ${r.steps_added} step(s)`
213
+ )
214
+ );
215
+ } finally {
216
+ store.close();
217
+ }
218
+ });
219
+ ingest.command("discover").description("List Claude Code sessions visible to Meterbility, newest first").option("--cwd <dir>", "Restrict to one project").action(async (opts) => {
220
+ const sessions = await discoverSessions({ cwd: opts.cwd });
221
+ if (sessions.length === 0) {
222
+ console.log(pc2.dim("no sessions found"));
223
+ return;
224
+ }
225
+ console.log(pc2.bold("Sessions:"));
226
+ for (const s of sessions.slice(0, 50)) {
227
+ console.log(
228
+ ` ${pc2.cyan(s.session_id.slice(0, 8))} ${s.mtime.toISOString()} ${(s.size_bytes / 1024).toFixed(1).padStart(8)}KB ${pc2.dim(s.project_dir)}`
229
+ );
230
+ }
231
+ if (sessions.length > 50) {
232
+ console.log(pc2.dim(` \u2026 ${sessions.length - 50} more`));
233
+ }
234
+ });
235
+ }
236
+
237
+ // src/commands/list.ts
238
+ import pc3 from "picocolors";
239
+ import { listRuns } from "@meterbility/collector";
240
+ function registerListCommand(program2) {
241
+ program2.command("list").alias("ls").description("List recent runs").option("-n, --limit <n>", "max rows", (v) => parseInt(v, 10), 30).option("--status <s>", "filter by status (ok|error|in_progress|abandoned)").option("--tool <name>", "filter to runs containing a tool call by name").action(async (opts) => {
242
+ const store = openStore();
243
+ try {
244
+ const runs = listRuns(store, {
245
+ limit: opts.limit,
246
+ status: opts.status,
247
+ containsTool: opts.tool
248
+ });
249
+ if (runs.length === 0) {
250
+ console.log(
251
+ pc3.dim(
252
+ "no runs found. Try: meter ingest claude-code --limit 1"
253
+ )
254
+ );
255
+ return;
256
+ }
257
+ console.log(
258
+ pc3.bold(
259
+ "RUN".padEnd(12) + " STATUS STEPS COST BRANCH TITLE"
260
+ )
261
+ );
262
+ for (const r of runs) console.log(runSummaryLine(r));
263
+ } finally {
264
+ store.close();
265
+ }
266
+ });
267
+ }
268
+
269
+ // src/commands/inspect.ts
270
+ import pc4 from "picocolors";
271
+ import {
272
+ getRun,
273
+ getStep,
274
+ getStepBySequence,
275
+ listAnnotations,
276
+ listFileChanges,
277
+ listForks,
278
+ listSteps,
279
+ resolveSnapshotBlobRef
280
+ } from "@meterbility/collector";
281
+ import { reformatJsonString, prettyTab } from "@meterbility/server";
282
+ function registerInspectCommand(program2) {
283
+ program2.command("inspect <run-id>").description("Terminal-rendered timeline + step inspector").option("--at <seq-or-step-id>", "Open a specific step").option(
284
+ "--show <tab>",
285
+ "Tab to print (context|decision|action|outcome|cost|files|all)",
286
+ "all"
287
+ ).option(
288
+ "--diff",
289
+ "When `--show files`, print the unified diff for each captured file"
290
+ ).option(
291
+ "--pretty-print",
292
+ "Render decision/action/outcome/cost tabs in a human-readable layout. Default is raw JSON. The context and files tabs are unchanged."
293
+ ).action(async (runId, opts) => {
294
+ const store = openStore();
295
+ try {
296
+ const run = getRun(store, runId);
297
+ if (!run) throw new Error(`run not found: ${runId}`);
298
+ printRunHeader(run);
299
+ const steps = listSteps(store, run.run_id);
300
+ printTimeline(steps);
301
+ const annotations = listAnnotations(store, "run", run.run_id);
302
+ if (annotations.length) {
303
+ console.log(pc4.bold("\nRun annotations"));
304
+ for (const a of annotations) {
305
+ console.log(
306
+ ` ${pc4.cyan(a.author)} ${pc4.dim(a.verdict ?? "note")} ${a.note ?? ""}`
307
+ );
308
+ }
309
+ }
310
+ const forks = listForks(store, run.run_id);
311
+ if (forks.length) {
312
+ console.log(pc4.bold("\nForks of this run"));
313
+ for (const f of forks) {
314
+ console.log(
315
+ ` ${pc4.magenta(f.edit_type)} from step ${pc4.dim(f.origin_step_id.slice(0, 12))} \u2192 ${pc4.cyan(f.fork_run_id.slice(0, 12))}`
316
+ );
317
+ }
318
+ }
319
+ if (opts.at !== void 0) {
320
+ const seq = Number(opts.at);
321
+ const step = Number.isFinite(seq) ? getStepBySequence(store, run.run_id, seq) : getStep(store, opts.at);
322
+ if (!step) throw new Error(`step not found: ${opts.at}`);
323
+ await printStep(store, step, opts.show, opts.diff === true, opts.prettyPrint === true);
324
+ } else {
325
+ console.log("");
326
+ for (const s of steps) await printStepSummary(s);
327
+ console.log(
328
+ pc4.dim(
329
+ `
330
+ open one with: meter inspect ${run.run_id.slice(0, 12)} --at 0`
331
+ )
332
+ );
333
+ }
334
+ } finally {
335
+ store.close();
336
+ }
337
+ });
338
+ }
339
+ function printRunHeader(run) {
340
+ if (!run) return;
341
+ const status = statusColor(run.status)(run.status);
342
+ console.log(pc4.bold(run.title ?? run.run_id));
343
+ console.log(
344
+ ` ${pc4.dim("run")} ${run.run_id}
345
+ ${pc4.dim("status")} ${status}
346
+ ${pc4.dim("branch")} ${run.git_branch ?? "\u2014"}
347
+ ${pc4.dim("cwd")} ${run.cwd ?? "\u2014"}
348
+ ${pc4.dim("steps")} ${run.step_count}
349
+ ${pc4.dim("cost")} ${fmtCents(run.cost_cents)} ${pc4.dim(`(input ${fmtTokens(run.tokens_total_input)} \xB7 output ${fmtTokens(run.tokens_total_output)} \xB7 cached ${fmtTokens(run.tokens_total_cached)})`)}
350
+ ${pc4.dim("started")} ${run.started_at}`
351
+ );
352
+ }
353
+ function printTimeline(steps) {
354
+ if (steps.length === 0) return;
355
+ console.log(pc4.bold("\nTimeline"));
356
+ const cells = [];
357
+ for (const s of steps) {
358
+ const color = s.status === "error" ? pc4.red : s.status === "ok" ? pc4.green : s.status === "in_progress" ? pc4.yellow : pc4.dim;
359
+ const label = `${s.sequence}.${actionLabel(s).slice(0, 6)}`;
360
+ cells.push(color(label));
361
+ }
362
+ let line = " ";
363
+ for (const cell of cells) {
364
+ if (line.length + cell.length > 130) {
365
+ console.log(line);
366
+ line = " ";
367
+ }
368
+ line += cell + " ";
369
+ }
370
+ if (line.trim().length) console.log(line);
371
+ }
372
+ async function printStepSummary(s) {
373
+ const color = statusColor(s.status);
374
+ console.log(
375
+ ` ${pc4.dim(String(s.sequence).padStart(3))} ${color(s.status.padEnd(11))} ${actionLabel(s).padEnd(18)} ${pc4.dim("tok " + fmtTokens(s.tokens.input) + "/" + fmtTokens(s.tokens.output))} ${pc4.dim(fmtCents(s.cost_cents).padStart(7))} ${pc4.dim(s.step_id.slice(0, 12))}`
376
+ );
377
+ }
378
+ async function printStep(store, step, show, withDiff = false, pretty = false) {
379
+ console.log(pc4.bold(`
380
+ Step #${step.sequence} ${pc4.dim(step.step_id)}`));
381
+ console.log(` ${pc4.dim("model")} ${step.model}`);
382
+ console.log(` ${pc4.dim("status")} ${statusColor(step.status)(step.status)}`);
383
+ console.log(` ${pc4.dim("action")} ${actionLabel(step)}`);
384
+ const showAll = show === "all";
385
+ const pmode = "ansi";
386
+ if (showAll || show === "action") {
387
+ if (pretty) {
388
+ console.log("\n" + indent(prettyTab("action", step.action, { mode: pmode }), " "));
389
+ } else {
390
+ console.log(pc4.bold("\n action"));
391
+ console.log(indent(JSON.stringify(step.action, null, 2)));
392
+ }
393
+ }
394
+ if (showAll || show === "outcome") {
395
+ if (pretty) {
396
+ const toolResultText = step.outcome.tool_result_ref ? await store.blobs.tryGetString(step.outcome.tool_result_ref) ?? void 0 : void 0;
397
+ console.log(
398
+ "\n" + indent(
399
+ prettyTab("outcome", step.outcome, { mode: pmode, toolResultText }),
400
+ " "
401
+ )
402
+ );
403
+ } else {
404
+ console.log(pc4.bold("\n outcome"));
405
+ console.log(indent(JSON.stringify(step.outcome, null, 2)));
406
+ if (step.outcome.tool_result_ref) {
407
+ const text = await store.blobs.tryGetString(step.outcome.tool_result_ref);
408
+ if (text) {
409
+ console.log(pc4.bold("\n tool result (truncated)"));
410
+ console.log(indent(truncate(reformatJsonString(text), 2e3)));
411
+ }
412
+ }
413
+ }
414
+ }
415
+ if (showAll || show === "decision") {
416
+ const text = await store.blobs.tryGetString(step.decision_ref);
417
+ if (text) {
418
+ if (pretty) {
419
+ console.log("\n" + indent(prettyTab("decision", text, { mode: pmode }), " "));
420
+ } else {
421
+ console.log(pc4.bold("\n decision"));
422
+ console.log(indent(truncate(reformatJsonString(text), 4e3)));
423
+ }
424
+ }
425
+ }
426
+ if (showAll || show === "context") {
427
+ await printResolvedContext(store, step);
428
+ }
429
+ if (showAll || show === "cost") {
430
+ if (pretty) {
431
+ console.log(
432
+ "\n" + indent(
433
+ prettyTab(
434
+ "cost",
435
+ {
436
+ tokens: step.tokens,
437
+ latency_ms: step.latency_ms,
438
+ cost_cents: step.cost_cents,
439
+ tags: step.tags
440
+ },
441
+ { mode: pmode }
442
+ ),
443
+ " "
444
+ )
445
+ );
446
+ } else {
447
+ console.log(pc4.bold("\n cost"));
448
+ console.log(
449
+ indent(
450
+ JSON.stringify(
451
+ {
452
+ tokens: step.tokens,
453
+ latency_ms: step.latency_ms,
454
+ cost_cents: step.cost_cents,
455
+ tags: step.tags
456
+ },
457
+ null,
458
+ 2
459
+ )
460
+ )
461
+ );
462
+ }
463
+ }
464
+ if (showAll || show === "files") {
465
+ await printStepFiles(store, step, withDiff);
466
+ }
467
+ }
468
+ async function printStepFiles(store, step, withDiff) {
469
+ const fcs = listFileChanges(store, { stepId: step.step_id });
470
+ if (fcs.length === 0) {
471
+ if (withDiff) {
472
+ console.log(pc4.bold("\n files"));
473
+ console.log(indent(pc4.dim("(no file changes captured for this step)")));
474
+ }
475
+ return;
476
+ }
477
+ console.log(
478
+ pc4.bold("\n files") + pc4.dim(` \xB7 ${fcs.length} change${fcs.length === 1 ? "" : "s"}`)
479
+ );
480
+ for (const fc of fcs) {
481
+ const renderedPath = fc.op === "rename" && fc.old_path ? `${fc.old_path} \u2192 ${fc.path}` : fc.path;
482
+ const flags = [];
483
+ if (fc.partial_diff) flags.push(pc4.yellow("partial"));
484
+ if (fc.patch_format === "binary") flags.push(pc4.dim("binary"));
485
+ const stats = `${fc.lines_added === 0 ? "+0" : pc4.green(`+${fc.lines_added}`)} ${fc.lines_removed === 0 ? "\u22120" : pc4.red(`\u2212${fc.lines_removed}`)}`;
486
+ console.log(
487
+ ` ${opTagInline(fc.op)} ${renderedPath.padEnd(30)} ${stats}${flags.length ? " " + flags.join(" ") : ""}`
488
+ );
489
+ if (withDiff && fc.patch_text) {
490
+ for (const line of fc.patch_text.split("\n")) {
491
+ if (line.startsWith("@@")) console.log(indent(pc4.cyan(line), " "));
492
+ else if (line.startsWith("+") && !line.startsWith("+++"))
493
+ console.log(indent(pc4.green(line), " "));
494
+ else if (line.startsWith("-") && !line.startsWith("---"))
495
+ console.log(indent(pc4.red(line), " "));
496
+ else console.log(indent(line, " "));
497
+ }
498
+ } else if (withDiff && fc.partial_diff) {
499
+ console.log(
500
+ indent(
501
+ pc4.yellow(
502
+ "(partial \u2014 ran outside captured tools; enable `meter watch --files` in v0.4 for full fidelity)"
503
+ ),
504
+ " "
505
+ )
506
+ );
507
+ }
508
+ }
509
+ }
510
+ function opTagInline(op) {
511
+ switch (op) {
512
+ case "create":
513
+ return pc4.green("A");
514
+ case "modify":
515
+ return pc4.yellow("M");
516
+ case "delete":
517
+ return pc4.red("D");
518
+ case "rename":
519
+ return pc4.magenta("R");
520
+ case "chmod":
521
+ return pc4.dim("X");
522
+ }
523
+ }
524
+ async function printResolvedContext(store, step) {
525
+ const ref = resolveSnapshotBlobRef(store, step.context_snapshot_id);
526
+ const raw = await store.blobs.tryGetString(ref);
527
+ if (!raw) {
528
+ console.log(pc4.bold("\n context"));
529
+ console.log(indent(pc4.dim("(no snapshot blob found)")));
530
+ return;
531
+ }
532
+ let snapshot;
533
+ try {
534
+ snapshot = JSON.parse(raw);
535
+ } catch {
536
+ console.log(pc4.bold("\n context (raw)"));
537
+ console.log(indent(truncate(raw, 4e3)));
538
+ return;
539
+ }
540
+ let totalChars = 0;
541
+ const fetchText = async (r) => {
542
+ const text = await store.blobs.tryGetString(r) ?? "(missing blob)";
543
+ totalChars += text.length;
544
+ return text;
545
+ };
546
+ const resolved = [];
547
+ for (const c of snapshot.components) {
548
+ if (c.type === "system_prompt") {
549
+ resolved.push({
550
+ type: "system_prompt",
551
+ ref: c.content_ref,
552
+ text: await fetchText(c.content_ref)
553
+ });
554
+ } else if (c.type === "tool_definitions") {
555
+ resolved.push({
556
+ type: "tool_definitions",
557
+ ref: c.content_ref,
558
+ text: await fetchText(c.content_ref)
559
+ });
560
+ } else if (c.type === "conversation_history") {
561
+ const messages = [];
562
+ for (const m of c.messages ?? []) {
563
+ messages.push({
564
+ role: m.role,
565
+ ref: m.content_ref,
566
+ text: await fetchText(m.content_ref),
567
+ step_ref: m.step_ref
568
+ });
569
+ }
570
+ resolved.push({ type: "conversation_history", messages });
571
+ } else if (c.type === "retrieved_documents") {
572
+ const docs = [];
573
+ for (const d of c.docs ?? []) {
574
+ docs.push({
575
+ source: d.source,
576
+ ref: d.content_ref,
577
+ text: await fetchText(d.content_ref)
578
+ });
579
+ }
580
+ resolved.push({ type: "retrieved_documents", docs });
581
+ } else if (c.type === "compaction_summary") {
582
+ resolved.push({
583
+ type: "compaction_summary",
584
+ ref: c.content_ref,
585
+ text: await fetchText(c.content_ref),
586
+ replaces_steps: c.replaces_steps
587
+ });
588
+ }
589
+ }
590
+ console.log(
591
+ pc4.bold(
592
+ `
593
+ context (snapshot \xB7 ${resolved.length} component${resolved.length === 1 ? "" : "s"} \xB7 ${totalChars.toLocaleString()} chars)`
594
+ )
595
+ );
596
+ for (const c of resolved) {
597
+ if (c.type === "system_prompt") {
598
+ console.log(
599
+ pc4.bold(`
600
+ system_prompt`) + pc4.dim(` \xB7 ${c.text.length.toLocaleString()} chars \xB7 ${c.ref.slice(0, 12)}`)
601
+ );
602
+ console.log(indent(truncate(c.text, 2e3)));
603
+ } else if (c.type === "tool_definitions") {
604
+ console.log(
605
+ pc4.bold(`
606
+ tool_definitions`) + pc4.dim(` \xB7 ${c.text.length.toLocaleString()} chars \xB7 ${c.ref.slice(0, 12)}`)
607
+ );
608
+ console.log(indent(truncate(reformatJsonString(c.text), 2e3)));
609
+ } else if (c.type === "conversation_history") {
610
+ console.log(
611
+ pc4.bold(`
612
+ conversation_history`) + pc4.dim(
613
+ ` \xB7 ${c.messages.length} turn${c.messages.length === 1 ? "" : "s"}`
614
+ )
615
+ );
616
+ for (const m of c.messages) {
617
+ const roleColor = m.role === "user" ? pc4.cyan : m.role === "assistant" ? pc4.green : pc4.yellow;
618
+ const tag = roleColor(`[${m.role.toUpperCase()}]`);
619
+ const meta = pc4.dim(
620
+ ` \xB7 ${m.text.length.toLocaleString()} chars \xB7 ${m.ref.slice(0, 12)}${m.step_ref ? ` \xB7 step ${m.step_ref.slice(0, 12)}` : ""}`
621
+ );
622
+ console.log(` ${tag}${meta}`);
623
+ console.log(indent(truncate(m.text, 1200), " "));
624
+ }
625
+ } else if (c.type === "retrieved_documents") {
626
+ console.log(
627
+ pc4.bold(`
628
+ retrieved_documents`) + pc4.dim(` \xB7 ${c.docs.length} doc${c.docs.length === 1 ? "" : "s"}`)
629
+ );
630
+ for (const d of c.docs) {
631
+ console.log(
632
+ ` ${pc4.magenta(d.source)}${pc4.dim(` \xB7 ${d.text.length.toLocaleString()} chars \xB7 ${d.ref.slice(0, 12)}`)}`
633
+ );
634
+ console.log(indent(truncate(d.text, 1e3), " "));
635
+ }
636
+ } else if (c.type === "compaction_summary") {
637
+ console.log(
638
+ pc4.bold(`
639
+ compaction_summary`) + pc4.dim(
640
+ ` \xB7 replaces ${c.replaces_steps.length} step${c.replaces_steps.length === 1 ? "" : "s"} \xB7 ${c.ref.slice(0, 12)}`
641
+ )
642
+ );
643
+ console.log(indent(truncate(c.text, 2e3)));
644
+ if (c.replaces_steps.length) {
645
+ console.log(
646
+ indent(
647
+ pc4.dim(
648
+ "replaced: " + c.replaces_steps.map((s) => s.slice(0, 12)).join(", ")
649
+ )
650
+ )
651
+ );
652
+ }
653
+ }
654
+ }
655
+ }
656
+ function indent(s, prefix = " ") {
657
+ return s.split("\n").map((l) => prefix + l).join("\n");
658
+ }
659
+ function truncate(s, max) {
660
+ return s.length > max ? s.slice(0, max) + pc4.dim(`
661
+ \u2026 (${s.length - max} more chars)`) : s;
662
+ }
663
+
664
+ // src/commands/fork.ts
665
+ import { readFile } from "fs/promises";
666
+ import pc5 from "picocolors";
667
+ import {
668
+ forkRun,
669
+ anthropicResponder,
670
+ continueFork,
671
+ fakeResponder
672
+ } from "@meterbility/server";
673
+ import { getSetting } from "@meterbility/collector";
674
+ var EDIT_TYPES = [
675
+ "replace_system_prompt",
676
+ "add_context",
677
+ "remove_tool",
678
+ "modify_tool_description",
679
+ "replace_user_message",
680
+ "inject_message",
681
+ "change_model"
682
+ ];
683
+ function registerForkCommand(program2) {
684
+ program2.command("fork <run-id>").description("Replay a run with an edit applied at a chosen step").requiredOption(
685
+ "--at <seq-or-step-id>",
686
+ "Sequence index or step id to fork from"
687
+ ).requiredOption(
688
+ "--edit <type>",
689
+ `Edit type (${EDIT_TYPES.join("|")})`
690
+ ).option(
691
+ "--payload <json>",
692
+ "Edit payload as JSON (string or object)"
693
+ ).option("--payload-file <path>", "Read payload from a file (treated as text)").option("--text <string>", "Shortcut: set payload to { text: <string> }").option(
694
+ "--live",
695
+ "Run a live model call for the suffix step (requires ANTHROPIC_API_KEY)"
696
+ ).option("--live-model <model>", "Model for live suffix", "claude-opus-4-7").option("--fake <text>", "Use a fake responder that emits the given text").option(
697
+ "--continue <mode>",
698
+ "After the first suffix step, continue the agent loop. Values: simulate (use original tool results) | live (caller-provided executor; CLI supports bash-only safe mode)"
699
+ ).option(
700
+ "--max-iterations <n>",
701
+ "Cap continuation loop iterations",
702
+ (v) => parseInt(v, 10),
703
+ 25
704
+ ).option(
705
+ "--allow-tool <name>",
706
+ "(live continuation) Tools the executor will run. Repeatable.",
707
+ (v, prev = []) => [...prev, v],
708
+ []
709
+ ).action(async (runId, opts) => {
710
+ if (!EDIT_TYPES.includes(opts.edit)) {
711
+ throw new Error(
712
+ `unsupported edit type: ${opts.edit}
713
+ allowed: ${EDIT_TYPES.join(", ")}`
714
+ );
715
+ }
716
+ const edit = {
717
+ type: opts.edit,
718
+ payload: await resolvePayload(opts)
719
+ };
720
+ const store = openStore();
721
+ const flagModel = opts.liveModel && opts.liveModel !== "claude-opus-4-7" ? opts.liveModel : void 0;
722
+ const liveModelEffective = flagModel ?? getSetting(store, "fork.default_model") ?? "claude-opus-4-7";
723
+ const maxIterFromSetting = getSetting(
724
+ store,
725
+ "fork.default_max_iterations"
726
+ );
727
+ const maxIterationsEffective = opts.maxIterations !== 25 ? opts.maxIterations : maxIterFromSetting ? parseInt(maxIterFromSetting, 10) || opts.maxIterations : opts.maxIterations;
728
+ const responder = opts.fake ? fakeResponder(opts.fake) : opts.live ? buildAnthropicResponder(liveModelEffective) : void 0;
729
+ try {
730
+ const at = Number.isFinite(Number(opts.at)) ? Number(opts.at) : opts.at;
731
+ const result = await forkRun(
732
+ store,
733
+ {
734
+ origin_run_id: runId,
735
+ at,
736
+ edit
737
+ },
738
+ responder
739
+ );
740
+ console.log(pc5.bold("\nfork created"));
741
+ console.log(` ${pc5.dim("fork_id")} ${result.fork_id}`);
742
+ console.log(` ${pc5.dim("fork_run_id")} ${result.fork_run_id}`);
743
+ console.log(` ${pc5.dim("prefix")} ${result.prefix_steps} steps`);
744
+ console.log(
745
+ ` ${pc5.dim("suffix")} ${result.live ? pc5.green("live step appended") : pc5.yellow("none \u2014 use --live or --fake to extend")}`
746
+ );
747
+ if (opts.continue) {
748
+ if (opts.continue !== "simulate" && opts.continue !== "live") {
749
+ throw new Error(
750
+ `unknown --continue mode: ${opts.continue}
751
+ allowed: simulate, live`
752
+ );
753
+ }
754
+ if (opts.continue === "live" && !process.env.ANTHROPIC_API_KEY) {
755
+ throw new Error(
756
+ "--continue=live requires ANTHROPIC_API_KEY for the model loop"
757
+ );
758
+ }
759
+ const modelCaller = buildContinuationCaller(liveModelEffective);
760
+ const cont = await continueFork(store, result.fork_run_id, {
761
+ mode: opts.continue,
762
+ modelCaller,
763
+ toolExecutor: opts.continue === "live" ? buildBashOnlyExecutor(opts.allowTool) : void 0,
764
+ maxIterations: maxIterationsEffective,
765
+ originRunId: runId
766
+ });
767
+ console.log(pc5.bold("\ncontinuation"));
768
+ console.log(` ${pc5.dim("mode")} ${opts.continue}`);
769
+ console.log(` ${pc5.dim("iterations")} ${cont.iterations_run}`);
770
+ console.log(` ${pc5.dim("steps added")} ${cont.steps_added}`);
771
+ console.log(
772
+ ` ${pc5.dim("terminal")} ${terminalColor(cont.terminal_reason)}`
773
+ );
774
+ }
775
+ console.log(
776
+ pc5.dim(
777
+ `
778
+ open with: meter inspect ${result.fork_run_id.slice(0, 12)}
779
+ diff vs origin: meter diff ${runId.slice(0, 12)} ${result.fork_run_id.slice(0, 12)}`
780
+ )
781
+ );
782
+ } finally {
783
+ store.close();
784
+ }
785
+ });
786
+ }
787
+ function terminalColor(reason) {
788
+ switch (reason) {
789
+ case "model_completed":
790
+ return pc5.green(reason);
791
+ case "max_iterations":
792
+ return pc5.yellow(reason);
793
+ case "simulate_miss":
794
+ return pc5.yellow(reason);
795
+ case "tool_error":
796
+ case "model_error":
797
+ return pc5.red(reason);
798
+ default:
799
+ return reason;
800
+ }
801
+ }
802
+ function buildContinuationCaller(model) {
803
+ const apiKey = process.env.ANTHROPIC_API_KEY;
804
+ return async (args) => {
805
+ if (!apiKey) {
806
+ return {
807
+ model: "dry-run",
808
+ decision_content: [
809
+ { type: "text", text: "(dry run \u2014 set ANTHROPIC_API_KEY for live continuation)" }
810
+ ],
811
+ action: { kind: "message", text: "(dry run)" },
812
+ tokens: { input: 0, output: 0, cached_read: 0, cache_creation: 0 },
813
+ latency_ms: 0
814
+ };
815
+ }
816
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
817
+ const { Store: Store2 } = await import("@meterbility/collector");
818
+ const store = Store2.open();
819
+ try {
820
+ const client = new Anthropic({ apiKey });
821
+ const messages = [];
822
+ for (const m of args.history) {
823
+ if (m.role === "tool") continue;
824
+ const text2 = await store.blobs.tryGetString(m.content_ref);
825
+ if (text2) messages.push({ role: m.role, content: text2 });
826
+ }
827
+ const t0 = Date.now();
828
+ const resp = await client.messages.create({
829
+ model,
830
+ max_tokens: 4096,
831
+ system: args.system_prompt,
832
+ messages: messages.length ? messages : [{ role: "user", content: "(no history)" }]
833
+ });
834
+ const t1 = Date.now();
835
+ const blocks = resp.content ?? [];
836
+ const toolUse = blocks.find(
837
+ (b) => b.type === "tool_use"
838
+ );
839
+ const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("\n");
840
+ const cc = resp.usage?.cache_creation;
841
+ const tokens5m = cc ? cc.ephemeral_5m_input_tokens ?? 0 : resp.usage?.cache_creation_input_tokens ?? 0;
842
+ const tokens1h = cc?.ephemeral_1h_input_tokens ?? 0;
843
+ return {
844
+ model: resp.model,
845
+ decision_content: resp.content,
846
+ action: toolUse ? {
847
+ kind: "tool_call",
848
+ tool_name: toolUse.name,
849
+ tool_use_id: toolUse.id,
850
+ tool_input: toolUse.input
851
+ } : { kind: "message", text },
852
+ tokens: {
853
+ input: resp.usage?.input_tokens ?? 0,
854
+ output: resp.usage?.output_tokens ?? 0,
855
+ cached_read: resp.usage?.cache_read_input_tokens ?? 0,
856
+ cache_creation: tokens5m,
857
+ cache_creation_1h: tokens1h
858
+ },
859
+ latency_ms: t1 - t0
860
+ };
861
+ } finally {
862
+ store.close();
863
+ }
864
+ };
865
+ }
866
+ function buildBashOnlyExecutor(allowList) {
867
+ const allowSet = new Set(allowList);
868
+ return async (call) => {
869
+ if (!allowSet.has(call.tool_name)) {
870
+ return {
871
+ output: {
872
+ meter_note: `tool '${call.tool_name}' not in --allow-tool set; skipped`
873
+ },
874
+ is_error: false,
875
+ summary: `skipped (not allowed): ${call.tool_name}`
876
+ };
877
+ }
878
+ if (call.tool_name === "Bash") {
879
+ const input = call.tool_input;
880
+ const cmd = input?.command;
881
+ if (!cmd || typeof cmd !== "string") {
882
+ return {
883
+ output: { error: "missing command" },
884
+ is_error: true,
885
+ summary: "missing command"
886
+ };
887
+ }
888
+ if (/\brm\s+-[rRfF]|sudo\b|mkfs|dd\s+if=|--no-verify|>\s*\/dev\/sd/.test(cmd)) {
889
+ return {
890
+ output: { error: "command rejected as destructive" },
891
+ is_error: true,
892
+ summary: "destructive command rejected"
893
+ };
894
+ }
895
+ const { spawnSync } = await import("child_process");
896
+ const r = spawnSync("bash", ["-lc", cmd], {
897
+ encoding: "utf-8",
898
+ timeout: 3e4
899
+ });
900
+ return {
901
+ output: {
902
+ stdout: r.stdout?.slice(0, 8e3) ?? "",
903
+ stderr: r.stderr?.slice(0, 2e3) ?? "",
904
+ exit_code: r.status
905
+ },
906
+ is_error: (r.status ?? 0) !== 0,
907
+ summary: (r.stdout?.split("\n")[0] ?? "").slice(0, 200)
908
+ };
909
+ }
910
+ return {
911
+ output: { meter_note: `tool '${call.tool_name}' has no live executor; no-op` },
912
+ is_error: false,
913
+ summary: `no-op: ${call.tool_name}`
914
+ };
915
+ };
916
+ }
917
+ async function resolvePayload(opts) {
918
+ if (opts.text !== void 0) return { text: opts.text };
919
+ if (opts.payloadFile) {
920
+ const buf = await readFile(opts.payloadFile, "utf-8");
921
+ try {
922
+ return JSON.parse(buf);
923
+ } catch {
924
+ return { text: buf };
925
+ }
926
+ }
927
+ if (opts.payload) {
928
+ try {
929
+ return JSON.parse(opts.payload);
930
+ } catch {
931
+ return { text: opts.payload };
932
+ }
933
+ }
934
+ return null;
935
+ }
936
+ function buildAnthropicResponder(model) {
937
+ const apiKey = process.env.ANTHROPIC_API_KEY;
938
+ if (!apiKey) {
939
+ throw new Error(
940
+ "--live requires ANTHROPIC_API_KEY in the environment"
941
+ );
942
+ }
943
+ return /* @__PURE__ */ (() => {
944
+ return async (args) => {
945
+ const { Store: Store2 } = await import("@meterbility/collector");
946
+ const store = Store2.open();
947
+ try {
948
+ const fn = anthropicResponder(store, { apiKey, model });
949
+ return fn(args);
950
+ } finally {
951
+ store.close();
952
+ }
953
+ };
954
+ })();
955
+ }
956
+
957
+ // src/commands/diff.ts
958
+ import pc6 from "picocolors";
959
+ import { getRun as getRun2 } from "@meterbility/collector";
960
+ import { diffRuns } from "@meterbility/server";
961
+ function registerDiffCommand(program2) {
962
+ program2.command("diff <run-a> <run-b>").description("Side-by-side trajectory diff of two runs").option("--json", "Emit machine-readable JSON").option("--all", "Include shared rows (default: only diff rows)").action((runA, runB, opts) => {
963
+ const store = openStore();
964
+ try {
965
+ const a = getRun2(store, runA);
966
+ const b = getRun2(store, runB);
967
+ if (!a) throw new Error(`run not found: ${runA}`);
968
+ if (!b) throw new Error(`run not found: ${runB}`);
969
+ const result = diffRuns(store, a.run_id, b.run_id);
970
+ if (opts.json) {
971
+ console.log(JSON.stringify(result, null, 2));
972
+ return;
973
+ }
974
+ printDiff(result, opts.all ?? false);
975
+ } finally {
976
+ store.close();
977
+ }
978
+ });
979
+ }
980
+ function printDiff(d, includeShared) {
981
+ console.log(pc6.bold(`Diff: ${d.run_a_id.slice(0, 12)} vs ${d.run_b_id.slice(0, 12)}`));
982
+ console.log(
983
+ ` ${pc6.dim("shared prefix")} ${d.shared_prefix_length} step(s)
984
+ ${pc6.dim("divergence at")} ${d.first_divergence_sequence ?? "\u2014"}
985
+ ${pc6.dim("total A / B")} ${d.total_steps_a} / ${d.total_steps_b}`
986
+ );
987
+ console.log("");
988
+ for (const row of d.rows) {
989
+ if (!includeShared && row.kind === "shared") continue;
990
+ const color = pickColor(row.kind);
991
+ const a = row.a ? `${row.a.action_kind}${row.a.tool_name ? `(${row.a.tool_name})` : ""} \xB7 ${row.a.outcome_status}` : "\u2014";
992
+ const b = row.b ? `${row.b.action_kind}${row.b.tool_name ? `(${row.b.tool_name})` : ""} \xB7 ${row.b.outcome_status}` : "\u2014";
993
+ console.log(
994
+ ` ${String(row.sequence).padStart(4)} ${color(row.kind.padEnd(14))} ${a.padEnd(34)} ${pc6.dim("\u2502")} ${b}`
995
+ );
996
+ }
997
+ }
998
+ function pickColor(kind) {
999
+ switch (kind) {
1000
+ case "shared":
1001
+ return pc6.dim;
1002
+ case "context_diff":
1003
+ return pc6.yellow;
1004
+ case "decision_diff":
1005
+ return pc6.cyan;
1006
+ case "action_diff":
1007
+ return pc6.red;
1008
+ case "outcome_diff":
1009
+ return pc6.magenta;
1010
+ case "only_a":
1011
+ return pc6.red;
1012
+ case "only_b":
1013
+ return pc6.green;
1014
+ case "diverged":
1015
+ return pc6.magenta;
1016
+ default:
1017
+ return (s) => s;
1018
+ }
1019
+ }
1020
+
1021
+ // src/commands/annotate.ts
1022
+ import pc7 from "picocolors";
1023
+ import { getRun as getRun3, getStep as getStep2, insertAnnotation } from "@meterbility/collector";
1024
+ var VERDICTS = [
1025
+ "correct",
1026
+ "incorrect",
1027
+ "unclear",
1028
+ "good_decision",
1029
+ "bad_decision"
1030
+ ];
1031
+ function registerAnnotateCommand(program2) {
1032
+ program2.command("annotate <target>").description("Attach a verdict + note to a step or run").option("--verdict <v>", `One of: ${VERDICTS.join(", ")}`).option("--note <text>", "Free-form note").option("--author <name>", "Author name", process.env.USER ?? "anonymous").action(async (target, opts) => {
1033
+ const store = openStore();
1034
+ try {
1035
+ let kind;
1036
+ let resolvedId;
1037
+ if (target.startsWith("stp_")) {
1038
+ const step = getStep2(store, target);
1039
+ if (!step) throw new Error(`step not found: ${target}`);
1040
+ kind = "step";
1041
+ resolvedId = step.step_id;
1042
+ } else if (target.startsWith("run_")) {
1043
+ const run = getRun3(store, target);
1044
+ if (!run) throw new Error(`run not found: ${target}`);
1045
+ kind = "run";
1046
+ resolvedId = run.run_id;
1047
+ } else {
1048
+ throw new Error(
1049
+ "target must be a step id (stp_\u2026) or run id (run_\u2026)"
1050
+ );
1051
+ }
1052
+ if (opts.verdict && !VERDICTS.includes(opts.verdict)) {
1053
+ throw new Error(
1054
+ `invalid verdict: ${opts.verdict}
1055
+ allowed: ${VERDICTS.join(", ")}`
1056
+ );
1057
+ }
1058
+ const ann = insertAnnotation(store, {
1059
+ targetKind: kind,
1060
+ targetId: resolvedId,
1061
+ author: opts.author,
1062
+ verdict: opts.verdict,
1063
+ note: opts.note
1064
+ });
1065
+ console.log(
1066
+ `${pc7.green("annotated")} ${kind}=${target.slice(0, 12)} ${pc7.dim(ann.annotation_id)}`
1067
+ );
1068
+ } finally {
1069
+ store.close();
1070
+ }
1071
+ });
1072
+ }
1073
+
1074
+ // src/commands/web.ts
1075
+ import pc8 from "picocolors";
1076
+ import { serveApp, SlackNotifier } from "@meterbility/server";
1077
+ import { resolveSetting, getSetting as getSetting2 } from "@meterbility/collector";
1078
+ function registerWebCommand(program2) {
1079
+ program2.command("web").description("Serve the Meterbility web UI on a local port").option("-p, --port <n>", "Port", (v) => parseInt(v, 10), 4317).option("-h, --host <addr>", "Host", "127.0.0.1").option("--no-open", "Do not auto-open the browser").option(
1080
+ "--live",
1081
+ "Watch ~/.claude/projects for live agent activity (fleet view + SSE)"
1082
+ ).option(
1083
+ "--watch-tool <name>",
1084
+ "Fire an alert when this tool is invoked (repeatable)",
1085
+ (val, prev = []) => [...prev, val],
1086
+ []
1087
+ ).option(
1088
+ "--stall-seconds <n>",
1089
+ "Stall alert threshold in seconds",
1090
+ (v) => parseInt(v, 10),
1091
+ 120
1092
+ ).option(
1093
+ "--slack-webhook <url>",
1094
+ "Slack incoming-webhook URL (or set METERBILITY_SLACK_WEBHOOK)"
1095
+ ).option(
1096
+ "--slack-event <kind>",
1097
+ "Slack event type to forward (repeatable). Default: alert. Other values: run:created, run:completed",
1098
+ (v, prev = []) => [...prev, v],
1099
+ []
1100
+ ).option(
1101
+ "--allow-unauth-bind",
1102
+ "Allow binding to a non-loopback host without web.bind_token set. Per SPEC-V0_3 \xA711 this requires explicit opt-in \u2014 the default behavior is to refuse, since anyone on the network can read all runs and step blobs through the unauthenticated /api/* surface."
1103
+ ).action(async (opts) => {
1104
+ const store = openStore();
1105
+ const isLoopback = opts.host === "127.0.0.1" || opts.host === "::1" || opts.host === "localhost" || opts.host.startsWith("127.");
1106
+ if (!isLoopback) {
1107
+ const bindToken = getSetting2(store, "web.bind_token");
1108
+ if (!bindToken && !opts.allowUnauthBind) {
1109
+ console.error(
1110
+ pc8.red("refusing to bind: ") + `--host=${opts.host} is non-loopback and web.bind_token is not set.
1111
+ ` + pc8.dim(
1112
+ " Anyone on this network would be able to read every run and step blob via /api/*.\n Fix either:\n \u2022 meter config set web.bind_token <random-string>\n \u2022 or re-run with --allow-unauth-bind to opt into the insecure config.\n See SPEC-V0_3 \xA711."
1113
+ )
1114
+ );
1115
+ store.close();
1116
+ process.exit(1);
1117
+ }
1118
+ console.warn(
1119
+ pc8.yellow("\u26A0 binding to ") + pc8.yellow(`${opts.host}:${opts.port}`) + pc8.yellow(" \u2014 non-loopback exposure")
1120
+ );
1121
+ if (!bindToken) {
1122
+ console.warn(
1123
+ pc8.yellow(
1124
+ " /api/* is UNAUTHENTICATED. Anyone on this network can read all runs and step blobs."
1125
+ )
1126
+ );
1127
+ } else {
1128
+ console.warn(
1129
+ pc8.dim(
1130
+ " /api/* is gated by web.bind_token. Pass it as `Authorization: Bearer <token>` from clients."
1131
+ )
1132
+ );
1133
+ }
1134
+ }
1135
+ const watchToolsEffective = opts.watchTool.length > 0 ? opts.watchTool : (getSetting2(store, "live.watch_tools") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
1136
+ const stallFromSetting = getSetting2(store, "live.stall_seconds");
1137
+ const stallSecondsEffective = opts.stallSeconds !== 120 ? opts.stallSeconds : stallFromSetting ? parseInt(stallFromSetting, 10) || opts.stallSeconds : opts.stallSeconds;
1138
+ const { url, live } = serveApp(store, {
1139
+ port: opts.port,
1140
+ host: opts.host,
1141
+ live: opts.live === true,
1142
+ liveOptions: {
1143
+ watchTools: watchToolsEffective,
1144
+ stallSeconds: stallSecondsEffective
1145
+ }
1146
+ });
1147
+ console.log(pc8.green("Meterbility running at ") + pc8.cyan(url));
1148
+ if (live) {
1149
+ console.log(
1150
+ pc8.dim(
1151
+ `live mode on \u2014 watching Claude Code sessions every ${1500}ms. Watching tools: ${watchToolsEffective.join(", ") || "(none)"}.`
1152
+ )
1153
+ );
1154
+ const webhook = opts.slackWebhook ?? resolveSetting(store, "slack.webhook", "METERBILITY_SLACK_WEBHOOK") ?? "";
1155
+ const slackEventsFromSetting = getSetting2(
1156
+ store,
1157
+ "slack.default_events"
1158
+ );
1159
+ if (webhook) {
1160
+ try {
1161
+ const fallbackEvents = slackEventsFromSetting ? slackEventsFromSetting.split(",").map((s) => s.trim()).filter(Boolean) : ["alert"];
1162
+ const events = opts.slackEvent.length > 0 ? opts.slackEvent : fallbackEvents;
1163
+ const slack = new SlackNotifier({
1164
+ webhookUrl: webhook,
1165
+ serverUrl: url,
1166
+ events
1167
+ });
1168
+ slack.attach(live);
1169
+ console.log(
1170
+ pc8.dim(
1171
+ `slack notifications enabled \xB7 forwarding: ${events.join(", ")}`
1172
+ )
1173
+ );
1174
+ } catch (err) {
1175
+ console.error(
1176
+ pc8.red("slack disabled: ") + err.message
1177
+ );
1178
+ }
1179
+ }
1180
+ live.on("data", (e) => {
1181
+ if (e.type === "alert") {
1182
+ console.log(
1183
+ pc8.yellow(`alert[${e.kind}]`) + ` ${e.run_id.slice(0, 12)} \xB7 ${e.message}`
1184
+ );
1185
+ } else if (e.type === "run:created") {
1186
+ console.log(
1187
+ pc8.blue("run:created") + ` ${e.run.run_id.slice(0, 12)} \xB7 ${e.run.title ?? ""}`
1188
+ );
1189
+ } else if (e.type === "run:completed") {
1190
+ console.log(
1191
+ pc8.green("run:completed") + ` ${e.run.run_id.slice(0, 12)} \xB7 status=${e.run.status}`
1192
+ );
1193
+ }
1194
+ });
1195
+ }
1196
+ console.log(pc8.dim("press ctrl-c to stop"));
1197
+ if (opts.open !== false) {
1198
+ await openBrowser(url);
1199
+ }
1200
+ });
1201
+ }
1202
+ async function openBrowser(url) {
1203
+ const { spawn: spawn3 } = await import("child_process");
1204
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
1205
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
1206
+ spawn3(cmd, args, { detached: true, stdio: "ignore" }).unref();
1207
+ }
1208
+
1209
+ // src/commands/doctor.ts
1210
+ import { existsSync as existsSync2 } from "fs";
1211
+ import { stat } from "fs/promises";
1212
+ import pc9 from "picocolors";
1213
+ import { claudeHome, claudeProjectsRoot, dbPath, meterHome } from "@meterbility/shared";
1214
+ import { discoverSessions as discoverSessions2 } from "@meterbility/claude-code-adapter";
1215
+ function registerDoctorCommand(program2) {
1216
+ program2.command("doctor").description("Verify Meterbility can capture and store agent runs (Gate 2 check)").option("--json", "Emit results as a single JSON object instead of pretty output").action(async (opts) => {
1217
+ const checks = [];
1218
+ const record = (status, label, detail = "") => {
1219
+ checks.push({ status, label, detail });
1220
+ };
1221
+ const node = process.versions.node;
1222
+ const [major, minor] = node.split(".").map(Number);
1223
+ if (major > 20 || major === 20 && minor >= 6 || major >= 22) {
1224
+ record("ok", "Node", `v${node}`);
1225
+ } else if (major >= 20) {
1226
+ record(
1227
+ "warn",
1228
+ "Node",
1229
+ `v${node} \u2014 upgrade to 20.6+ for full --import support`
1230
+ );
1231
+ } else {
1232
+ record("fail", "Node version >= 20.6", `found v${node}`);
1233
+ }
1234
+ record("ok", "METERBILITY_HOME", meterHome());
1235
+ if (existsSync2(claudeHome())) {
1236
+ record("ok", "CLAUDE_HOME", claudeHome());
1237
+ } else {
1238
+ record(
1239
+ "fail",
1240
+ "CLAUDE_HOME",
1241
+ `not found at ${claudeHome()} \u2014 set CLAUDE_HOME or install Claude Code`
1242
+ );
1243
+ }
1244
+ if (existsSync2(claudeProjectsRoot())) {
1245
+ record("ok", "Claude projects dir", claudeProjectsRoot());
1246
+ } else {
1247
+ record(
1248
+ "warn",
1249
+ "Claude projects dir",
1250
+ `no ${claudeProjectsRoot()} \u2014 nothing to ingest yet`
1251
+ );
1252
+ }
1253
+ try {
1254
+ const sessions = await discoverSessions2();
1255
+ if (sessions.length === 0) {
1256
+ record("warn", "Session discovery", "no .jsonl session files found");
1257
+ } else {
1258
+ const newest = sessions[0];
1259
+ record(
1260
+ "ok",
1261
+ "Session discovery",
1262
+ `${sessions.length} session(s) \u2014 newest ${newest.session_id.slice(0, 8)} (${(newest.size_bytes / 1024).toFixed(0)}KB)`
1263
+ );
1264
+ }
1265
+ } catch (err) {
1266
+ record("fail", "Session discovery", err.message);
1267
+ }
1268
+ try {
1269
+ const { Store: Store2 } = await import("@meterbility/collector");
1270
+ const store = Store2.open();
1271
+ store.close();
1272
+ const s = await stat(dbPath());
1273
+ record("ok", "SQLite store", `${dbPath()} (${s.size} bytes)`);
1274
+ } catch (err) {
1275
+ record("fail", "SQLite store", err.message);
1276
+ }
1277
+ const summary = checks.reduce(
1278
+ (acc, c) => {
1279
+ acc[c.status] += 1;
1280
+ return acc;
1281
+ },
1282
+ { ok: 0, warn: 0, fail: 0 }
1283
+ );
1284
+ if (opts.json) {
1285
+ const payload = {
1286
+ meter_home: meterHome(),
1287
+ claude_home: claudeHome(),
1288
+ claude_projects_root: claudeProjectsRoot(),
1289
+ db_path: dbPath(),
1290
+ node: process.versions.node,
1291
+ checks,
1292
+ summary,
1293
+ ok: summary.fail === 0
1294
+ };
1295
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
1296
+ if (summary.fail > 0) process.exit(1);
1297
+ return;
1298
+ }
1299
+ for (const c of checks) {
1300
+ const icon = c.status === "ok" ? pc9.green("\u2714") : c.status === "warn" ? pc9.yellow("\u26A0") : pc9.red("\u2716");
1301
+ const tag = c.status === "ok" ? pc9.green("PASS") : c.status === "warn" ? pc9.yellow("WARN") : pc9.red("FAIL");
1302
+ console.log(`${icon} [${tag}] ${pc9.bold(c.label)} ${pc9.dim(c.detail)}`);
1303
+ }
1304
+ console.log("");
1305
+ console.log(
1306
+ pc9.bold(
1307
+ `${pc9.green(String(summary.ok))} pass \xB7 ${pc9.yellow(String(summary.warn))} warn \xB7 ${pc9.red(String(summary.fail))} fail`
1308
+ )
1309
+ );
1310
+ if (summary.fail > 0) process.exit(1);
1311
+ });
1312
+ }
1313
+
1314
+ // src/commands/export.ts
1315
+ import { writeFile } from "fs/promises";
1316
+ import pc10 from "picocolors";
1317
+ import {
1318
+ getBaselineTree,
1319
+ getRun as getRun4,
1320
+ getSetting as getSetting3,
1321
+ listFileChanges as listFileChanges2,
1322
+ listSteps as listSteps2,
1323
+ resolveSnapshotBlobRef as resolveSnapshotBlobRef2
1324
+ } from "@meterbility/collector";
1325
+ import { TRACE_FORMAT_VERSION } from "@meterbility/spec";
1326
+ function registerExportCommand(program2) {
1327
+ program2.command("export <run-id>").description("Export a run to the open Meterbility Trace Format (0.3.0)").option("-o, --output <path>", "Output file (default: stdout)").option(
1328
+ "--no-blobs",
1329
+ "Skip inlining blob contents (refs only \u2014 much smaller)"
1330
+ ).option(
1331
+ "--include-file-blobs",
1332
+ "Inline file-content blobs in the export (default: omit \u2014 bug reports get shared)"
1333
+ ).action(async (runId, opts) => {
1334
+ const store = openStore();
1335
+ try {
1336
+ const run = getRun4(store, runId);
1337
+ if (!run) throw new Error(`run not found: ${runId}`);
1338
+ const steps = listSteps2(store, run.run_id);
1339
+ const file_changes = listFileChanges2(store, { runId: run.run_id });
1340
+ const baseline_trees = run.baseline_tree_id ? [getBaselineTree(store, run.baseline_tree_id)].filter(
1341
+ (bt) => !!bt
1342
+ ) : [];
1343
+ const trace = {
1344
+ meter_trace_version: TRACE_FORMAT_VERSION,
1345
+ run,
1346
+ steps,
1347
+ file_changes,
1348
+ baseline_trees
1349
+ };
1350
+ if (opts.blobs !== false) {
1351
+ const includeFileBlobs = opts.includeFileBlobs ?? getSetting3(store, "export.include_file_blobs") === "true";
1352
+ const blobs = {};
1353
+ const fileBlobRefs = /* @__PURE__ */ new Set();
1354
+ for (const fc of file_changes) {
1355
+ if (fc.before_blob_ref) fileBlobRefs.add(fc.before_blob_ref);
1356
+ if (fc.after_blob_ref) fileBlobRefs.add(fc.after_blob_ref);
1357
+ }
1358
+ const structuralRefs = /* @__PURE__ */ new Set();
1359
+ for (const s of steps) {
1360
+ structuralRefs.add(
1361
+ resolveSnapshotBlobRef2(store, s.context_snapshot_id)
1362
+ );
1363
+ structuralRefs.add(s.decision_ref);
1364
+ if (s.outcome.tool_result_ref) {
1365
+ structuralRefs.add(s.outcome.tool_result_ref);
1366
+ }
1367
+ }
1368
+ for (const bt of baseline_trees) {
1369
+ if (bt.manifest_blob_ref) {
1370
+ structuralRefs.add(bt.manifest_blob_ref);
1371
+ }
1372
+ }
1373
+ const refsToInline = new Set(structuralRefs);
1374
+ if (includeFileBlobs) {
1375
+ for (const r of fileBlobRefs) refsToInline.add(r);
1376
+ }
1377
+ for (const r of refsToInline) {
1378
+ const text = await store.blobs.tryGetString(r);
1379
+ if (text !== void 0) {
1380
+ blobs[r] = Buffer.from(text, "utf-8").toString("base64");
1381
+ }
1382
+ }
1383
+ trace.blobs = blobs;
1384
+ }
1385
+ const json = JSON.stringify(trace, null, 2);
1386
+ if (opts.output) {
1387
+ await writeFile(opts.output, json);
1388
+ console.log(
1389
+ `${pc10.green("exported")} ${runId.slice(0, 12)} \u2192 ${opts.output} ${pc10.dim(`(${(json.length / 1024).toFixed(1)}KB \xB7 ${file_changes.length} file changes)`)}`
1390
+ );
1391
+ } else {
1392
+ process.stdout.write(json + "\n");
1393
+ }
1394
+ } finally {
1395
+ store.close();
1396
+ }
1397
+ });
1398
+ }
1399
+
1400
+ // src/commands/test.ts
1401
+ import { writeFile as writeFile2, readFile as readFile2 } from "fs/promises";
1402
+ import pc11 from "picocolors";
1403
+ import { getRun as getRun5, listSteps as listSteps3, listRuns as listRuns2 } from "@meterbility/collector";
1404
+ import {
1405
+ addAssertion,
1406
+ createTest,
1407
+ deleteTest,
1408
+ deriveAssertionsFromRun,
1409
+ getTestByName,
1410
+ listResults,
1411
+ listTests,
1412
+ runTest
1413
+ } from "@meterbility/server";
1414
+ var KINDS = [
1415
+ "includes_tool_call",
1416
+ "excludes_tool_call",
1417
+ "tool_call_count",
1418
+ "output_contains",
1419
+ "output_does_not_contain",
1420
+ "min_steps",
1421
+ "max_steps",
1422
+ "final_status",
1423
+ "max_cost_cents",
1424
+ "no_error_step",
1425
+ "step_status_at"
1426
+ ];
1427
+ function registerTestCommand(program2) {
1428
+ const test = program2.command("test").description("Regression tests \u2014 assertions over captured runs");
1429
+ test.command("list").description("List defined regression tests").action(() => {
1430
+ const store = openStore();
1431
+ try {
1432
+ const tests = listTests(store);
1433
+ if (tests.length === 0) {
1434
+ console.log(pc11.dim("no tests defined. Try: meter test create <name> --from <run-id>"));
1435
+ return;
1436
+ }
1437
+ for (const t of tests) {
1438
+ console.log(
1439
+ ` ${pc11.cyan(t.name.padEnd(28))} ${pc11.dim(t.assertions.length + " assertions")} ${pc11.dim(t.canonical_run_id?.slice(0, 12) ?? "")} ${t.description ?? ""}`
1440
+ );
1441
+ }
1442
+ } finally {
1443
+ store.close();
1444
+ }
1445
+ });
1446
+ test.command("create <name>").description("Create a regression test, optionally derived from a canonical run").option("--from <run-id>", "Derive assertions from this run").option("--description <text>", "Free-form description").option("--from-file <path>", "Load assertions from a JSON file").action(async (name, opts) => {
1447
+ const store = openStore();
1448
+ try {
1449
+ let assertions = [];
1450
+ let canonicalRunId;
1451
+ if (opts.from) {
1452
+ const run = getRun5(store, opts.from);
1453
+ if (!run) throw new Error(`run not found: ${opts.from}`);
1454
+ const steps = listSteps3(store, run.run_id);
1455
+ assertions = deriveAssertionsFromRun(run, steps);
1456
+ canonicalRunId = run.run_id;
1457
+ }
1458
+ if (opts.fromFile) {
1459
+ const buf = await readFile2(opts.fromFile, "utf-8");
1460
+ assertions = JSON.parse(buf);
1461
+ }
1462
+ const t = createTest(store, {
1463
+ name,
1464
+ description: opts.description,
1465
+ assertions,
1466
+ canonical_run_id: canonicalRunId
1467
+ });
1468
+ console.log(
1469
+ `${pc11.green("created")} ${t.name} ${pc11.dim(t.assertions.length + " assertions")}`
1470
+ );
1471
+ for (const a of t.assertions) {
1472
+ console.log(
1473
+ ` ${pc11.dim("\xB7")} ${a.kind} ${pc11.cyan(String(a.value))}${a.label ? pc11.dim(` (${a.label})`) : ""}`
1474
+ );
1475
+ }
1476
+ } finally {
1477
+ store.close();
1478
+ }
1479
+ });
1480
+ test.command("add-assertion <test-name> <kind> <value>").description(
1481
+ `Append an assertion to a test. Kinds: ${KINDS.join(", ")}.`
1482
+ ).option("--at <seq>", "Step sequence (for step_status_at)", (v) => parseInt(v, 10)).option("--label <text>", "Friendly label").action((name, kind, value, opts) => {
1483
+ if (!KINDS.includes(kind)) {
1484
+ throw new Error(`unknown kind: ${kind}
1485
+ allowed: ${KINDS.join(", ")}`);
1486
+ }
1487
+ const store = openStore();
1488
+ try {
1489
+ const numericValue = /^[\d.]+$/.test(value) && !["final_status"].includes(kind) ? Number(value) : value;
1490
+ const t = addAssertion(store, name, {
1491
+ kind,
1492
+ value: numericValue,
1493
+ at: opts.at,
1494
+ label: opts.label
1495
+ });
1496
+ console.log(
1497
+ `${pc11.green("added")} ${t.assertions.length} assertions on ${t.name}`
1498
+ );
1499
+ } finally {
1500
+ store.close();
1501
+ }
1502
+ });
1503
+ test.command("rm <name>").description("Delete a regression test").action((name) => {
1504
+ const store = openStore();
1505
+ try {
1506
+ if (!deleteTest(store, name)) throw new Error(`test not found: ${name}`);
1507
+ console.log(`${pc11.yellow("deleted")} ${name}`);
1508
+ } finally {
1509
+ store.close();
1510
+ }
1511
+ });
1512
+ test.command("run <test-name> [run-id]").description(
1513
+ "Run a test against a specific run, or against every captured run if no id is given"
1514
+ ).option("--limit <n>", "Cap runs scanned", (v) => parseInt(v, 10), 100).option("--json", "Emit machine-readable JSON").action((testName, runId, opts) => {
1515
+ const store = openStore();
1516
+ try {
1517
+ const t = getTestByName(store, testName);
1518
+ if (!t) throw new Error(`test not found: ${testName}`);
1519
+ let runs;
1520
+ if (runId) {
1521
+ const r = getRun5(store, runId);
1522
+ if (!r) throw new Error(`run not found: ${runId}`);
1523
+ runs = [r];
1524
+ } else {
1525
+ runs = listRuns2(store, { limit: opts.limit });
1526
+ }
1527
+ const results = runs.map((r) => runTest(store, t, r.run_id));
1528
+ if (opts.json) {
1529
+ console.log(JSON.stringify(results, null, 2));
1530
+ return;
1531
+ }
1532
+ let pass = 0;
1533
+ let fail = 0;
1534
+ for (const r of results) {
1535
+ const tag = r.passed ? pc11.green("PASS") : pc11.red("FAIL");
1536
+ console.log(
1537
+ `${tag} ${r.run_id.slice(0, 12)} ${pc11.dim(`${r.assertions.filter((a) => a.passed).length}/${r.assertions.length} assertions`)}`
1538
+ );
1539
+ if (!r.passed) {
1540
+ for (const a of r.assertions.filter((x) => !x.passed)) {
1541
+ console.log(
1542
+ ` ${pc11.red("\u2716")} ${pc11.dim(a.assertion.kind)} ${a.reason}`
1543
+ );
1544
+ }
1545
+ }
1546
+ if (r.passed) pass += 1;
1547
+ else fail += 1;
1548
+ }
1549
+ console.log(
1550
+ pc11.bold(
1551
+ `
1552
+ ${results.length} run(s) checked \xB7 ${pc11.green(`${pass} pass`)} \xB7 ${pc11.red(`${fail} fail`)}`
1553
+ )
1554
+ );
1555
+ if (fail > 0) process.exit(1);
1556
+ } finally {
1557
+ store.close();
1558
+ }
1559
+ });
1560
+ test.command("show <name>").description("Show a test's assertions").action((name) => {
1561
+ const store = openStore();
1562
+ try {
1563
+ const t = getTestByName(store, name);
1564
+ if (!t) throw new Error(`test not found: ${name}`);
1565
+ console.log(pc11.bold(t.name));
1566
+ if (t.description) console.log(` ${pc11.dim(t.description)}`);
1567
+ if (t.canonical_run_id)
1568
+ console.log(` ${pc11.dim("canonical run:")} ${t.canonical_run_id}`);
1569
+ console.log(` ${pc11.dim("created:")} ${t.created_at}`);
1570
+ for (const a of t.assertions) {
1571
+ console.log(
1572
+ ` \xB7 ${pc11.cyan(a.kind)} ${pc11.bold(String(a.value))}${a.at !== void 0 ? pc11.dim(` at=${a.at}`) : ""}${a.label ? pc11.dim(` (${a.label})`) : ""}`
1573
+ );
1574
+ }
1575
+ } finally {
1576
+ store.close();
1577
+ }
1578
+ });
1579
+ test.command("results [test-name]").description("Show recent regression results").option("-n, --limit <n>", "max rows", (v) => parseInt(v, 10), 25).action((name, opts) => {
1580
+ const store = openStore();
1581
+ try {
1582
+ const testId = name ? getTestByName(store, name)?.test_id : void 0;
1583
+ if (name && !testId) throw new Error(`test not found: ${name}`);
1584
+ const results = listResults(store, testId, opts.limit);
1585
+ if (results.length === 0) {
1586
+ console.log(pc11.dim("no results yet"));
1587
+ return;
1588
+ }
1589
+ for (const r of results) {
1590
+ console.log(
1591
+ ` ${r.passed ? pc11.green("PASS") : pc11.red("FAIL")} ${pc11.dim(r.created_at)} ${pc11.cyan(r.test_name.padEnd(20))} ${r.run_id.slice(0, 12)}`
1592
+ );
1593
+ }
1594
+ } finally {
1595
+ store.close();
1596
+ }
1597
+ });
1598
+ test.command("export <name>").description("Export a test as a JSON file (round-trip via --from-file)").option("-o, --output <path>", "Output file (default: stdout)").action(async (name, opts) => {
1599
+ const store = openStore();
1600
+ try {
1601
+ const t = getTestByName(store, name);
1602
+ if (!t) throw new Error(`test not found: ${name}`);
1603
+ const json = JSON.stringify(t.assertions, null, 2);
1604
+ if (opts.output) {
1605
+ await writeFile2(opts.output, json);
1606
+ console.log(`${pc11.green("exported")} ${name} \u2192 ${opts.output}`);
1607
+ } else {
1608
+ process.stdout.write(json + "\n");
1609
+ }
1610
+ } finally {
1611
+ store.close();
1612
+ }
1613
+ });
1614
+ }
1615
+
1616
+ // src/commands/db.ts
1617
+ import pc12 from "picocolors";
1618
+ import { resolveSetting as resolveSetting2 } from "@meterbility/collector";
1619
+ function resolvePostgresUrl(store, flag) {
1620
+ if (flag) return flag;
1621
+ return resolveSetting2(store, "postgres.url", "METERBILITY_DB_URL");
1622
+ }
1623
+ function registerDbCommand(program2) {
1624
+ const db = program2.command("db").description("Hosted backend management (Postgres, optional)");
1625
+ db.command("postgres-init").description(
1626
+ "Connect to a Postgres URL and ensure Meterbility's schema is present."
1627
+ ).option(
1628
+ "--url <conn>",
1629
+ "Postgres connection URL (defaults to METERBILITY_DB_URL or postgres.url setting)"
1630
+ ).action(async (opts) => {
1631
+ const { PostgresStore } = await import("@meterbility/store-postgres");
1632
+ const sqlite = openStore();
1633
+ let store;
1634
+ try {
1635
+ store = await PostgresStore.open({
1636
+ url: resolvePostgresUrl(sqlite, opts.url)
1637
+ });
1638
+ } finally {
1639
+ sqlite.close();
1640
+ }
1641
+ try {
1642
+ const r = await store.client.query(
1643
+ "SELECT value FROM meta WHERE key='schema_version'"
1644
+ );
1645
+ console.log(
1646
+ `${pc12.green("connected")} schema_version=${r.rows[0]?.value ?? "?"}`
1647
+ );
1648
+ } finally {
1649
+ await store.close();
1650
+ }
1651
+ });
1652
+ db.command("postgres-sync").description(
1653
+ "Copy local SQLite runs / steps / blobs into Postgres. Idempotent."
1654
+ ).option(
1655
+ "--url <conn>",
1656
+ "Postgres URL (defaults to METERBILITY_DB_URL or postgres.url setting)"
1657
+ ).option(
1658
+ "--limit <n>",
1659
+ "Cap runs synced (default 1000)",
1660
+ (v) => parseInt(v, 10)
1661
+ ).action(async (opts) => {
1662
+ const { PostgresStore, syncSqliteToPostgres } = await import("@meterbility/store-postgres");
1663
+ const sqlite = openStore();
1664
+ const postgres = await PostgresStore.open({
1665
+ url: resolvePostgresUrl(sqlite, opts.url)
1666
+ });
1667
+ try {
1668
+ const t0 = Date.now();
1669
+ const r = await syncSqliteToPostgres(sqlite, postgres, {
1670
+ limitRuns: opts.limit
1671
+ });
1672
+ const dt = Date.now() - t0;
1673
+ console.log(
1674
+ pc12.bold(
1675
+ `synced ${r.runs} runs \xB7 ${r.steps} steps \xB7 ${r.blobs} blobs (${(r.bytes / 1024).toFixed(1)}KB) in ${dt}ms`
1676
+ )
1677
+ );
1678
+ } finally {
1679
+ sqlite.close();
1680
+ await postgres.close();
1681
+ }
1682
+ });
1683
+ }
1684
+
1685
+ // src/commands/slack.ts
1686
+ import pc13 from "picocolors";
1687
+ import { SlackNotifier as SlackNotifier2 } from "@meterbility/server";
1688
+ function registerSlackCommand(program2) {
1689
+ const slack = program2.command("slack").description("Slack integration utilities");
1690
+ slack.command("test").description("Send a one-off Slack message to verify the webhook").option(
1691
+ "--webhook <url>",
1692
+ "Webhook URL (or set METERBILITY_SLACK_WEBHOOK)"
1693
+ ).action(async (opts) => {
1694
+ const url = opts.webhook ?? process.env.METERBILITY_SLACK_WEBHOOK;
1695
+ if (!url) {
1696
+ throw new Error(
1697
+ "missing --webhook or METERBILITY_SLACK_WEBHOOK environment variable"
1698
+ );
1699
+ }
1700
+ const n = new SlackNotifier2({ webhookUrl: url });
1701
+ await n.sendTest();
1702
+ console.log(`${pc13.green("ok")} test message sent`);
1703
+ });
1704
+ }
1705
+
1706
+ // src/commands/config.ts
1707
+ import pc14 from "picocolors";
1708
+ import {
1709
+ deleteSetting,
1710
+ getSetting as getSetting4,
1711
+ isSecret,
1712
+ listSettings,
1713
+ maskSecret,
1714
+ setSetting
1715
+ } from "@meterbility/collector";
1716
+ var KNOWN_KEYS = [
1717
+ "slack.webhook",
1718
+ "slack.default_events",
1719
+ "live.watch_tools",
1720
+ "live.stall_seconds",
1721
+ "fork.default_model",
1722
+ "fork.default_max_iterations",
1723
+ "anthropic.api_key",
1724
+ "postgres.url"
1725
+ ];
1726
+ function registerConfigCommand(program2) {
1727
+ const config = program2.command("config").description(
1728
+ "Get/set persisted Meterbility settings (mirrors the web UI's Settings page)"
1729
+ );
1730
+ config.command("list").alias("ls").description("List all configured settings (secrets masked)").option("--reveal", "Show secret values in plaintext").option("--json", "Emit JSON for scripting").action((opts) => {
1731
+ const store = openStore();
1732
+ try {
1733
+ const rows = listSettings(store);
1734
+ if (opts.json) {
1735
+ const payload = rows.map((r) => ({
1736
+ key: r.key,
1737
+ value: opts.reveal || !isSecret(r.key) ? r.value : maskSecret(r.value),
1738
+ updated_at: r.updated_at,
1739
+ secret: isSecret(r.key)
1740
+ }));
1741
+ console.log(JSON.stringify(payload, null, 2));
1742
+ return;
1743
+ }
1744
+ if (rows.length === 0) {
1745
+ console.log(pc14.dim("(no settings configured)"));
1746
+ console.log(
1747
+ pc14.dim(
1748
+ " set one with: meter config set <key> <value>\n known keys: " + KNOWN_KEYS.join(", ")
1749
+ )
1750
+ );
1751
+ return;
1752
+ }
1753
+ for (const r of rows) {
1754
+ const displayed = opts.reveal || !isSecret(r.key) ? r.value : maskSecret(r.value);
1755
+ const tag = isSecret(r.key) ? pc14.yellow(" [secret]") : "";
1756
+ console.log(
1757
+ ` ${pc14.cyan(r.key.padEnd(28))} ${displayed}${tag} ${pc14.dim(r.updated_at)}`
1758
+ );
1759
+ }
1760
+ } finally {
1761
+ store.close();
1762
+ }
1763
+ });
1764
+ config.command("get <key>").description("Print a single setting value (raw, no masking)").action((key) => {
1765
+ const store = openStore();
1766
+ try {
1767
+ const v = getSetting4(store, key);
1768
+ if (v === void 0) {
1769
+ console.error(pc14.red(`not set: ${key}`));
1770
+ process.exit(1);
1771
+ }
1772
+ process.stdout.write(v + "\n");
1773
+ } finally {
1774
+ store.close();
1775
+ }
1776
+ });
1777
+ config.command("set <key> <value>").description(
1778
+ `Persist a setting. Known keys: ${KNOWN_KEYS.join(", ")}`
1779
+ ).action((key, value) => {
1780
+ const store = openStore();
1781
+ try {
1782
+ setSetting(store, key, value);
1783
+ const display = isSecret(key) ? maskSecret(value) : value;
1784
+ console.log(
1785
+ `${pc14.green("set")} ${pc14.cyan(key)} = ${display}${isSecret(key) ? pc14.yellow(" [secret]") : ""}`
1786
+ );
1787
+ if (!KNOWN_KEYS.includes(key)) {
1788
+ console.log(
1789
+ pc14.dim(
1790
+ ` note: '${key}' is not a known SettingKey \u2014 saved anyway. Known keys: ${KNOWN_KEYS.join(", ")}`
1791
+ )
1792
+ );
1793
+ }
1794
+ } finally {
1795
+ store.close();
1796
+ }
1797
+ });
1798
+ config.command("rm <key>").alias("unset").description("Delete a setting").action((key) => {
1799
+ const store = openStore();
1800
+ try {
1801
+ const existing = getSetting4(store, key);
1802
+ if (existing === void 0) {
1803
+ console.error(pc14.red(`not set: ${key}`));
1804
+ process.exit(1);
1805
+ }
1806
+ deleteSetting(store, key);
1807
+ console.log(`${pc14.green("removed")} ${pc14.cyan(key)}`);
1808
+ } finally {
1809
+ store.close();
1810
+ }
1811
+ });
1812
+ }
1813
+
1814
+ // src/commands/watch.ts
1815
+ import pc15 from "picocolors";
1816
+ import {
1817
+ LiveInspector
1818
+ } from "@meterbility/server";
1819
+ import { getSetting as getSetting5 } from "@meterbility/collector";
1820
+ function registerWatchCommand(program2) {
1821
+ program2.command("watch").description(
1822
+ "Stream live agent activity from ~/.claude/projects to the terminal"
1823
+ ).option(
1824
+ "--watch-tool <name>",
1825
+ "Fire an alert when this tool is invoked (repeatable)",
1826
+ (val, prev = []) => [...prev, val],
1827
+ []
1828
+ ).option(
1829
+ "--stall-seconds <n>",
1830
+ "Stall alert threshold in seconds",
1831
+ (v) => parseInt(v, 10),
1832
+ 120
1833
+ ).option(
1834
+ "--filter <kinds>",
1835
+ "Comma-separated event kinds to keep (alert,run:created,run:updated,run:completed,fleet:snapshot)"
1836
+ ).option("--run <id>", "Filter to a single run id (or its 12-char prefix)").option(
1837
+ "--no-snapshot",
1838
+ "Suppress the periodic fleet:snapshot events (they're noisy)"
1839
+ ).option("--json", "Emit each event as one JSON line (newline-delimited)").action(
1840
+ async (opts) => {
1841
+ const store = openStore();
1842
+ const watchToolsEffective = opts.watchTool.length > 0 ? opts.watchTool : (getSetting5(store, "live.watch_tools") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
1843
+ const stallFromSetting = getSetting5(store, "live.stall_seconds");
1844
+ const stallSecondsEffective = opts.stallSeconds !== 120 ? opts.stallSeconds : stallFromSetting ? parseInt(stallFromSetting, 10) || opts.stallSeconds : opts.stallSeconds;
1845
+ const allowedKinds = opts.filter ? new Set(
1846
+ opts.filter.split(",").map((s) => s.trim()).filter(Boolean)
1847
+ ) : null;
1848
+ const runFilter = opts.run;
1849
+ const matchesRun = (id) => {
1850
+ if (!runFilter) return true;
1851
+ return id === runFilter || id.startsWith(runFilter);
1852
+ };
1853
+ const live = new LiveInspector(store, {
1854
+ watchTools: watchToolsEffective,
1855
+ stallSeconds: stallSecondsEffective
1856
+ });
1857
+ if (!opts.json) {
1858
+ console.log(
1859
+ pc15.dim(
1860
+ `watching ~/.claude/projects \xB7 tools=${watchToolsEffective.join(",") || "(none)"} \xB7 stall=${stallSecondsEffective}s` + (runFilter ? ` \xB7 run=${runFilter}` : "") + (allowedKinds ? ` \xB7 filter=${[...allowedKinds].join(",")}` : "") + "\npress ctrl-c to stop"
1861
+ )
1862
+ );
1863
+ }
1864
+ live.on("data", (e) => {
1865
+ if (allowedKinds && !allowedKinds.has(e.type)) return;
1866
+ if (e.type === "fleet:snapshot" && opts.snapshot === false) return;
1867
+ if (runFilter && (e.type === "run:created" || e.type === "run:updated" || e.type === "run:completed") && !matchesRun(e.run.run_id)) {
1868
+ return;
1869
+ }
1870
+ if (runFilter && e.type === "alert" && !matchesRun(e.run_id)) {
1871
+ return;
1872
+ }
1873
+ if (opts.json) {
1874
+ process.stdout.write(JSON.stringify(e) + "\n");
1875
+ return;
1876
+ }
1877
+ printPretty(e);
1878
+ });
1879
+ await live.start();
1880
+ const stop = () => {
1881
+ live.stop();
1882
+ store.close();
1883
+ process.exit(0);
1884
+ };
1885
+ process.on("SIGINT", stop);
1886
+ process.on("SIGTERM", stop);
1887
+ }
1888
+ );
1889
+ }
1890
+ function printPretty(e) {
1891
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
1892
+ const head = pc15.dim(ts) + " ";
1893
+ switch (e.type) {
1894
+ case "run:created":
1895
+ console.log(
1896
+ head + pc15.blue("run:created ") + pc15.cyan(e.run.run_id.slice(0, 12)) + (e.run.title ? " " + e.run.title : "")
1897
+ );
1898
+ return;
1899
+ case "run:updated":
1900
+ console.log(
1901
+ head + pc15.dim("run:updated ") + pc15.cyan(e.run.run_id.slice(0, 12)) + pc15.dim(` +${e.new_steps.length} step${e.new_steps.length === 1 ? "" : "s"}`)
1902
+ );
1903
+ return;
1904
+ case "run:completed":
1905
+ console.log(
1906
+ head + pc15.green("run:completed") + " " + pc15.cyan(e.run.run_id.slice(0, 12)) + pc15.dim(` status=${e.run.status}`)
1907
+ );
1908
+ return;
1909
+ case "alert":
1910
+ console.log(
1911
+ head + pc15.yellow(`alert[${e.kind}] `) + pc15.cyan(e.run_id.slice(0, 12)) + " " + e.message
1912
+ );
1913
+ return;
1914
+ case "fleet:snapshot": {
1915
+ const counts = e.entries.reduce((acc, x) => {
1916
+ acc[x.status] = (acc[x.status] ?? 0) + 1;
1917
+ return acc;
1918
+ }, {});
1919
+ const summary = Object.entries(counts).map(([k, v]) => `${k}=${v}`).join(" ");
1920
+ console.log(
1921
+ head + pc15.dim(`fleet:snapshot ${e.entries.length} runs \xB7 ${summary}`)
1922
+ );
1923
+ return;
1924
+ }
1925
+ }
1926
+ }
1927
+
1928
+ // src/commands/open.ts
1929
+ import { spawn } from "child_process";
1930
+ import http from "http";
1931
+ import pc16 from "picocolors";
1932
+ import { getRun as getRun6, getStep as getStep3, getStepBySequence as getStepBySequence2 } from "@meterbility/collector";
1933
+ function registerOpenCommand(program2) {
1934
+ program2.command("open <run-id>").description("Open a run in the local Meterbility web UI (auto-starts the server if needed)").option("--at <seq-or-step-id>", "Deep-link to a specific step").option(
1935
+ "--context",
1936
+ "When --at is set, link into the resolved-context page"
1937
+ ).option("-p, --port <n>", "Web UI port", (v) => parseInt(v, 10), 4317).option("-h, --host <addr>", "Web UI host", "127.0.0.1").option("--no-launch", "Print the URL but do not open a browser or start the server").option(
1938
+ "--print",
1939
+ "Print the URL and exit without opening (useful for piping to pbcopy)"
1940
+ ).action(
1941
+ async (runId, opts) => {
1942
+ const store = openStore();
1943
+ let url;
1944
+ try {
1945
+ const run = getRun6(store, runId);
1946
+ if (!run) throw new Error(`run not found: ${runId}`);
1947
+ const fullRunId = run.run_id;
1948
+ let path = `/runs/${fullRunId}`;
1949
+ if (opts.at !== void 0) {
1950
+ const seq = Number(opts.at);
1951
+ const step = Number.isFinite(seq) ? getStepBySequence2(store, fullRunId, seq) : getStep3(store, opts.at);
1952
+ if (!step) throw new Error(`step not found: ${opts.at}`);
1953
+ if (opts.context) {
1954
+ path = `/contexts/${step.context_snapshot_id}`;
1955
+ } else {
1956
+ path = `/runs/${fullRunId}#step-${step.step_id}`;
1957
+ }
1958
+ }
1959
+ url = `http://${opts.host}:${opts.port}${path}`;
1960
+ } finally {
1961
+ store.close();
1962
+ }
1963
+ if (opts.print) {
1964
+ process.stdout.write(url + "\n");
1965
+ return;
1966
+ }
1967
+ if (opts.launch === false) {
1968
+ console.log(pc16.cyan(url));
1969
+ return;
1970
+ }
1971
+ const alive = await isServerUp(opts.host, opts.port);
1972
+ if (!alive) {
1973
+ console.log(
1974
+ pc16.dim(
1975
+ `no server on ${opts.host}:${opts.port} \u2014 starting \`meter web\` in the background\u2026`
1976
+ )
1977
+ );
1978
+ await launchDetachedWeb(opts.host, opts.port);
1979
+ const ready = await waitForServer(opts.host, opts.port, 3e3);
1980
+ if (!ready) {
1981
+ console.error(
1982
+ pc16.red("server didn't become reachable in time. ") + pc16.dim(
1983
+ `try \`meter web --port ${opts.port}\` in another terminal, then re-run.`
1984
+ )
1985
+ );
1986
+ process.exit(1);
1987
+ }
1988
+ }
1989
+ console.log(pc16.green("opening ") + pc16.cyan(url));
1990
+ await openBrowser2(url);
1991
+ }
1992
+ );
1993
+ }
1994
+ function isServerUp(host, port) {
1995
+ return new Promise((resolve2) => {
1996
+ const req = http.request(
1997
+ { host, port, path: "/api/runs", method: "GET", timeout: 500 },
1998
+ (res) => {
1999
+ res.resume();
2000
+ resolve2((res.statusCode ?? 0) < 500);
2001
+ }
2002
+ );
2003
+ req.on("error", () => resolve2(false));
2004
+ req.on("timeout", () => {
2005
+ req.destroy();
2006
+ resolve2(false);
2007
+ });
2008
+ req.end();
2009
+ });
2010
+ }
2011
+ async function waitForServer(host, port, timeoutMs) {
2012
+ const deadline = Date.now() + timeoutMs;
2013
+ while (Date.now() < deadline) {
2014
+ if (await isServerUp(host, port)) return true;
2015
+ await new Promise((r) => setTimeout(r, 150));
2016
+ }
2017
+ return false;
2018
+ }
2019
+ async function launchDetachedWeb(host, port) {
2020
+ const proc = spawn(
2021
+ process.execPath,
2022
+ [process.argv[1], "web", "--no-open", "--host", host, "--port", String(port)],
2023
+ {
2024
+ detached: true,
2025
+ stdio: "ignore",
2026
+ env: process.env
2027
+ }
2028
+ );
2029
+ proc.unref();
2030
+ }
2031
+ async function openBrowser2(url) {
2032
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2033
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
2034
+ spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
2035
+ }
2036
+
2037
+ // src/commands/proxy.ts
2038
+ import pc17 from "picocolors";
2039
+ import { startProxy } from "@meterbility/proxy";
2040
+ function registerProxyCommand(program2) {
2041
+ program2.command("proxy").description("Run a local LLM-API forward proxy that captures every call as a Meterbility Step").option("-p, --port <n>", "Port to listen on", (v) => parseInt(v, 10), 8765).option("-h, --host <addr>", "Host to bind", "127.0.0.1").option(
2042
+ "--project <name>",
2043
+ "Project label written to captured runs (defaults to cwd)"
2044
+ ).option(
2045
+ "--agent <name>",
2046
+ "Agent label written to captured runs (defaults to 'proxy')"
2047
+ ).option(
2048
+ "--anthropic-target <url>",
2049
+ "Override the Anthropic upstream (default: https://api.anthropic.com)"
2050
+ ).option(
2051
+ "--openai-target <url>",
2052
+ "Override the OpenAI upstream (default: https://api.openai.com)"
2053
+ ).option("--quiet", "Suppress per-request log lines").action(async (opts) => {
2054
+ const proxyOpts = {
2055
+ port: opts.port,
2056
+ host: opts.host,
2057
+ spec: {
2058
+ project: opts.project ?? process.cwd(),
2059
+ agent: opts.agent ?? "proxy"
2060
+ },
2061
+ upstreams: {
2062
+ ...opts.anthropicTarget ? { anthropic: opts.anthropicTarget } : {},
2063
+ ...opts.openaiTarget ? { openai: opts.openaiTarget } : {}
2064
+ },
2065
+ logger: opts.quiet ? () => {
2066
+ } : (line) => console.log(pc17.dim(line))
2067
+ };
2068
+ const handle = await startProxy(proxyOpts);
2069
+ console.log(pc17.green("meter proxy ready ") + pc17.cyan(handle.url));
2070
+ console.log(
2071
+ pc17.dim(
2072
+ ` set in your shell:
2073
+ export ANTHROPIC_BASE_URL=${handle.url}
2074
+ export OPENAI_BASE_URL=${handle.url}/v1
2075
+ or wrap a command with:
2076
+ meter run -- python myagent.py`
2077
+ )
2078
+ );
2079
+ console.log(pc17.dim("press ctrl-c to stop"));
2080
+ const stop = async () => {
2081
+ await handle.close();
2082
+ process.exit(0);
2083
+ };
2084
+ process.on("SIGINT", stop);
2085
+ process.on("SIGTERM", stop);
2086
+ });
2087
+ }
2088
+
2089
+ // src/commands/run.ts
2090
+ import { spawn as spawn2 } from "child_process";
2091
+ import net from "net";
2092
+ import pc18 from "picocolors";
2093
+ import { startProxy as startProxy2 } from "@meterbility/proxy";
2094
+ function registerRunCommand(program2) {
2095
+ program2.command("run").description("Run a command with the Meterbility proxy auto-wired (zero code change capture)").allowUnknownOption(true).option("--port <n>", "Pin the proxy to a port (default: random free port)", (v) => parseInt(v, 10)).option(
2096
+ "--project <name>",
2097
+ "Project label written to captured runs (defaults to cwd)"
2098
+ ).option(
2099
+ "--agent <name>",
2100
+ "Agent label written to captured runs (defaults to 'proxy')"
2101
+ ).option("--quiet", "Suppress per-request capture log lines").option(
2102
+ "--no-openai",
2103
+ "Don't set OPENAI_BASE_URL (only proxy Anthropic calls)"
2104
+ ).option(
2105
+ "--no-anthropic",
2106
+ "Don't set ANTHROPIC_BASE_URL (only proxy OpenAI calls)"
2107
+ ).option(
2108
+ "--anthropic-target <url>",
2109
+ "Override the Anthropic upstream (default: https://api.anthropic.com)"
2110
+ ).option(
2111
+ "--openai-target <url>",
2112
+ "Override the OpenAI upstream (default: https://api.openai.com)"
2113
+ ).action(async (opts, cmd) => {
2114
+ const userArgs = collectUserArgs(cmd);
2115
+ if (userArgs.length === 0) {
2116
+ console.error(
2117
+ pc18.red("meter run: missing command. Usage: meter run -- <command> [args]")
2118
+ );
2119
+ process.exit(2);
2120
+ }
2121
+ const port = opts.port ?? await pickFreePort();
2122
+ const handle = await startProxy2({
2123
+ port,
2124
+ spec: {
2125
+ project: opts.project ?? process.cwd(),
2126
+ agent: opts.agent ?? "proxy"
2127
+ },
2128
+ upstreams: {
2129
+ ...opts.anthropicTarget ? { anthropic: opts.anthropicTarget } : {},
2130
+ ...opts.openaiTarget ? { openai: opts.openaiTarget } : {}
2131
+ },
2132
+ logger: opts.quiet ? () => {
2133
+ } : (line) => process.stderr.write(pc18.dim(`[meter] ${line}
2134
+ `))
2135
+ });
2136
+ const childEnv = { ...process.env };
2137
+ if (opts.anthropic !== false) {
2138
+ childEnv.ANTHROPIC_BASE_URL = handle.url;
2139
+ }
2140
+ if (opts.openai !== false) {
2141
+ childEnv.OPENAI_BASE_URL = handle.url + "/v1";
2142
+ }
2143
+ if (!opts.quiet) {
2144
+ process.stderr.write(
2145
+ pc18.dim(
2146
+ `[meter] proxy ${handle.url} \u2192 spawning: ${userArgs.join(" ")}
2147
+ `
2148
+ )
2149
+ );
2150
+ }
2151
+ const [bin, ...rest] = userArgs;
2152
+ const child = spawn2(bin, rest, {
2153
+ env: childEnv,
2154
+ stdio: "inherit"
2155
+ });
2156
+ const shutdown = async (code) => {
2157
+ await handle.close();
2158
+ process.exit(code);
2159
+ };
2160
+ let signaled = false;
2161
+ const onSig = (sig) => {
2162
+ if (signaled) return;
2163
+ signaled = true;
2164
+ try {
2165
+ child.kill(sig);
2166
+ } catch {
2167
+ }
2168
+ };
2169
+ process.on("SIGINT", () => onSig("SIGINT"));
2170
+ process.on("SIGTERM", () => onSig("SIGTERM"));
2171
+ child.on("exit", (code, signal) => {
2172
+ setTimeout(() => {
2173
+ if (!opts.quiet) {
2174
+ process.stderr.write(
2175
+ pc18.dim(
2176
+ `[meter] child exited ${code ?? `(signal ${signal})`} \u2014 proxy stopping
2177
+ `
2178
+ )
2179
+ );
2180
+ }
2181
+ void shutdown(code ?? (signal ? 130 : 0));
2182
+ }, 250);
2183
+ });
2184
+ child.on("error", (err) => {
2185
+ process.stderr.write(pc18.red(`meter run: failed to spawn child: ${err.message}
2186
+ `));
2187
+ void shutdown(127);
2188
+ });
2189
+ });
2190
+ }
2191
+ function collectUserArgs(cmd) {
2192
+ if (cmd.args && cmd.args.length > 0) return cmd.args.slice();
2193
+ const dashIdx = process.argv.indexOf("--");
2194
+ if (dashIdx >= 0) return process.argv.slice(dashIdx + 1);
2195
+ return [];
2196
+ }
2197
+ function pickFreePort() {
2198
+ return new Promise((resolve2, reject) => {
2199
+ const srv = net.createServer();
2200
+ srv.listen(0, "127.0.0.1", () => {
2201
+ const addr = srv.address();
2202
+ if (typeof addr === "object" && addr) {
2203
+ const port = addr.port;
2204
+ srv.close(() => resolve2(port));
2205
+ } else {
2206
+ srv.close(() => reject(new Error("could not allocate free port")));
2207
+ }
2208
+ });
2209
+ srv.on("error", reject);
2210
+ });
2211
+ }
2212
+
2213
+ // src/commands/runs.ts
2214
+ import pc19 from "picocolors";
2215
+ import {
2216
+ getRun as getRun7,
2217
+ listRuns as listRuns3,
2218
+ setRunStatus,
2219
+ updateRunTotals
2220
+ } from "@meterbility/collector";
2221
+ function registerRunsCommand(program2) {
2222
+ const runs = program2.command("runs").description("Run-management operations");
2223
+ runs.command("close [id]").description("Mark an in-progress run as completed (seals status + ended_at)").option(
2224
+ "--status <kind>",
2225
+ "Final status to set (ok|error|abandoned)",
2226
+ "ok"
2227
+ ).option(
2228
+ "--all",
2229
+ "Close every in_progress run (use --older-than to scope by age)"
2230
+ ).option(
2231
+ "--older-than <minutes>",
2232
+ "Only close runs whose last activity is older than N minutes",
2233
+ (v) => parseInt(v, 10)
2234
+ ).option("--source <runtime>", "Restrict --all to one source_runtime (e.g. proxy)").option("--dry-run", "Print what would close without writing").action(
2235
+ (id, opts) => {
2236
+ if (!id && !opts.all) {
2237
+ console.error(
2238
+ pc19.red(
2239
+ "meter runs close: provide a run id, or pass --all to close every in_progress run."
2240
+ )
2241
+ );
2242
+ process.exit(2);
2243
+ }
2244
+ if (!isValidStatus(opts.status)) {
2245
+ console.error(
2246
+ pc19.red(`invalid --status: ${opts.status}. allowed: ok | error | abandoned`)
2247
+ );
2248
+ process.exit(2);
2249
+ }
2250
+ const finalStatus = opts.status;
2251
+ const store = openStore();
2252
+ try {
2253
+ const targets = id ? singleTarget(store, id) : bulkTargets(store, opts);
2254
+ if (targets.length === 0) {
2255
+ console.log(pc19.dim("no runs to close"));
2256
+ return;
2257
+ }
2258
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2259
+ for (const run of targets) {
2260
+ if (opts.dryRun) {
2261
+ console.log(
2262
+ `${pc19.yellow("would close")} ${pc19.cyan(run.run_id.slice(0, 12))} ${pc19.dim(`(${run.status} \u2192 ${finalStatus})`)} ${pc19.dim(run.title ?? "")}`
2263
+ );
2264
+ continue;
2265
+ }
2266
+ setRunStatus(store, run.run_id, finalStatus, now);
2267
+ updateRunTotals(store, run.run_id);
2268
+ console.log(
2269
+ `${pc19.green("closed")} ${pc19.cyan(run.run_id.slice(0, 12))} ${pc19.dim(`${run.status} \u2192 ${finalStatus}`)} ${pc19.dim(run.title ?? "")}`
2270
+ );
2271
+ }
2272
+ if (!opts.dryRun && targets.length > 1) {
2273
+ console.log(pc19.bold(`
2274
+ ${targets.length} run(s) closed`));
2275
+ }
2276
+ } finally {
2277
+ store.close();
2278
+ }
2279
+ }
2280
+ );
2281
+ }
2282
+ function singleTarget(store, id) {
2283
+ const run = getRun7(store, id);
2284
+ if (!run) {
2285
+ console.error(pc19.red(`run not found: ${id}`));
2286
+ process.exit(1);
2287
+ }
2288
+ if (run.status !== "in_progress") {
2289
+ console.log(
2290
+ pc19.dim(
2291
+ `${run.run_id.slice(0, 12)} already ${run.status} \u2014 nothing to close. (Pass any explicit --status to override.)`
2292
+ )
2293
+ );
2294
+ process.exit(0);
2295
+ }
2296
+ return [run];
2297
+ }
2298
+ function bulkTargets(store, opts) {
2299
+ const all = listRuns3(store, { limit: 1e3 });
2300
+ const cutoffMs = opts.olderThan ? Date.now() - opts.olderThan * 6e4 : void 0;
2301
+ return all.filter((run) => {
2302
+ if (run.status !== "in_progress") return false;
2303
+ if (opts.source && run.source_runtime !== opts.source) return false;
2304
+ if (cutoffMs !== void 0) {
2305
+ const startedMs = Date.parse(run.started_at);
2306
+ if (Number.isFinite(startedMs) && startedMs > cutoffMs) return false;
2307
+ }
2308
+ return true;
2309
+ });
2310
+ }
2311
+ function isValidStatus(s) {
2312
+ return s === "ok" || s === "error" || s === "abandoned";
2313
+ }
2314
+
2315
+ // src/commands/init.ts
2316
+ import { existsSync as existsSync3, mkdirSync } from "fs";
2317
+ import { writeFile as writeFile3 } from "fs/promises";
2318
+ import { join, resolve } from "path";
2319
+ import pc20 from "picocolors";
2320
+ import { DEFAULT_METERBILITYIGNORE } from "@meterbility/shared";
2321
+ function registerInitCommand(program2) {
2322
+ program2.command("init [path]").description(
2323
+ "Scaffold .meterbility/config.toml and .meterbilityignore for v0.3 file capture"
2324
+ ).option(
2325
+ "--force",
2326
+ "Overwrite existing files (default: leave existing files untouched)"
2327
+ ).option(
2328
+ "--quiet",
2329
+ "Suppress per-file output (still prints the final summary)"
2330
+ ).action(async (pathArg, opts) => {
2331
+ const root = resolve(pathArg ?? process.cwd());
2332
+ if (!existsSync3(root)) {
2333
+ console.error(pc20.red(`meter init: target path does not exist: ${root}`));
2334
+ process.exit(2);
2335
+ }
2336
+ const ignorePath = join(root, ".meterbilityignore");
2337
+ const configDir = join(root, ".meter");
2338
+ const configPath = join(configDir, "config.toml");
2339
+ mkdirSync(configDir, { recursive: true });
2340
+ const ignoreResult = await writeIfNeeded(
2341
+ ignorePath,
2342
+ renderMeterbilityignore(),
2343
+ opts.force === true
2344
+ );
2345
+ const configResult = await writeIfNeeded(
2346
+ configPath,
2347
+ renderConfigToml(),
2348
+ opts.force === true
2349
+ );
2350
+ if (!opts.quiet) {
2351
+ printFileResult(".meterbilityignore", ignorePath, ignoreResult);
2352
+ printFileResult(".meterbility/config.toml", configPath, configResult);
2353
+ }
2354
+ const allCreated = ignoreResult === "created" && configResult === "created";
2355
+ if (allCreated) {
2356
+ console.log(
2357
+ pc20.bold(pc20.green("\nmeter init: scaffolded")) + pc20.dim(" \u2014 v0.3 file capture is now on for this project.")
2358
+ );
2359
+ } else {
2360
+ console.log(
2361
+ pc20.dim(
2362
+ "\nmeter init: complete. Re-run with --force to regenerate existing files."
2363
+ )
2364
+ );
2365
+ }
2366
+ console.log(
2367
+ pc20.dim(
2368
+ " edit .meterbilityignore to tune what gets captured\n see `meter files <run-id>` after your next Claude Code session"
2369
+ )
2370
+ );
2371
+ });
2372
+ }
2373
+ async function writeIfNeeded(path, contents, force) {
2374
+ const present = existsSync3(path);
2375
+ if (present && !force) return "skipped";
2376
+ await writeFile3(path, contents, "utf-8");
2377
+ return present ? "overwritten" : "created";
2378
+ }
2379
+ function printFileResult(label, path, result) {
2380
+ const tag = result === "created" ? pc20.green("created") : result === "overwritten" ? pc20.yellow("overwrote") : pc20.dim("kept existing");
2381
+ console.log(` ${tag} ${pc20.cyan(label)} ${pc20.dim(`(${path})`)}`);
2382
+ }
2383
+ function renderMeterbilityignore() {
2384
+ return `# .meterbilityignore \u2014 paths Meterbility excludes from file capture (v0.3+).
2385
+ # Same syntax subset as .gitignore. First match wins; no negation in v0.3.
2386
+ # Generated by \`meter init\`. Edit freely.
2387
+
2388
+ # Build artifacts
2389
+ node_modules/
2390
+ dist/
2391
+ build/
2392
+ .next/
2393
+ target/
2394
+ .cache/
2395
+
2396
+ # Language-specific
2397
+ .venv/
2398
+ __pycache__/
2399
+ *.pyc
2400
+
2401
+ # Version control internals
2402
+ .git/objects/
2403
+ .git/logs/
2404
+
2405
+ # Editor / OS
2406
+ .DS_Store
2407
+ .idea/
2408
+ .vscode/
2409
+
2410
+ # Coverage / tooling
2411
+ coverage/
2412
+ .nyc_output/
2413
+
2414
+ # Sensitive by default \u2014 paired with redaction rules in the collector
2415
+ .env
2416
+ .env.*
2417
+ *.pem
2418
+ *.key
2419
+ id_rsa*
2420
+ id_ed25519*
2421
+ credentials.json
2422
+ .aws/
2423
+ .kube/config
2424
+ `;
2425
+ }
2426
+ function renderConfigToml() {
2427
+ return `# .meterbility/config.toml \u2014 per-project Meterbility configuration.
2428
+ # v0.3 reads [capture.files] only; later milestones add more sections.
2429
+
2430
+ [capture.files]
2431
+ # When false, FileChange rows still record the *fact* of an edit but
2432
+ # leave before/after blob refs unset (partial_diff = true). Set true to
2433
+ # capture the bytes Claude Code / Codex / the SDK / the proxy actually
2434
+ # wrote. Off-by-default because capturing code from a random \`cd\`
2435
+ # deserves deliberation.
2436
+ enabled = true
2437
+
2438
+ # Glob patterns. Empty include = "everything not ignored."
2439
+ include = []
2440
+ exclude = []
2441
+
2442
+ # Files larger than this are excluded from the baseline manifest and
2443
+ # from full-snapshot FileChange capture. Defaults to 5 MB; the
2444
+ # patch_text on the row still records the edit when applicable. Raise
2445
+ # this for repos with large generated assets you actually want
2446
+ # captured.
2447
+ max_file_size_bytes = 5_242_880
2448
+
2449
+ # Binary detection strategy. "null-byte-heuristic" matches PR 1's
2450
+ # isProbablyText (NUL in first 8 KB \u21D2 binary). "magic-bytes" is
2451
+ # scheduled for v0.4 \u2014 it parses file magic for higher precision.
2452
+ binary_detection = "null-byte-heuristic"
2453
+ `;
2454
+ }
2455
+
2456
+ // src/commands/files.ts
2457
+ import pc21 from "picocolors";
2458
+ import {
2459
+ getBaselineTree as getBaselineTree2,
2460
+ getRun as getRun8,
2461
+ getStep as getStep4,
2462
+ getStepBySequence as getStepBySequence3,
2463
+ listFileChanges as listFileChanges3,
2464
+ listSteps as listSteps4
2465
+ } from "@meterbility/collector";
2466
+ function registerFilesCommand(program2) {
2467
+ program2.command("files <run-id>").description("Show file changes captured for a run (v0.3+)").option(
2468
+ "--at <seq-or-step-id>",
2469
+ "Restrict to changes from a specific step (sequence number or step id)"
2470
+ ).option(
2471
+ "--diff <path>",
2472
+ "Print the unified diff for one path. Combine with --from/--to to scope."
2473
+ ).option(
2474
+ "--from <seq-or-step-id>",
2475
+ "Lower step bound for --diff (inclusive). Defaults to start of run."
2476
+ ).option(
2477
+ "--to <seq-or-step-id>",
2478
+ "Upper step bound for --diff (inclusive). Defaults to end of run."
2479
+ ).option("--json", "Emit JSON instead of human-readable output").action(
2480
+ async (runId, opts) => {
2481
+ const store = openStore();
2482
+ try {
2483
+ const run = getRun8(store, runId);
2484
+ if (!run) {
2485
+ console.error(pc21.red(`run not found: ${runId}`));
2486
+ process.exit(1);
2487
+ }
2488
+ if (opts.diff) {
2489
+ await runDiffMode(store, run, opts);
2490
+ return;
2491
+ }
2492
+ if (opts.at !== void 0) {
2493
+ await runAtMode(store, run, opts);
2494
+ return;
2495
+ }
2496
+ await runSummaryMode(store, run, opts);
2497
+ } finally {
2498
+ store.close();
2499
+ }
2500
+ }
2501
+ );
2502
+ }
2503
+ async function runSummaryMode(store, run, opts) {
2504
+ const steps = listSteps4(store, run.run_id);
2505
+ const fcs = listFileChanges3(store, { runId: run.run_id });
2506
+ const collapsed = collapseByPath(fcs);
2507
+ const stats = totalStats(collapsed);
2508
+ const baseline = run.baseline_tree_id ? getBaselineTree2(store, run.baseline_tree_id) : void 0;
2509
+ if (opts.json) {
2510
+ process.stdout.write(
2511
+ JSON.stringify(
2512
+ {
2513
+ run_id: run.run_id,
2514
+ source_runtime: run.source_runtime,
2515
+ step_count: steps.length,
2516
+ files_touched: collapsed.length,
2517
+ lines_added_total: stats.added,
2518
+ lines_removed_total: stats.removed,
2519
+ baseline: baseline ? {
2520
+ baseline_tree_id: baseline.baseline_tree_id,
2521
+ git_head: baseline.git_head,
2522
+ git_dirty: baseline.git_dirty
2523
+ } : void 0,
2524
+ files: collapsed.map(rowToJson)
2525
+ },
2526
+ null,
2527
+ 2
2528
+ ) + "\n"
2529
+ );
2530
+ return;
2531
+ }
2532
+ const sourceLabel = run.source_runtime === "fork" ? "Fork" : run.source_runtime;
2533
+ console.log(
2534
+ pc21.bold(`RUN ${run.run_id.slice(0, 12)}`) + pc21.dim(
2535
+ ` (${sourceLabel} \xB7 ${steps.length} step${steps.length === 1 ? "" : "s"} \xB7 ${collapsed.length} file${collapsed.length === 1 ? "" : "s"} touched)`
2536
+ )
2537
+ );
2538
+ if (collapsed.length === 0) {
2539
+ console.log(pc21.dim("\n no file changes captured for this run"));
2540
+ if (!run.baseline_tree_id) {
2541
+ console.log(
2542
+ pc21.dim(
2543
+ " v0.3 file capture is opt-in per project \u2014 run `meter init` in the project's cwd to enable"
2544
+ )
2545
+ );
2546
+ }
2547
+ return;
2548
+ }
2549
+ console.log("");
2550
+ for (const row of collapsed) {
2551
+ printRow(row);
2552
+ }
2553
+ console.log(
2554
+ pc21.bold(
2555
+ `
2556
+ Final ${signedAdd(stats.added)} ${signedRemove(stats.removed)}`
2557
+ ) + pc21.dim(` across ${collapsed.length} file${collapsed.length === 1 ? "" : "s"}`)
2558
+ );
2559
+ if (baseline) {
2560
+ const head = baseline.git_head ? baseline.git_head.slice(0, 7) : "(no git)";
2561
+ const dirty = baseline.git_dirty ? pc21.yellow("dirty") : pc21.dim("clean");
2562
+ console.log(
2563
+ pc21.dim(`Baseline: git HEAD ${head} (${dirty})`)
2564
+ );
2565
+ } else {
2566
+ console.log(
2567
+ pc21.dim(
2568
+ "Baseline: (none captured \u2014 proxy / non-coding runs or pre-v0.3 ingest)"
2569
+ )
2570
+ );
2571
+ }
2572
+ }
2573
+ async function runAtMode(store, run, opts) {
2574
+ const step = resolveStep(store, run, opts.at);
2575
+ const fcs = listFileChanges3(store, { stepId: step.step_id });
2576
+ if (opts.json) {
2577
+ process.stdout.write(
2578
+ JSON.stringify(
2579
+ {
2580
+ run_id: run.run_id,
2581
+ step_id: step.step_id,
2582
+ sequence: step.sequence,
2583
+ files: fcs.map(rowToJson)
2584
+ },
2585
+ null,
2586
+ 2
2587
+ ) + "\n"
2588
+ );
2589
+ return;
2590
+ }
2591
+ console.log(
2592
+ pc21.bold(`STEP #${step.sequence}`) + pc21.dim(` ${step.step_id.slice(0, 12)} \xB7 ${fcs.length} file change${fcs.length === 1 ? "" : "s"}`)
2593
+ );
2594
+ if (fcs.length === 0) {
2595
+ console.log(pc21.dim("\n step did not modify any files"));
2596
+ return;
2597
+ }
2598
+ console.log("");
2599
+ for (const fc of fcs) {
2600
+ printRow(toCollapsedRow(fc));
2601
+ }
2602
+ }
2603
+ async function runDiffMode(store, run, opts) {
2604
+ const path = opts.diff;
2605
+ const fromSeq = opts.from ? resolveStepSeqLoose(store, run, opts.from) : 0;
2606
+ const toSeq = opts.to ? resolveStepSeqLoose(store, run, opts.to) : Number.MAX_SAFE_INTEGER;
2607
+ if (fromSeq > toSeq) {
2608
+ console.error(pc21.red(`--from (${fromSeq}) must be <= --to (${toSeq})`));
2609
+ process.exit(2);
2610
+ }
2611
+ const all = listFileChanges3(store, { runId: run.run_id, path });
2612
+ const inWindow = filterByStepSeq(store, all, fromSeq, toSeq);
2613
+ if (opts.json) {
2614
+ const blocks = await Promise.all(
2615
+ inWindow.map(async (fc) => ({
2616
+ step_id: fc.step_id,
2617
+ op: fc.op,
2618
+ partial_diff: fc.partial_diff,
2619
+ patch_text: fc.patch_text,
2620
+ lines_added: fc.lines_added,
2621
+ lines_removed: fc.lines_removed,
2622
+ before_blob_ref: fc.before_blob_ref,
2623
+ after_blob_ref: fc.after_blob_ref
2624
+ }))
2625
+ );
2626
+ process.stdout.write(
2627
+ JSON.stringify(
2628
+ { run_id: run.run_id, path, from: fromSeq, to: toSeq, blocks },
2629
+ null,
2630
+ 2
2631
+ ) + "\n"
2632
+ );
2633
+ return;
2634
+ }
2635
+ console.log(
2636
+ pc21.bold(`DIFF ${path}`) + pc21.dim(` (run ${run.run_id.slice(0, 12)} \xB7 ${inWindow.length} change${inWindow.length === 1 ? "" : "s"} in window)`)
2637
+ );
2638
+ if (inWindow.length === 0) {
2639
+ console.log(pc21.dim("\n no captured changes for this path in the chosen step window"));
2640
+ return;
2641
+ }
2642
+ const steps = listSteps4(store, run.run_id);
2643
+ const seqByStepId = /* @__PURE__ */ new Map();
2644
+ for (const s of steps) seqByStepId.set(s.step_id, s.sequence);
2645
+ for (const fc of inWindow) {
2646
+ const parentSeq = seqByStepId.get(fc.step_id);
2647
+ const seqLabel = parentSeq !== void 0 ? `step #${parentSeq}` : "step ?";
2648
+ console.log(
2649
+ "\n" + pc21.bold(`@@ ${seqLabel}`) + pc21.dim(` ${fc.step_id.slice(0, 12)} \xB7 ${fc.op}`)
2650
+ );
2651
+ if (fc.partial_diff) {
2652
+ console.log(
2653
+ pc21.yellow(
2654
+ " partial: this change ran outside captured tools (e.g. Bash). Enable `meter watch --files` in v0.4 for full fidelity."
2655
+ )
2656
+ );
2657
+ continue;
2658
+ }
2659
+ if (!fc.patch_text) {
2660
+ console.log(pc21.dim(" (binary or no-op \u2014 no patch text)"));
2661
+ continue;
2662
+ }
2663
+ printColorizedPatch(fc.patch_text);
2664
+ }
2665
+ }
2666
+ function collapseByPath(fcs) {
2667
+ const byPath = /* @__PURE__ */ new Map();
2668
+ for (const fc of fcs) {
2669
+ const key = fc.path;
2670
+ const existing = byPath.get(key);
2671
+ if (existing) {
2672
+ existing.lines_added += fc.lines_added;
2673
+ existing.lines_removed += fc.lines_removed;
2674
+ existing.terminalOp = fc.op;
2675
+ existing.any_partial = existing.any_partial || fc.partial_diff;
2676
+ existing.any_binary = existing.any_binary || fc.patch_format === "binary";
2677
+ existing.change_count += 1;
2678
+ if (fc.op === "rename" && fc.old_path) {
2679
+ existing.rename_from = fc.old_path;
2680
+ }
2681
+ } else {
2682
+ byPath.set(key, toCollapsedRow(fc));
2683
+ }
2684
+ }
2685
+ return [...byPath.values()].sort(
2686
+ (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
2687
+ );
2688
+ }
2689
+ function toCollapsedRow(fc) {
2690
+ return {
2691
+ path: fc.path,
2692
+ terminalOp: fc.op,
2693
+ lines_added: fc.lines_added,
2694
+ lines_removed: fc.lines_removed,
2695
+ rename_from: fc.op === "rename" ? fc.old_path : void 0,
2696
+ any_partial: fc.partial_diff,
2697
+ any_binary: fc.patch_format === "binary",
2698
+ change_count: 1
2699
+ };
2700
+ }
2701
+ function totalStats(rows) {
2702
+ let added = 0;
2703
+ let removed = 0;
2704
+ for (const r of rows) {
2705
+ added += r.lines_added;
2706
+ removed += r.lines_removed;
2707
+ }
2708
+ return { added, removed };
2709
+ }
2710
+ function printRow(row) {
2711
+ const tag = opTag(row.terminalOp);
2712
+ const renderedPath = row.terminalOp === "rename" && row.rename_from ? `${row.rename_from} \u2192 ${row.path}` : row.path;
2713
+ const pathCell = renderedPath.padEnd(32);
2714
+ const stats = `${signedAdd(row.lines_added).padStart(6)} ${signedRemove(row.lines_removed).padStart(6)}`;
2715
+ const flags = [];
2716
+ if (row.any_partial) flags.push(pc21.yellow("partial"));
2717
+ if (row.any_binary) flags.push(pc21.dim("binary"));
2718
+ if (row.change_count > 1) flags.push(pc21.dim(`${row.change_count} changes`));
2719
+ console.log(
2720
+ ` ${tag} ${pathCell} ${stats}${flags.length ? " " + flags.join(" ") : ""}`
2721
+ );
2722
+ }
2723
+ function opTag(op) {
2724
+ switch (op) {
2725
+ case "create":
2726
+ return pc21.green("A");
2727
+ case "modify":
2728
+ return pc21.yellow("M");
2729
+ case "delete":
2730
+ return pc21.red("D");
2731
+ case "rename":
2732
+ return pc21.magenta("R");
2733
+ case "chmod":
2734
+ return pc21.dim("X");
2735
+ }
2736
+ }
2737
+ function signedAdd(n) {
2738
+ return n === 0 ? "+0" : pc21.green(`+${n}`);
2739
+ }
2740
+ function signedRemove(n) {
2741
+ return n === 0 ? "\u22120" : pc21.red(`\u2212${n}`);
2742
+ }
2743
+ function printColorizedPatch(patch) {
2744
+ for (const line of patch.split("\n")) {
2745
+ if (line.startsWith("@@")) {
2746
+ console.log(pc21.cyan(line));
2747
+ } else if (line.startsWith("+") && !line.startsWith("+++")) {
2748
+ console.log(pc21.green(line));
2749
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2750
+ console.log(pc21.red(line));
2751
+ } else {
2752
+ console.log(line);
2753
+ }
2754
+ }
2755
+ }
2756
+ function rowToJson(arg) {
2757
+ if ("change_count" in arg) {
2758
+ const r = arg;
2759
+ return {
2760
+ path: r.path,
2761
+ op: r.terminalOp,
2762
+ rename_from: r.rename_from,
2763
+ lines_added: r.lines_added,
2764
+ lines_removed: r.lines_removed,
2765
+ change_count: r.change_count,
2766
+ partial: r.any_partial,
2767
+ binary: r.any_binary
2768
+ };
2769
+ }
2770
+ const fc = arg;
2771
+ return {
2772
+ file_change_id: fc.file_change_id,
2773
+ step_id: fc.step_id,
2774
+ sequence: fc.sequence,
2775
+ path: fc.path,
2776
+ old_path: fc.old_path,
2777
+ op: fc.op,
2778
+ before_blob_ref: fc.before_blob_ref,
2779
+ after_blob_ref: fc.after_blob_ref,
2780
+ partial_diff: fc.partial_diff,
2781
+ patch_format: fc.patch_format,
2782
+ lines_added: fc.lines_added,
2783
+ lines_removed: fc.lines_removed,
2784
+ source_tool_name: fc.source_tool_name
2785
+ };
2786
+ }
2787
+ function resolveStep(store, run, needle) {
2788
+ const seq = Number(needle);
2789
+ const step = Number.isFinite(seq) ? getStepBySequence3(store, run.run_id, seq) : getStep4(store, needle);
2790
+ if (!step) {
2791
+ console.error(pc21.red(`step not found: ${needle}`));
2792
+ process.exit(1);
2793
+ }
2794
+ return step;
2795
+ }
2796
+ function resolveStepSeqLoose(store, run, needle) {
2797
+ const seq = Number(needle);
2798
+ if (Number.isFinite(seq)) return seq;
2799
+ const step = getStep4(store, needle);
2800
+ if (!step) {
2801
+ console.error(pc21.red(`step not found: ${needle}`));
2802
+ process.exit(1);
2803
+ }
2804
+ return step.sequence;
2805
+ }
2806
+ function filterByStepSeq(store, fcs, fromSeq, toSeq) {
2807
+ if (fcs.length === 0) return fcs;
2808
+ const steps = listSteps4(store, fcs[0].run_id);
2809
+ const seqByStepId = /* @__PURE__ */ new Map();
2810
+ for (const s of steps) seqByStepId.set(s.step_id, s.sequence);
2811
+ return fcs.filter((fc) => {
2812
+ const seq = seqByStepId.get(fc.step_id);
2813
+ if (seq === void 0) return false;
2814
+ return seq >= fromSeq && seq <= toSeq;
2815
+ });
2816
+ }
2817
+
2818
+ // src/commands/probe.ts
2819
+ import { readFileSync } from "fs";
2820
+ import pc22 from "picocolors";
2821
+ import {
2822
+ clearProbe,
2823
+ readState,
2824
+ requestPause,
2825
+ requestResume,
2826
+ setInject
2827
+ } from "@meterbility/shared";
2828
+ import { getRun as getRun9 } from "@meterbility/collector";
2829
+ function registerProbeCommand(program2) {
2830
+ const probe = program2.command("probe").description(
2831
+ "Pause, inject a message, or resume a running agent (Live Probe)"
2832
+ );
2833
+ probe.command("status <run-id>").description("Show current probe state for a run").option("--json", "Emit the full ProbeRecord as one JSON line").action(async (runIdArg, opts) => {
2834
+ const runId = await resolveRunId(runIdArg);
2835
+ const state = readState(runId);
2836
+ if (opts.json) {
2837
+ process.stdout.write(JSON.stringify(state) + "\n");
2838
+ return;
2839
+ }
2840
+ printStatus(runId, state);
2841
+ });
2842
+ probe.command("pause <run-id>").description(
2843
+ "Request a graceful pause \u2014 SDK finishes current call before yielding"
2844
+ ).option("--json", "Emit the resulting ProbeRecord as JSON").action(async (runIdArg, opts) => {
2845
+ const runId = await resolveRunId(runIdArg);
2846
+ const prev = readState(runId);
2847
+ const next = requestPause(runId);
2848
+ if (opts.json) {
2849
+ process.stdout.write(JSON.stringify(next) + "\n");
2850
+ return;
2851
+ }
2852
+ if (prev.state === "pause_requested" || prev.state === "paused") {
2853
+ console.log(
2854
+ pc22.dim(
2855
+ `already ${prev.state} (since ${fmtAgoFromMs(prev.requested_at_ms)})`
2856
+ )
2857
+ );
2858
+ } else {
2859
+ console.log(
2860
+ pc22.yellow("pause requested ") + pc22.cyan(runId.slice(0, 12)) + pc22.dim(" (will take effect after the current model call)")
2861
+ );
2862
+ }
2863
+ });
2864
+ probe.command("inject <run-id>").description(
2865
+ "Queue a message to be appended to the next user turn (pause + inject + resume is the natural flow)"
2866
+ ).option(
2867
+ "-m, --message <text>",
2868
+ "Message text. Use --stdin for multi-line or to avoid shell-quoting"
2869
+ ).option("--stdin", "Read the message from stdin").option(
2870
+ "--force",
2871
+ "Overwrite a pending inject without warning"
2872
+ ).option("--json", "Emit the resulting ProbeRecord as JSON").action(
2873
+ async (runIdArg, opts) => {
2874
+ const runId = await resolveRunId(runIdArg);
2875
+ const message = readInjectMessage(opts);
2876
+ const prev = readState(runId);
2877
+ if (prev.inject !== null && prev.inject !== void 0 && !opts.force) {
2878
+ throw new Error(
2879
+ `a pending inject is already queued for this run (use --force to overwrite). current: ${JSON.stringify(prev.inject)}`
2880
+ );
2881
+ }
2882
+ const next = setInject(runId, message);
2883
+ if (opts.json) {
2884
+ process.stdout.write(JSON.stringify(next) + "\n");
2885
+ return;
2886
+ }
2887
+ console.log(
2888
+ pc22.green("inject queued ") + pc22.cyan(runId.slice(0, 12)) + pc22.dim(
2889
+ ` (${message.length} chars, will land on next model call)`
2890
+ )
2891
+ );
2892
+ }
2893
+ );
2894
+ probe.command("resume <run-id>").description(
2895
+ "Resume a paused run. Preserves any pending inject \u2014 operator can resume-with-message"
2896
+ ).option("--json", "Emit the resulting ProbeRecord as JSON").action(async (runIdArg, opts) => {
2897
+ const runId = await resolveRunId(runIdArg);
2898
+ const prev = readState(runId);
2899
+ const next = requestResume(runId);
2900
+ if (opts.json) {
2901
+ process.stdout.write(JSON.stringify(next) + "\n");
2902
+ return;
2903
+ }
2904
+ if (prev.state === "running") {
2905
+ console.log(pc22.dim("already running \u2014 nothing to do"));
2906
+ } else {
2907
+ const carried = next.inject !== null ? pc22.dim(` (carrying pending inject \u2014 will land on next call)`) : "";
2908
+ console.log(
2909
+ pc22.green("resumed ") + pc22.cyan(runId.slice(0, 12)) + carried
2910
+ );
2911
+ }
2912
+ });
2913
+ probe.command("clear <run-id>").description(
2914
+ "Remove the probe file. For stale recovery when the SDK crashed before tracer.end() ran"
2915
+ ).action(async (runIdArg) => {
2916
+ clearProbe(runIdArg);
2917
+ console.log(pc22.dim(`cleared probe file for ${runIdArg}`));
2918
+ });
2919
+ }
2920
+ async function resolveRunId(input) {
2921
+ const store = openStore();
2922
+ try {
2923
+ const run = getRun9(store, input);
2924
+ if (!run) {
2925
+ throw new Error(
2926
+ `run not found: ${input} (provide the full id or a unique 6+-char prefix)`
2927
+ );
2928
+ }
2929
+ return run.run_id;
2930
+ } finally {
2931
+ store.close();
2932
+ }
2933
+ }
2934
+ function readInjectMessage(opts) {
2935
+ if (opts.stdin) {
2936
+ const buf = readFileSync(0, "utf-8");
2937
+ if (buf.length === 0) {
2938
+ throw new Error("--stdin specified but no input received on stdin");
2939
+ }
2940
+ return buf.endsWith("\n") ? buf.slice(0, -1) : buf;
2941
+ }
2942
+ if (opts.message !== void 0) {
2943
+ if (opts.message === "") {
2944
+ throw new Error("--message cannot be empty");
2945
+ }
2946
+ return opts.message;
2947
+ }
2948
+ throw new Error("provide a message via -m <text> or --stdin");
2949
+ }
2950
+ function printStatus(runId, r) {
2951
+ const stateColor = r.state === "running" ? pc22.green : r.state === "pause_requested" ? pc22.yellow : pc22.red;
2952
+ console.log(pc22.bold(runId));
2953
+ console.log(` ${pc22.dim("state")} ${stateColor(r.state)}`);
2954
+ console.log(
2955
+ ` ${pc22.dim("requested at")} ${fmtIsoFromMs(r.requested_at_ms)}`
2956
+ );
2957
+ console.log(
2958
+ ` ${pc22.dim("paused at")} ${fmtIsoFromMs(r.paused_at_ms)}`
2959
+ );
2960
+ console.log(
2961
+ ` ${pc22.dim("resumed at")} ${fmtIsoFromMs(r.resumed_at_ms)}`
2962
+ );
2963
+ if (r.inject !== null) {
2964
+ console.log(
2965
+ ` ${pc22.dim("inject")} ${pc22.cyan("queued")} (${r.inject.length} chars)`
2966
+ );
2967
+ const firstLine = r.inject.split("\n")[0] ?? "";
2968
+ const preview = firstLine.length > 80 ? firstLine.slice(0, 77) + "..." : firstLine;
2969
+ console.log(` ${pc22.dim("\u203A")} ${preview}`);
2970
+ } else {
2971
+ console.log(` ${pc22.dim("inject")} ${pc22.dim("none")}`);
2972
+ }
2973
+ }
2974
+ function fmtIsoFromMs(ms) {
2975
+ if (ms === null) return pc22.dim("\u2014");
2976
+ return new Date(ms).toISOString();
2977
+ }
2978
+ function fmtAgoFromMs(ms) {
2979
+ if (ms === null) return "earlier";
2980
+ const delta = Date.now() - ms;
2981
+ if (delta < 1e3) return "just now";
2982
+ const s = Math.floor(delta / 1e3);
2983
+ if (s < 60) return `${s}s ago`;
2984
+ const m = Math.floor(s / 60);
2985
+ if (m < 60) return `${m}m ago`;
2986
+ const h = Math.floor(m / 60);
2987
+ return `${h}h ago`;
2988
+ }
2989
+
2990
+ // src/index.ts
2991
+ var program = new Command();
2992
+ program.name("meter").description(pc23.bold("Meterbility ") + pc23.dim("\u2014 the debugger for AI agents")).version("0.3.1");
2993
+ registerDoctorCommand(program);
2994
+ registerIngestCommand(program);
2995
+ registerListCommand(program);
2996
+ registerInspectCommand(program);
2997
+ registerForkCommand(program);
2998
+ registerDiffCommand(program);
2999
+ registerAnnotateCommand(program);
3000
+ registerWebCommand(program);
3001
+ registerExportCommand(program);
3002
+ registerTestCommand(program);
3003
+ registerDbCommand(program);
3004
+ registerSlackCommand(program);
3005
+ registerConfigCommand(program);
3006
+ registerWatchCommand(program);
3007
+ registerOpenCommand(program);
3008
+ registerProxyCommand(program);
3009
+ registerRunCommand(program);
3010
+ registerRunsCommand(program);
3011
+ registerInitCommand(program);
3012
+ registerFilesCommand(program);
3013
+ registerProbeCommand(program);
3014
+ program.parseAsync(process.argv).catch((err) => {
3015
+ console.error(pc23.red("error: ") + err.message);
3016
+ if (process.env.METERBILITY_DEBUG) console.error(err);
3017
+ process.exit(1);
3018
+ });