@os-eco/overstory-cli 0.6.8 → 0.6.10

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 (69) hide show
  1. package/README.md +19 -5
  2. package/agents/builder.md +6 -15
  3. package/agents/lead.md +4 -6
  4. package/agents/merger.md +5 -13
  5. package/agents/reviewer.md +2 -9
  6. package/package.json +1 -1
  7. package/src/agents/hooks-deployer.test.ts +232 -0
  8. package/src/agents/hooks-deployer.ts +54 -8
  9. package/src/agents/overlay.test.ts +156 -1
  10. package/src/agents/overlay.ts +67 -7
  11. package/src/commands/agents.ts +9 -6
  12. package/src/commands/clean.ts +2 -1
  13. package/src/commands/completions.test.ts +8 -20
  14. package/src/commands/completions.ts +7 -6
  15. package/src/commands/coordinator.test.ts +8 -0
  16. package/src/commands/coordinator.ts +11 -8
  17. package/src/commands/costs.test.ts +48 -38
  18. package/src/commands/costs.ts +48 -38
  19. package/src/commands/dashboard.ts +7 -7
  20. package/src/commands/doctor.test.ts +8 -0
  21. package/src/commands/doctor.ts +96 -51
  22. package/src/commands/ecosystem.ts +291 -0
  23. package/src/commands/errors.test.ts +47 -40
  24. package/src/commands/errors.ts +5 -4
  25. package/src/commands/feed.test.ts +40 -33
  26. package/src/commands/feed.ts +5 -4
  27. package/src/commands/group.ts +23 -14
  28. package/src/commands/hooks.ts +2 -1
  29. package/src/commands/init.test.ts +104 -0
  30. package/src/commands/init.ts +11 -7
  31. package/src/commands/inspect.test.ts +2 -0
  32. package/src/commands/inspect.ts +9 -8
  33. package/src/commands/logs.test.ts +5 -6
  34. package/src/commands/logs.ts +2 -1
  35. package/src/commands/mail.test.ts +11 -10
  36. package/src/commands/mail.ts +11 -12
  37. package/src/commands/merge.ts +11 -12
  38. package/src/commands/metrics.test.ts +15 -2
  39. package/src/commands/metrics.ts +3 -2
  40. package/src/commands/monitor.ts +5 -4
  41. package/src/commands/nudge.ts +2 -3
  42. package/src/commands/prime.test.ts +1 -6
  43. package/src/commands/prime.ts +2 -3
  44. package/src/commands/replay.test.ts +62 -55
  45. package/src/commands/replay.ts +3 -2
  46. package/src/commands/run.ts +17 -20
  47. package/src/commands/sling.ts +3 -2
  48. package/src/commands/status.test.ts +2 -1
  49. package/src/commands/status.ts +7 -6
  50. package/src/commands/stop.test.ts +2 -0
  51. package/src/commands/stop.ts +10 -11
  52. package/src/commands/supervisor.ts +7 -6
  53. package/src/commands/trace.test.ts +52 -44
  54. package/src/commands/trace.ts +5 -4
  55. package/src/commands/upgrade.test.ts +46 -0
  56. package/src/commands/upgrade.ts +259 -0
  57. package/src/commands/watch.ts +8 -10
  58. package/src/commands/worktree.test.ts +21 -15
  59. package/src/commands/worktree.ts +10 -4
  60. package/src/doctor/databases.test.ts +38 -0
  61. package/src/doctor/databases.ts +7 -10
  62. package/src/doctor/ecosystem.test.ts +307 -0
  63. package/src/doctor/ecosystem.ts +155 -0
  64. package/src/doctor/merge-queue.test.ts +98 -0
  65. package/src/doctor/merge-queue.ts +23 -0
  66. package/src/doctor/structure.test.ts +130 -1
  67. package/src/doctor/structure.ts +87 -1
  68. package/src/doctor/types.ts +5 -2
  69. package/src/index.ts +25 -1
@@ -0,0 +1,291 @@
1
+ /**
2
+ * CLI command: ov ecosystem
3
+ *
4
+ * Shows a summary dashboard of all installed os-eco tools: version, update
5
+ * status (latest vs outdated), and doctor health (overstory only).
6
+ */
7
+
8
+ import { Command } from "commander";
9
+ import { jsonError, jsonOutput } from "../json.ts";
10
+ import { accent, brand, color, muted } from "../logging/color.ts";
11
+
12
+ const TOOLS = [
13
+ { name: "overstory", cli: "ov", npm: "@os-eco/overstory-cli" },
14
+ { name: "mulch", cli: "ml", npm: "@os-eco/mulch-cli" },
15
+ { name: "seeds", cli: "sd", npm: "@os-eco/seeds-cli" },
16
+ { name: "canopy", cli: "cn", npm: "@os-eco/canopy-cli" },
17
+ ] as const;
18
+
19
+ export interface EcosystemOptions {
20
+ json?: boolean;
21
+ }
22
+
23
+ interface DoctorSummary {
24
+ pass: number;
25
+ warn: number;
26
+ fail: number;
27
+ }
28
+
29
+ interface ToolResult {
30
+ name: string;
31
+ cli: string;
32
+ npm: string;
33
+ installed: boolean;
34
+ version?: string;
35
+ latest?: string;
36
+ upToDate?: boolean;
37
+ doctorSummary?: DoctorSummary;
38
+ latestError?: string;
39
+ }
40
+
41
+ async function getInstalledVersion(cli: string): Promise<string | null> {
42
+ // Try --version --json first
43
+ try {
44
+ const proc = Bun.spawn([cli, "--version", "--json"], {
45
+ stdout: "pipe",
46
+ stderr: "pipe",
47
+ });
48
+ const exitCode = await proc.exited;
49
+ if (exitCode === 0) {
50
+ const stdout = await new Response(proc.stdout).text();
51
+ try {
52
+ const data = JSON.parse(stdout.trim()) as { version?: string };
53
+ if (data.version) return data.version;
54
+ } catch {
55
+ // Not valid JSON, fall through to plain text
56
+ }
57
+ }
58
+ } catch {
59
+ // CLI not found — fall through to plain text fallback
60
+ }
61
+
62
+ // Fallback: --version plain text
63
+ try {
64
+ const proc = Bun.spawn([cli, "--version"], {
65
+ stdout: "pipe",
66
+ stderr: "pipe",
67
+ });
68
+ const exitCode = await proc.exited;
69
+ if (exitCode === 0) {
70
+ const stdout = await new Response(proc.stdout).text();
71
+ const match = stdout.match(/(\d+\.\d+\.\d+)/);
72
+ if (match?.[1]) return match[1];
73
+ }
74
+ } catch {
75
+ // CLI not found
76
+ }
77
+
78
+ return null;
79
+ }
80
+
81
+ async function fetchLatestVersion(packageName: string): Promise<string> {
82
+ const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
83
+ if (!res.ok) {
84
+ throw new Error(`npm registry error: ${res.status} ${res.statusText}`);
85
+ }
86
+ const data = (await res.json()) as { version: string };
87
+ return data.version;
88
+ }
89
+
90
+ async function getDoctorSummary(): Promise<DoctorSummary | undefined> {
91
+ try {
92
+ const proc = Bun.spawn(["ov", "doctor", "--json"], {
93
+ stdout: "pipe",
94
+ stderr: "pipe",
95
+ });
96
+ await proc.exited;
97
+ const stdout = await new Response(proc.stdout).text();
98
+ const trimmed = stdout.trim();
99
+ if (trimmed) {
100
+ const data = JSON.parse(trimmed) as {
101
+ summary?: { pass?: number; warn?: number; fail?: number };
102
+ };
103
+ if (data.summary) {
104
+ return {
105
+ pass: data.summary.pass ?? 0,
106
+ warn: data.summary.warn ?? 0,
107
+ fail: data.summary.fail ?? 0,
108
+ };
109
+ }
110
+ }
111
+ } catch {
112
+ // Doctor failed — report nothing
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ async function checkTool(tool: { name: string; cli: string; npm: string }): Promise<ToolResult> {
118
+ const version = await getInstalledVersion(tool.cli);
119
+
120
+ if (version === null) {
121
+ return { name: tool.name, cli: tool.cli, npm: tool.npm, installed: false };
122
+ }
123
+
124
+ let latest: string | undefined;
125
+ let latestError: string | undefined;
126
+ let doctorSummary: DoctorSummary | undefined;
127
+
128
+ const latestPromise = fetchLatestVersion(tool.npm)
129
+ .then((v) => {
130
+ latest = v;
131
+ })
132
+ .catch((err) => {
133
+ latestError = err instanceof Error ? err.message : String(err);
134
+ });
135
+
136
+ const doctorPromise =
137
+ tool.name === "overstory"
138
+ ? getDoctorSummary().then((d) => {
139
+ doctorSummary = d;
140
+ })
141
+ : Promise.resolve();
142
+
143
+ await Promise.all([latestPromise, doctorPromise]);
144
+
145
+ const upToDate = latest !== undefined ? version === latest : undefined;
146
+
147
+ return {
148
+ name: tool.name,
149
+ cli: tool.cli,
150
+ npm: tool.npm,
151
+ installed: true,
152
+ version,
153
+ latest,
154
+ upToDate,
155
+ doctorSummary,
156
+ latestError,
157
+ };
158
+ }
159
+
160
+ function formatDoctorLine(summary: DoctorSummary): string {
161
+ const parts: string[] = [];
162
+ if (summary.pass > 0) parts.push(color.green(`${summary.pass} passed`));
163
+ if (summary.warn > 0) parts.push(color.yellow(`${summary.warn} warn`));
164
+ if (summary.fail > 0) parts.push(color.red(`${summary.fail} fail`));
165
+ return parts.length > 0 ? parts.join(", ") : "no checks";
166
+ }
167
+
168
+ function printHumanOutput(results: ToolResult[]): void {
169
+ process.stdout.write(`${brand.bold("os-eco Ecosystem")}\n`);
170
+ process.stdout.write(`${"═".repeat(60)}\n`);
171
+ process.stdout.write("\n");
172
+
173
+ for (const tool of results) {
174
+ if (!tool.installed) {
175
+ process.stdout.write(
176
+ ` ${color.red("x")} ${accent(tool.name)} ${muted(`(${tool.cli})`)} ${color.red("not installed")}\n`,
177
+ );
178
+ process.stdout.write(` ${muted(`npm i -g ${tool.npm}`)}\n`);
179
+ process.stdout.write("\n");
180
+ continue;
181
+ }
182
+
183
+ // Determine status icon
184
+ let icon: string;
185
+ if (tool.latestError !== undefined || tool.upToDate === undefined) {
186
+ icon = muted("-");
187
+ } else if (tool.upToDate) {
188
+ icon = color.green("-");
189
+ } else {
190
+ icon = color.yellow("!");
191
+ }
192
+
193
+ process.stdout.write(` ${icon} ${accent(tool.name)} ${muted(`(${tool.cli})`)}\n`);
194
+
195
+ // Version line
196
+ let versionLine = `Version: ${tool.version}`;
197
+ if (tool.latestError !== undefined) {
198
+ versionLine += ` ${muted("(version check failed)")}`;
199
+ } else if (tool.upToDate === true) {
200
+ versionLine += ` ${color.green("(up to date)")}`;
201
+ } else if (tool.upToDate === false) {
202
+ versionLine += ` ${color.yellow(`(outdated, latest: ${tool.latest})`)}`;
203
+ }
204
+ process.stdout.write(` ${versionLine}\n`);
205
+
206
+ // Doctor summary (overstory only)
207
+ if (tool.doctorSummary !== undefined) {
208
+ process.stdout.write(` Doctor: ${formatDoctorLine(tool.doctorSummary)}\n`);
209
+ }
210
+
211
+ process.stdout.write("\n");
212
+ }
213
+
214
+ const installed = results.filter((t) => t.installed).length;
215
+ const missing = results.filter((t) => !t.installed).length;
216
+ const outdated = results.filter(
217
+ (t) => t.installed && t.upToDate === false && t.latestError === undefined,
218
+ ).length;
219
+
220
+ let summary = `Summary: ${installed}/${results.length} installed`;
221
+ if (missing > 0) summary += `, ${missing} missing`;
222
+ if (outdated > 0) summary += `, ${outdated} outdated`;
223
+ process.stdout.write(`${summary}\n`);
224
+ }
225
+
226
+ export async function executeEcosystem(opts: EcosystemOptions): Promise<void> {
227
+ const json = opts.json ?? false;
228
+
229
+ let results: ToolResult[];
230
+ try {
231
+ results = await Promise.all(TOOLS.map((tool) => checkTool(tool)));
232
+ } catch (err) {
233
+ const msg = err instanceof Error ? err.message : String(err);
234
+ if (json) {
235
+ jsonError("ecosystem", msg);
236
+ } else {
237
+ process.stderr.write(`Error: ${msg}\n`);
238
+ }
239
+ process.exitCode = 1;
240
+ return;
241
+ }
242
+
243
+ if (json) {
244
+ const installed = results.filter((t) => t.installed).length;
245
+ const missing = results.filter((t) => !t.installed).length;
246
+ const outdated = results.filter(
247
+ (t) => t.installed && t.upToDate === false && t.latestError === undefined,
248
+ ).length;
249
+
250
+ jsonOutput("ecosystem", {
251
+ tools: results.map((t) => {
252
+ const entry: Record<string, unknown> = {
253
+ name: t.name,
254
+ cli: t.cli,
255
+ npm: t.npm,
256
+ installed: t.installed,
257
+ };
258
+ if (t.installed) {
259
+ entry.version = t.version;
260
+ entry.latest = t.latest;
261
+ entry.upToDate = t.upToDate;
262
+ if (t.doctorSummary !== undefined) {
263
+ entry.doctorSummary = t.doctorSummary;
264
+ }
265
+ if (t.latestError !== undefined) {
266
+ entry.latestError = t.latestError;
267
+ }
268
+ }
269
+ return entry;
270
+ }),
271
+ summary: {
272
+ total: results.length,
273
+ installed,
274
+ missing,
275
+ outdated,
276
+ },
277
+ });
278
+ return;
279
+ }
280
+
281
+ printHumanOutput(results);
282
+ }
283
+
284
+ export function createEcosystemCommand(): Command {
285
+ return new Command("ecosystem")
286
+ .description("Show a summary dashboard of all installed os-eco tools")
287
+ .option("--json", "Output as JSON")
288
+ .action(async (opts: EcosystemOptions) => {
289
+ await executeEcosystem(opts);
290
+ });
291
+ }
@@ -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 () => {