@os-eco/overstory-cli 0.6.8 → 0.6.9

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 (50) hide show
  1. package/README.md +4 -2
  2. package/package.json +1 -1
  3. package/src/agents/hooks-deployer.test.ts +180 -0
  4. package/src/agents/hooks-deployer.ts +32 -1
  5. package/src/commands/agents.ts +9 -6
  6. package/src/commands/clean.ts +2 -1
  7. package/src/commands/completions.ts +3 -4
  8. package/src/commands/coordinator.test.ts +8 -0
  9. package/src/commands/coordinator.ts +11 -8
  10. package/src/commands/costs.test.ts +48 -38
  11. package/src/commands/costs.ts +48 -38
  12. package/src/commands/dashboard.ts +7 -7
  13. package/src/commands/doctor.test.ts +8 -0
  14. package/src/commands/doctor.ts +2 -6
  15. package/src/commands/errors.test.ts +47 -40
  16. package/src/commands/errors.ts +5 -4
  17. package/src/commands/feed.test.ts +40 -33
  18. package/src/commands/feed.ts +3 -2
  19. package/src/commands/group.ts +23 -14
  20. package/src/commands/hooks.ts +2 -1
  21. package/src/commands/init.test.ts +104 -0
  22. package/src/commands/init.ts +11 -7
  23. package/src/commands/inspect.test.ts +2 -0
  24. package/src/commands/inspect.ts +9 -8
  25. package/src/commands/logs.test.ts +5 -6
  26. package/src/commands/logs.ts +2 -1
  27. package/src/commands/mail.test.ts +11 -10
  28. package/src/commands/mail.ts +11 -12
  29. package/src/commands/merge.ts +11 -12
  30. package/src/commands/metrics.test.ts +15 -2
  31. package/src/commands/metrics.ts +3 -2
  32. package/src/commands/monitor.ts +5 -4
  33. package/src/commands/nudge.ts +2 -3
  34. package/src/commands/prime.test.ts +1 -6
  35. package/src/commands/prime.ts +2 -3
  36. package/src/commands/replay.test.ts +62 -55
  37. package/src/commands/replay.ts +3 -2
  38. package/src/commands/run.ts +17 -20
  39. package/src/commands/sling.ts +2 -1
  40. package/src/commands/status.test.ts +2 -1
  41. package/src/commands/status.ts +7 -6
  42. package/src/commands/stop.test.ts +2 -0
  43. package/src/commands/stop.ts +10 -11
  44. package/src/commands/supervisor.ts +7 -6
  45. package/src/commands/trace.test.ts +52 -44
  46. package/src/commands/trace.ts +5 -4
  47. package/src/commands/watch.ts +8 -10
  48. package/src/commands/worktree.test.ts +21 -15
  49. package/src/commands/worktree.ts +10 -4
  50. package/src/index.ts +3 -1
@@ -11,6 +11,7 @@ import { join } from "node:path";
11
11
  import { Command } from "commander";
12
12
  import { loadConfig } from "../config.ts";
13
13
  import { ValidationError } from "../errors.ts";
14
+ import { jsonError, jsonOutput } from "../json.ts";
14
15
  import { color } from "../logging/color.ts";
15
16
  import { createMetricsStore } from "../metrics/store.ts";
16
17
  import { estimateCost, parseTranscriptUsage } from "../metrics/transcript.ts";
@@ -272,10 +273,7 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
272
273
  const transcriptPath = await discoverOrchestratorTranscript(config.project.root);
273
274
  if (!transcriptPath) {
274
275
  if (json) {
275
- process.stdout.write(
276
- JSON.stringify({ error: "no_transcript", message: "No orchestrator transcript found" }) +
277
- "\n",
278
- );
276
+ jsonError("costs", "No orchestrator transcript found");
279
277
  } else {
280
278
  process.stdout.write(
281
279
  "No orchestrator transcript found.\nExpected at: ~/.claude/projects/{project-key}/*.jsonl\n",
@@ -289,18 +287,16 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
289
287
  const cacheTotal = usage.cacheReadTokens + usage.cacheCreationTokens;
290
288
 
291
289
  if (json) {
292
- process.stdout.write(
293
- `${JSON.stringify({
294
- source: "self",
295
- transcriptPath,
296
- model: usage.modelUsed,
297
- inputTokens: usage.inputTokens,
298
- outputTokens: usage.outputTokens,
299
- cacheReadTokens: usage.cacheReadTokens,
300
- cacheCreationTokens: usage.cacheCreationTokens,
301
- estimatedCostUsd: cost,
302
- })}\n`,
303
- );
290
+ jsonOutput("costs", {
291
+ source: "self",
292
+ transcriptPath,
293
+ model: usage.modelUsed,
294
+ inputTokens: usage.inputTokens,
295
+ outputTokens: usage.outputTokens,
296
+ cacheReadTokens: usage.cacheReadTokens,
297
+ cacheCreationTokens: usage.cacheCreationTokens,
298
+ estimatedCostUsd: cost,
299
+ });
304
300
  } else {
305
301
  const w = process.stdout.write.bind(process.stdout);
306
302
  const separator = "\u2500".repeat(70);
@@ -327,9 +323,17 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
327
323
  const metricsFile = Bun.file(metricsDbPath);
328
324
  if (!(await metricsFile.exists())) {
329
325
  if (json) {
330
- process.stdout.write(
331
- `${JSON.stringify({ agents: [], totals: { inputTokens: 0, outputTokens: 0, cacheTokens: 0, costUsd: 0, burnRatePerMin: 0, tokensPerMin: 0 } })}\n`,
332
- );
326
+ jsonOutput("costs", {
327
+ agents: [],
328
+ totals: {
329
+ inputTokens: 0,
330
+ outputTokens: 0,
331
+ cacheTokens: 0,
332
+ costUsd: 0,
333
+ burnRatePerMin: 0,
334
+ tokensPerMin: 0,
335
+ },
336
+ });
333
337
  } else {
334
338
  process.stdout.write(
335
339
  "No live data available. Token snapshots begin after first tool call.\n",
@@ -345,9 +349,17 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
345
349
  const snapshots = metricsStore.getLatestSnapshots();
346
350
  if (snapshots.length === 0) {
347
351
  if (json) {
348
- process.stdout.write(
349
- `${JSON.stringify({ agents: [], totals: { inputTokens: 0, outputTokens: 0, cacheTokens: 0, costUsd: 0, burnRatePerMin: 0, tokensPerMin: 0 } })}\n`,
350
- );
352
+ jsonOutput("costs", {
353
+ agents: [],
354
+ totals: {
355
+ inputTokens: 0,
356
+ outputTokens: 0,
357
+ cacheTokens: 0,
358
+ costUsd: 0,
359
+ burnRatePerMin: 0,
360
+ tokensPerMin: 0,
361
+ },
362
+ });
351
363
  } else {
352
364
  process.stdout.write(
353
365
  "No live data available. Token snapshots begin after first tool call.\n",
@@ -428,19 +440,17 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
428
440
  const tokensPerMin = avgElapsedMs > 0 ? totalTokens / (avgElapsedMs / 60_000) : 0;
429
441
 
430
442
  if (json) {
431
- process.stdout.write(
432
- `${JSON.stringify({
433
- agents: agentData,
434
- totals: {
435
- inputTokens: totalInput,
436
- outputTokens: totalOutput,
437
- cacheTokens: totalCacheTokens,
438
- costUsd: totalCost,
439
- burnRatePerMin,
440
- tokensPerMin,
441
- },
442
- })}\n`,
443
- );
443
+ jsonOutput("costs", {
444
+ agents: agentData,
445
+ totals: {
446
+ inputTokens: totalInput,
447
+ outputTokens: totalOutput,
448
+ cacheTokens: totalCacheTokens,
449
+ costUsd: totalCost,
450
+ burnRatePerMin,
451
+ tokensPerMin,
452
+ },
453
+ });
444
454
  } else {
445
455
  const w = process.stdout.write.bind(process.stdout);
446
456
  const separator = "\u2500".repeat(70);
@@ -502,7 +512,7 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
502
512
  const metricsFile = Bun.file(metricsDbPath);
503
513
  if (!(await metricsFile.exists())) {
504
514
  if (json) {
505
- process.stdout.write("[]\n");
515
+ jsonOutput("costs", { sessions: [] });
506
516
  } else {
507
517
  process.stdout.write("No metrics data yet.\n");
508
518
  }
@@ -532,9 +542,9 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
532
542
  totals: group.totals,
533
543
  };
534
544
  }
535
- process.stdout.write(`${JSON.stringify(grouped)}\n`);
545
+ jsonOutput("costs", { grouped });
536
546
  } else {
537
- process.stdout.write(`${JSON.stringify(sessions)}\n`);
547
+ jsonOutput("costs", { sessions });
538
548
  }
539
549
  return;
540
550
  }
@@ -15,7 +15,7 @@ import { Command } from "commander";
15
15
  import { loadConfig } from "../config.ts";
16
16
  import { ValidationError } from "../errors.ts";
17
17
  import type { ColorFn } from "../logging/color.ts";
18
- import { color, noColor, visibleLength } from "../logging/color.ts";
18
+ import { accent, color, noColor, visibleLength } from "../logging/color.ts";
19
19
  import { createMailStore, type MailStore } from "../mail/store.ts";
20
20
  import { createMergeQueue, type MergeQueue } from "../merge/queue.ts";
21
21
  import { createMetricsStore, type MetricsStore } from "../metrics/store.ts";
@@ -404,7 +404,7 @@ async function loadDashboardData(
404
404
  function renderHeader(width: number, interval: number, currentRunId?: string | null): string {
405
405
  const left = color.bold(`ov dashboard v${PKG_VERSION}`);
406
406
  const now = new Date().toLocaleTimeString();
407
- const scope = currentRunId ? ` [run: ${currentRunId.slice(0, 8)}]` : " [all runs]";
407
+ const scope = currentRunId ? ` [run: ${accent(currentRunId.slice(0, 8))}]` : " [all runs]";
408
408
  const right = `${now}${scope} | refresh: ${interval}ms`;
409
409
  const padding = width - visibleLength(left) - right.length;
410
410
  const line = left + " ".repeat(Math.max(0, padding)) + right;
@@ -497,10 +497,10 @@ function renderAgentPanel(
497
497
 
498
498
  const icon = getStateIcon(agent.state);
499
499
  const stateColor = getStateColor(agent.state);
500
- const name = pad(truncate(agent.agentName, 15), 15);
500
+ const name = accent(pad(truncate(agent.agentName, 15), 15));
501
501
  const capability = pad(truncate(agent.capability, 12), 12);
502
502
  const state = pad(agent.state, 10);
503
- const taskId = pad(truncate(agent.taskId, 16), 16);
503
+ const taskId = accent(pad(truncate(agent.taskId, 16), 16));
504
504
  const endTime =
505
505
  agent.state === "completed" || agent.state === "zombie"
506
506
  ? new Date(agent.lastActivity).getTime()
@@ -575,8 +575,8 @@ function renderMailPanel(
575
575
 
576
576
  const priorityColorFn = getPriorityColor(msg.priority);
577
577
  const priority = msg.priority === "normal" ? "" : `[${msg.priority}] `;
578
- const from = truncate(msg.from, 12);
579
- const to = truncate(msg.to, 12);
578
+ const from = accent(truncate(msg.from, 12));
579
+ const to = accent(truncate(msg.to, 12));
580
580
  const subject = truncate(msg.subject, panelWidth - 40);
581
581
  const time = timeAgo(msg.createdAt);
582
582
 
@@ -643,7 +643,7 @@ function renderMergeQueuePanel(
643
643
 
644
644
  const statusColorFn = getMergeStatusColor(entry.status);
645
645
  const status = pad(entry.status, 10);
646
- const agent = truncate(entry.agentName, 15);
646
+ const agent = accent(truncate(entry.agentName, 15));
647
647
  const branch = truncate(entry.branchName, panelWidth - 30);
648
648
 
649
649
  const line = `${BOX.vertical} ${statusColorFn(status)} ${agent} ${branch}`;
@@ -85,10 +85,14 @@ describe("doctorCommand", () => {
85
85
  const out = output();
86
86
 
87
87
  const parsed = JSON.parse(out.trim()) as {
88
+ success: boolean;
89
+ command: string;
88
90
  checks: unknown[];
89
91
  summary: { pass: number; warn: number; fail: number };
90
92
  };
91
93
  expect(parsed).toBeDefined();
94
+ expect(parsed.success).toBe(true);
95
+ expect(parsed.command).toBe("doctor");
92
96
  expect(Array.isArray(parsed.checks)).toBe(true);
93
97
  expect(parsed.summary).toBeDefined();
94
98
  expect(typeof parsed.summary.pass).toBe("number");
@@ -101,9 +105,13 @@ describe("doctorCommand", () => {
101
105
  const out = output();
102
106
 
103
107
  const parsed = JSON.parse(out.trim()) as {
108
+ success: boolean;
109
+ command: string;
104
110
  checks: unknown[];
105
111
  summary: { pass: number; warn: number; fail: number };
106
112
  };
113
+ expect(parsed.success).toBe(true);
114
+ expect(parsed.command).toBe("doctor");
107
115
  expect(parsed.checks).toEqual([]);
108
116
  expect(parsed.summary.pass).toBe(0);
109
117
  expect(parsed.summary.warn).toBe(0);
@@ -18,6 +18,7 @@ import { checkStructure } from "../doctor/structure.ts";
18
18
  import type { DoctorCategory, DoctorCheck, DoctorCheckFn } from "../doctor/types.ts";
19
19
  import { checkVersion } from "../doctor/version.ts";
20
20
  import { ValidationError } from "../errors.ts";
21
+ import { jsonOutput } from "../json.ts";
21
22
  import { color } from "../logging/color.ts";
22
23
 
23
24
  /** Registry of all check modules in execution order. */
@@ -114,12 +115,7 @@ function printJSON(checks: DoctorCheck[]): void {
114
115
  const warn = checks.filter((c) => c.status === "warn").length;
115
116
  const fail = checks.filter((c) => c.status === "fail").length;
116
117
 
117
- const output = {
118
- checks,
119
- summary: { pass, warn, fail },
120
- };
121
-
122
- process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
118
+ jsonOutput("doctor", { checks, summary: { pass, warn, fail } });
123
119
  }
124
120
 
125
121
  /** Options for dependency injection in doctorCommand. */
@@ -133,7 +133,14 @@ describe("errorsCommand", () => {
133
133
  await errorsCommand(["--json"]);
134
134
  const out = output();
135
135
 
136
- expect(out).toBe("[]\n");
136
+ const parsed = JSON.parse(out.trim()) as {
137
+ success: boolean;
138
+ command: string;
139
+ events: unknown[];
140
+ };
141
+ expect(parsed.success).toBe(true);
142
+ expect(parsed.command).toBe("errors");
143
+ expect(parsed.events).toEqual([]);
137
144
  });
138
145
  });
139
146
 
@@ -157,9 +164,9 @@ describe("errorsCommand", () => {
157
164
  await errorsCommand(["--json"]);
158
165
  const out = output();
159
166
 
160
- const parsed = JSON.parse(out.trim()) as unknown[];
161
- expect(parsed).toHaveLength(2);
162
- expect(Array.isArray(parsed)).toBe(true);
167
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
168
+ expect(parsed.events).toHaveLength(2);
169
+ expect(Array.isArray(parsed.events)).toBe(true);
163
170
  });
164
171
 
165
172
  test("JSON output includes expected fields", async () => {
@@ -176,9 +183,9 @@ describe("errorsCommand", () => {
176
183
  await errorsCommand(["--json"]);
177
184
  const out = output();
178
185
 
179
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
180
- expect(parsed).toHaveLength(1);
181
- const event = parsed[0];
186
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
187
+ expect(parsed.events).toHaveLength(1);
188
+ const event = parsed.events[0];
182
189
  expect(event).toBeDefined();
183
190
  expect(event?.agentName).toBe("builder-1");
184
191
  expect(event?.eventType).toBe("error");
@@ -201,8 +208,8 @@ describe("errorsCommand", () => {
201
208
  await errorsCommand(["--json"]);
202
209
  const out = output();
203
210
 
204
- const parsed = JSON.parse(out.trim()) as unknown[];
205
- expect(parsed).toEqual([]);
211
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
212
+ expect(parsed.events).toEqual([]);
206
213
  });
207
214
  });
208
215
 
@@ -388,9 +395,9 @@ describe("errorsCommand", () => {
388
395
  await errorsCommand(["--agent", "builder-1", "--json"]);
389
396
  const out = output();
390
397
 
391
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
392
- expect(parsed).toHaveLength(2);
393
- for (const event of parsed) {
398
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
399
+ expect(parsed.events).toHaveLength(2);
400
+ for (const event of parsed.events) {
394
401
  expect(event.agentName).toBe("builder-1");
395
402
  }
396
403
  });
@@ -404,8 +411,8 @@ describe("errorsCommand", () => {
404
411
  await errorsCommand(["--agent", "nonexistent", "--json"]);
405
412
  const out = output();
406
413
 
407
- const parsed = JSON.parse(out.trim()) as unknown[];
408
- expect(parsed).toEqual([]);
414
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
415
+ expect(parsed.events).toEqual([]);
409
416
  });
410
417
 
411
418
  test("only returns error-level events for the agent", async () => {
@@ -431,9 +438,9 @@ describe("errorsCommand", () => {
431
438
  await errorsCommand(["--agent", "builder-1", "--json"]);
432
439
  const out = output();
433
440
 
434
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
435
- expect(parsed).toHaveLength(1);
436
- expect(parsed[0]?.level).toBe("error");
441
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
442
+ expect(parsed.events).toHaveLength(1);
443
+ expect(parsed.events[0]?.level).toBe("error");
437
444
  });
438
445
  });
439
446
 
@@ -451,9 +458,9 @@ describe("errorsCommand", () => {
451
458
  await errorsCommand(["--run", "run-001", "--json"]);
452
459
  const out = output();
453
460
 
454
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
455
- expect(parsed).toHaveLength(2);
456
- for (const event of parsed) {
461
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
462
+ expect(parsed.events).toHaveLength(2);
463
+ for (const event of parsed.events) {
457
464
  expect(event.runId).toBe("run-001");
458
465
  }
459
466
  });
@@ -467,8 +474,8 @@ describe("errorsCommand", () => {
467
474
  await errorsCommand(["--run", "run-999", "--json"]);
468
475
  const out = output();
469
476
 
470
- const parsed = JSON.parse(out.trim()) as unknown[];
471
- expect(parsed).toEqual([]);
477
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
478
+ expect(parsed.events).toEqual([]);
472
479
  });
473
480
 
474
481
  test("only returns error-level events for the run", async () => {
@@ -487,9 +494,9 @@ describe("errorsCommand", () => {
487
494
  await errorsCommand(["--run", "run-001", "--json"]);
488
495
  const out = output();
489
496
 
490
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
491
- expect(parsed).toHaveLength(1);
492
- expect(parsed[0]?.level).toBe("error");
497
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
498
+ expect(parsed.events).toHaveLength(1);
499
+ expect(parsed.events[0]?.level).toBe("error");
493
500
  });
494
501
  });
495
502
 
@@ -507,8 +514,8 @@ describe("errorsCommand", () => {
507
514
  await errorsCommand(["--json", "--limit", "3"]);
508
515
  const out = output();
509
516
 
510
- const parsed = JSON.parse(out.trim()) as unknown[];
511
- expect(parsed).toHaveLength(3);
517
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
518
+ expect(parsed.events).toHaveLength(3);
512
519
  });
513
520
 
514
521
  test("default limit is 100", async () => {
@@ -522,8 +529,8 @@ describe("errorsCommand", () => {
522
529
  await errorsCommand(["--json"]);
523
530
  const out = output();
524
531
 
525
- const parsed = JSON.parse(out.trim()) as unknown[];
526
- expect(parsed).toHaveLength(100);
532
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
533
+ expect(parsed.events).toHaveLength(100);
527
534
  });
528
535
  });
529
536
 
@@ -539,8 +546,8 @@ describe("errorsCommand", () => {
539
546
  await errorsCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
540
547
  const out = output();
541
548
 
542
- const parsed = JSON.parse(out.trim()) as unknown[];
543
- expect(parsed).toEqual([]);
549
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
550
+ expect(parsed.events).toEqual([]);
544
551
  });
545
552
 
546
553
  test("--since with past timestamp returns all errors", async () => {
@@ -553,8 +560,8 @@ describe("errorsCommand", () => {
553
560
  await errorsCommand(["--json", "--since", "2020-01-01T00:00:00Z"]);
554
561
  const out = output();
555
562
 
556
- const parsed = JSON.parse(out.trim()) as unknown[];
557
- expect(parsed).toHaveLength(2);
563
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
564
+ expect(parsed.events).toHaveLength(2);
558
565
  });
559
566
 
560
567
  test("--until with past timestamp returns no errors", async () => {
@@ -566,8 +573,8 @@ describe("errorsCommand", () => {
566
573
  await errorsCommand(["--json", "--until", "2000-01-01T00:00:00Z"]);
567
574
  const out = output();
568
575
 
569
- const parsed = JSON.parse(out.trim()) as unknown[];
570
- expect(parsed).toEqual([]);
576
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
577
+ expect(parsed.events).toEqual([]);
571
578
  });
572
579
  });
573
580
 
@@ -608,8 +615,8 @@ describe("errorsCommand", () => {
608
615
  await errorsCommand(["--json"]);
609
616
  const out = output();
610
617
 
611
- const parsed = JSON.parse(out.trim()) as unknown[];
612
- expect(parsed).toHaveLength(3);
618
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
619
+ expect(parsed.events).toHaveLength(3);
613
620
  });
614
621
 
615
622
  test("excludes non-error events from global view", async () => {
@@ -639,9 +646,9 @@ describe("errorsCommand", () => {
639
646
  await errorsCommand(["--json"]);
640
647
  const out = output();
641
648
 
642
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
643
- expect(parsed).toHaveLength(1);
644
- expect(parsed[0]?.level).toBe("error");
649
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
650
+ expect(parsed.events).toHaveLength(1);
651
+ expect(parsed.events[0]?.level).toBe("error");
645
652
  });
646
653
  });
647
654
  });
@@ -11,7 +11,8 @@ import { Command } from "commander";
11
11
  import { loadConfig } from "../config.ts";
12
12
  import { ValidationError } from "../errors.ts";
13
13
  import { createEventStore } from "../events/store.ts";
14
- import { color } from "../logging/color.ts";
14
+ import { jsonOutput } from "../json.ts";
15
+ import { accent, color } from "../logging/color.ts";
15
16
  import type { StoredEvent } from "../types.ts";
16
17
 
17
18
  /**
@@ -115,7 +116,7 @@ function printErrors(events: StoredEvent[]): void {
115
116
  firstGroup = false;
116
117
 
117
118
  w(
118
- `${color.bold(agentName)} ${color.dim(`(${agentEvents.length} error${agentEvents.length === 1 ? "" : "s"})`)}\n`,
119
+ `${accent(agentName)} ${color.dim(`(${agentEvents.length} error${agentEvents.length === 1 ? "" : "s"})`)}\n`,
119
120
  );
120
121
 
121
122
  for (const event of agentEvents) {
@@ -179,7 +180,7 @@ async function executeErrors(opts: ErrorsOpts): Promise<void> {
179
180
  const eventsFile = Bun.file(eventsDbPath);
180
181
  if (!(await eventsFile.exists())) {
181
182
  if (json) {
182
- process.stdout.write("[]\n");
183
+ jsonOutput("errors", { events: [] });
183
184
  } else {
184
185
  process.stdout.write("No events data yet.\n");
185
186
  }
@@ -209,7 +210,7 @@ async function executeErrors(opts: ErrorsOpts): Promise<void> {
209
210
  }
210
211
 
211
212
  if (json) {
212
- process.stdout.write(`${JSON.stringify(events)}\n`);
213
+ jsonOutput("errors", { events });
213
214
  return;
214
215
  }
215
216
 
@@ -138,7 +138,14 @@ describe("feedCommand", () => {
138
138
  await feedCommand(["--json"]);
139
139
  const out = output();
140
140
 
141
- expect(out).toBe("[]\n");
141
+ const parsed = JSON.parse(out.trim()) as {
142
+ success: boolean;
143
+ command: string;
144
+ events: unknown[];
145
+ };
146
+ expect(parsed.success).toBe(true);
147
+ expect(parsed.command).toBe("feed");
148
+ expect(parsed.events).toEqual([]);
142
149
  });
143
150
  });
144
151
 
@@ -156,9 +163,9 @@ describe("feedCommand", () => {
156
163
  await feedCommand(["--json"]);
157
164
  const out = output();
158
165
 
159
- const parsed = JSON.parse(out.trim()) as unknown[];
160
- expect(parsed).toHaveLength(3);
161
- expect(Array.isArray(parsed)).toBe(true);
166
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
167
+ expect(parsed.events).toHaveLength(3);
168
+ expect(Array.isArray(parsed.events)).toBe(true);
162
169
  });
163
170
 
164
171
  test("JSON output includes expected fields", async () => {
@@ -177,9 +184,9 @@ describe("feedCommand", () => {
177
184
  await feedCommand(["--json"]);
178
185
  const out = output();
179
186
 
180
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
181
- expect(parsed).toHaveLength(1);
182
- const event = parsed[0];
187
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
188
+ expect(parsed.events).toHaveLength(1);
189
+ const event = parsed.events[0];
183
190
  expect(event).toBeDefined();
184
191
  expect(event?.agentName).toBe("builder-1");
185
192
  expect(event?.eventType).toBe("tool_start");
@@ -198,8 +205,8 @@ describe("feedCommand", () => {
198
205
  await feedCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
199
206
  const out = output();
200
207
 
201
- const parsed = JSON.parse(out.trim()) as unknown[];
202
- expect(parsed).toEqual([]);
208
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
209
+ expect(parsed.events).toEqual([]);
203
210
  });
204
211
  });
205
212
 
@@ -317,9 +324,9 @@ describe("feedCommand", () => {
317
324
  await feedCommand(["--agent", "builder-1", "--json"]);
318
325
  const out = output();
319
326
 
320
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
321
- expect(parsed).toHaveLength(1);
322
- expect(parsed[0]?.agentName).toBe("builder-1");
327
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
328
+ expect(parsed.events).toHaveLength(1);
329
+ expect(parsed.events[0]?.agentName).toBe("builder-1");
323
330
  });
324
331
 
325
332
  test("filters to multiple agents", async () => {
@@ -333,9 +340,9 @@ describe("feedCommand", () => {
333
340
  await feedCommand(["--agent", "builder-1", "--agent", "scout-1", "--json"]);
334
341
  const out = output();
335
342
 
336
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
337
- expect(parsed).toHaveLength(2);
338
- const agents = parsed.map((e) => e.agentName);
343
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
344
+ expect(parsed.events).toHaveLength(2);
345
+ const agents = parsed.events.map((e) => e.agentName);
339
346
  expect(agents).toContain("builder-1");
340
347
  expect(agents).toContain("scout-1");
341
348
  expect(agents).not.toContain("builder-2");
@@ -356,9 +363,9 @@ describe("feedCommand", () => {
356
363
  await feedCommand(["--run", "run-001", "--json"]);
357
364
  const out = output();
358
365
 
359
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
360
- expect(parsed).toHaveLength(2);
361
- for (const event of parsed) {
366
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
367
+ expect(parsed.events).toHaveLength(2);
368
+ for (const event of parsed.events) {
362
369
  expect(event.runId).toBe("run-001");
363
370
  }
364
371
  });
@@ -378,8 +385,8 @@ describe("feedCommand", () => {
378
385
  await feedCommand(["--json", "--limit", "10"]);
379
386
  const out = output();
380
387
 
381
- const parsed = JSON.parse(out.trim()) as unknown[];
382
- expect(parsed).toHaveLength(10);
388
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
389
+ expect(parsed.events).toHaveLength(10);
383
390
  });
384
391
 
385
392
  test("default limit is 50", async () => {
@@ -393,8 +400,8 @@ describe("feedCommand", () => {
393
400
  await feedCommand(["--json"]);
394
401
  const out = output();
395
402
 
396
- const parsed = JSON.parse(out.trim()) as unknown[];
397
- expect(parsed).toHaveLength(50);
403
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
404
+ expect(parsed.events).toHaveLength(50);
398
405
  });
399
406
  });
400
407
 
@@ -411,8 +418,8 @@ describe("feedCommand", () => {
411
418
  await feedCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
412
419
  const out = output();
413
420
 
414
- const parsed = JSON.parse(out.trim()) as unknown[];
415
- expect(parsed).toEqual([]);
421
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
422
+ expect(parsed.events).toEqual([]);
416
423
  });
417
424
 
418
425
  test("--since with past timestamp returns all events", async () => {
@@ -425,8 +432,8 @@ describe("feedCommand", () => {
425
432
  await feedCommand(["--json", "--since", "2020-01-01T00:00:00Z"]);
426
433
  const out = output();
427
434
 
428
- const parsed = JSON.parse(out.trim()) as unknown[];
429
- expect(parsed).toHaveLength(2);
435
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
436
+ expect(parsed.events).toHaveLength(2);
430
437
  });
431
438
 
432
439
  test("default since is 5 minutes ago", async () => {
@@ -440,8 +447,8 @@ describe("feedCommand", () => {
440
447
  await feedCommand(["--json"]);
441
448
  const out = output();
442
449
 
443
- const parsed = JSON.parse(out.trim()) as unknown[];
444
- expect(parsed).toHaveLength(1);
450
+ const parsed = JSON.parse(out.trim()) as { events: unknown[] };
451
+ expect(parsed.events).toHaveLength(1);
445
452
  });
446
453
  });
447
454
 
@@ -503,11 +510,11 @@ describe("feedCommand", () => {
503
510
  await feedCommand(["--json"]);
504
511
  const out = output();
505
512
 
506
- const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
507
- expect(parsed).toHaveLength(3);
508
- expect(parsed[0]?.eventType).toBe("session_start");
509
- expect(parsed[1]?.eventType).toBe("tool_start");
510
- expect(parsed[2]?.eventType).toBe("session_end");
513
+ const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
514
+ expect(parsed.events).toHaveLength(3);
515
+ expect(parsed.events[0]?.eventType).toBe("session_start");
516
+ expect(parsed.events[1]?.eventType).toBe("tool_start");
517
+ expect(parsed.events[2]?.eventType).toBe("session_end");
511
518
  });
512
519
 
513
520
  test("handles event with all null optional fields", async () => {
@@ -11,6 +11,7 @@ import { Command } from "commander";
11
11
  import { loadConfig } from "../config.ts";
12
12
  import { ValidationError } from "../errors.ts";
13
13
  import { createEventStore } from "../events/store.ts";
14
+ import { jsonOutput } from "../json.ts";
14
15
  import type { ColorFn } from "../logging/color.ts";
15
16
  import { color } from "../logging/color.ts";
16
17
  import type { EventType, StoredEvent } from "../types.ts";
@@ -189,7 +190,7 @@ async function executeFeed(opts: FeedOpts): Promise<void> {
189
190
  const eventsFile = Bun.file(eventsDbPath);
190
191
  if (!(await eventsFile.exists())) {
191
192
  if (json) {
192
- process.stdout.write("[]\n");
193
+ jsonOutput("feed", { events: [] });
193
194
  } else {
194
195
  process.stdout.write("No events data yet.\n");
195
196
  }
@@ -228,7 +229,7 @@ async function executeFeed(opts: FeedOpts): Promise<void> {
228
229
  const events = queryEvents({ since, limit });
229
230
 
230
231
  if (json) {
231
- process.stdout.write(`${JSON.stringify(events)}\n`);
232
+ jsonOutput("feed", { events });
232
233
  return;
233
234
  }
234
235