@os-eco/overstory-cli 0.6.1 → 0.6.5

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 (110) hide show
  1. package/README.md +8 -7
  2. package/package.json +12 -4
  3. package/src/agents/checkpoint.test.ts +2 -2
  4. package/src/agents/hooks-deployer.test.ts +131 -16
  5. package/src/agents/hooks-deployer.ts +33 -1
  6. package/src/agents/identity.test.ts +27 -27
  7. package/src/agents/identity.ts +10 -10
  8. package/src/agents/lifecycle.test.ts +6 -6
  9. package/src/agents/lifecycle.ts +2 -2
  10. package/src/agents/manifest.test.ts +86 -0
  11. package/src/agents/overlay.test.ts +9 -9
  12. package/src/agents/overlay.ts +4 -4
  13. package/src/commands/agents.test.ts +8 -8
  14. package/src/commands/agents.ts +62 -91
  15. package/src/commands/clean.test.ts +36 -51
  16. package/src/commands/clean.ts +28 -49
  17. package/src/commands/completions.ts +14 -0
  18. package/src/commands/coordinator.test.ts +133 -26
  19. package/src/commands/coordinator.ts +101 -64
  20. package/src/commands/costs.test.ts +47 -47
  21. package/src/commands/costs.ts +96 -75
  22. package/src/commands/dashboard.test.ts +2 -2
  23. package/src/commands/dashboard.ts +75 -95
  24. package/src/commands/doctor.test.ts +2 -2
  25. package/src/commands/doctor.ts +92 -79
  26. package/src/commands/errors.test.ts +2 -2
  27. package/src/commands/errors.ts +56 -50
  28. package/src/commands/feed.test.ts +2 -2
  29. package/src/commands/feed.ts +86 -83
  30. package/src/commands/group.ts +167 -177
  31. package/src/commands/hooks.test.ts +2 -2
  32. package/src/commands/hooks.ts +52 -42
  33. package/src/commands/init.test.ts +19 -19
  34. package/src/commands/init.ts +7 -16
  35. package/src/commands/inspect.test.ts +18 -18
  36. package/src/commands/inspect.ts +55 -58
  37. package/src/commands/log.test.ts +26 -31
  38. package/src/commands/log.ts +97 -91
  39. package/src/commands/logs.test.ts +1 -1
  40. package/src/commands/logs.ts +101 -104
  41. package/src/commands/mail.test.ts +5 -5
  42. package/src/commands/mail.ts +157 -169
  43. package/src/commands/merge.test.ts +28 -66
  44. package/src/commands/merge.ts +21 -51
  45. package/src/commands/metrics.test.ts +8 -8
  46. package/src/commands/metrics.ts +34 -35
  47. package/src/commands/monitor.test.ts +3 -3
  48. package/src/commands/monitor.ts +57 -62
  49. package/src/commands/nudge.test.ts +1 -1
  50. package/src/commands/nudge.ts +41 -89
  51. package/src/commands/prime.test.ts +19 -51
  52. package/src/commands/prime.ts +13 -50
  53. package/src/commands/replay.test.ts +2 -2
  54. package/src/commands/replay.ts +79 -86
  55. package/src/commands/run.test.ts +1 -1
  56. package/src/commands/run.ts +97 -77
  57. package/src/commands/sling.test.ts +201 -5
  58. package/src/commands/sling.ts +37 -64
  59. package/src/commands/spec.test.ts +14 -40
  60. package/src/commands/spec.ts +32 -101
  61. package/src/commands/status.test.ts +97 -1
  62. package/src/commands/status.ts +63 -58
  63. package/src/commands/stop.test.ts +22 -40
  64. package/src/commands/stop.ts +18 -33
  65. package/src/commands/supervisor.test.ts +12 -14
  66. package/src/commands/supervisor.ts +144 -165
  67. package/src/commands/trace.test.ts +15 -15
  68. package/src/commands/trace.ts +59 -82
  69. package/src/commands/watch.test.ts +2 -2
  70. package/src/commands/watch.ts +38 -45
  71. package/src/commands/worktree.test.ts +213 -37
  72. package/src/commands/worktree.ts +110 -55
  73. package/src/config.test.ts +96 -0
  74. package/src/doctor/consistency.test.ts +14 -14
  75. package/src/doctor/databases.test.ts +22 -2
  76. package/src/doctor/databases.ts +16 -0
  77. package/src/doctor/dependencies.test.ts +55 -1
  78. package/src/doctor/dependencies.ts +113 -18
  79. package/src/doctor/merge-queue.test.ts +4 -4
  80. package/src/e2e/init-sling-lifecycle.test.ts +8 -8
  81. package/src/errors.ts +1 -1
  82. package/src/index.ts +223 -213
  83. package/src/logging/color.test.ts +74 -91
  84. package/src/logging/color.ts +52 -46
  85. package/src/logging/reporter.test.ts +10 -10
  86. package/src/logging/reporter.ts +6 -5
  87. package/src/mail/broadcast.test.ts +1 -1
  88. package/src/mail/client.test.ts +6 -6
  89. package/src/mail/store.test.ts +3 -3
  90. package/src/merge/queue.test.ts +73 -7
  91. package/src/merge/queue.ts +17 -2
  92. package/src/merge/resolver.test.ts +159 -7
  93. package/src/merge/resolver.ts +46 -2
  94. package/src/metrics/store.test.ts +44 -44
  95. package/src/metrics/store.ts +2 -2
  96. package/src/metrics/summary.test.ts +35 -35
  97. package/src/mulch/client.test.ts +1 -1
  98. package/src/schema-consistency.test.ts +239 -0
  99. package/src/sessions/compat.test.ts +3 -3
  100. package/src/sessions/compat.ts +2 -2
  101. package/src/sessions/store.test.ts +41 -4
  102. package/src/sessions/store.ts +13 -2
  103. package/src/types.ts +14 -14
  104. package/src/watchdog/daemon.test.ts +10 -10
  105. package/src/watchdog/daemon.ts +1 -1
  106. package/src/watchdog/health.test.ts +1 -1
  107. package/src/worktree/manager.test.ts +20 -20
  108. package/src/worktree/manager.ts +120 -4
  109. package/src/worktree/tmux.test.ts +98 -9
  110. package/src/worktree/tmux.ts +18 -0
@@ -22,7 +22,7 @@ import { costsCommand } from "./costs.ts";
22
22
  function makeMetrics(overrides: Partial<SessionMetrics> = {}): SessionMetrics {
23
23
  return {
24
24
  agentName: "builder-1",
25
- beadId: "task-001",
25
+ taskId: "task-001",
26
26
  capability: "builder",
27
27
  startedAt: new Date().toISOString(),
28
28
  completedAt: new Date().toISOString(),
@@ -86,7 +86,7 @@ describe("costsCommand", () => {
86
86
  await costsCommand(["--help"]);
87
87
  const out = output();
88
88
 
89
- expect(out).toContain("overstory costs");
89
+ expect(out).toContain("costs");
90
90
  expect(out).toContain("--agent");
91
91
  expect(out).toContain("--run");
92
92
  expect(out).toContain("--by-capability");
@@ -98,7 +98,7 @@ describe("costsCommand", () => {
98
98
  await costsCommand(["-h"]);
99
99
  const out = output();
100
100
 
101
- expect(out).toContain("overstory costs");
101
+ expect(out).toContain("costs");
102
102
  });
103
103
  });
104
104
 
@@ -142,8 +142,8 @@ describe("costsCommand", () => {
142
142
  test("outputs valid JSON array with sessions", async () => {
143
143
  const dbPath = join(tempDir, ".overstory", "metrics.db");
144
144
  const store = createMetricsStore(dbPath);
145
- store.recordSession(makeMetrics({ agentName: "builder-1", beadId: "t1" }));
146
- store.recordSession(makeMetrics({ agentName: "scout-1", beadId: "t2", capability: "scout" }));
145
+ store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "t1" }));
146
+ store.recordSession(makeMetrics({ agentName: "scout-1", taskId: "t2", capability: "scout" }));
147
147
  store.close();
148
148
 
149
149
  await costsCommand(["--json"]);
@@ -160,7 +160,7 @@ describe("costsCommand", () => {
160
160
  store.recordSession(
161
161
  makeMetrics({
162
162
  agentName: "builder-1",
163
- beadId: "t1",
163
+ taskId: "t1",
164
164
  inputTokens: 100,
165
165
  outputTokens: 50,
166
166
  cacheReadTokens: 30,
@@ -187,7 +187,7 @@ describe("costsCommand", () => {
187
187
  test("JSON output returns empty array when no sessions match", async () => {
188
188
  const dbPath = join(tempDir, ".overstory", "metrics.db");
189
189
  const store = createMetricsStore(dbPath);
190
- store.recordSession(makeMetrics({ agentName: "builder-1", beadId: "t1" }));
190
+ store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "t1" }));
191
191
  store.close();
192
192
 
193
193
  await costsCommand(["--json", "--agent", "nonexistent"]);
@@ -203,7 +203,7 @@ describe("costsCommand", () => {
203
203
  store.recordSession(
204
204
  makeMetrics({
205
205
  agentName: "builder-1",
206
- beadId: "t1",
206
+ taskId: "t1",
207
207
  capability: "builder",
208
208
  inputTokens: 100,
209
209
  }),
@@ -211,7 +211,7 @@ describe("costsCommand", () => {
211
211
  store.recordSession(
212
212
  makeMetrics({
213
213
  agentName: "scout-1",
214
- beadId: "t2",
214
+ taskId: "t2",
215
215
  capability: "scout",
216
216
  inputTokens: 50,
217
217
  }),
@@ -238,7 +238,7 @@ describe("costsCommand", () => {
238
238
  test("shows Cost Summary header", async () => {
239
239
  const dbPath = join(tempDir, ".overstory", "metrics.db");
240
240
  const store = createMetricsStore(dbPath);
241
- store.recordSession(makeMetrics({ agentName: "builder-1", beadId: "t1" }));
241
+ store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "t1" }));
242
242
  store.close();
243
243
 
244
244
  await costsCommand([]);
@@ -250,7 +250,7 @@ describe("costsCommand", () => {
250
250
  test("shows column headers", async () => {
251
251
  const dbPath = join(tempDir, ".overstory", "metrics.db");
252
252
  const store = createMetricsStore(dbPath);
253
- store.recordSession(makeMetrics({ agentName: "builder-1", beadId: "t1" }));
253
+ store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "t1" }));
254
254
  store.close();
255
255
 
256
256
  await costsCommand([]);
@@ -270,7 +270,7 @@ describe("costsCommand", () => {
270
270
  store.recordSession(
271
271
  makeMetrics({
272
272
  agentName: "builder-1",
273
- beadId: "t1",
273
+ taskId: "t1",
274
274
  capability: "builder",
275
275
  }),
276
276
  );
@@ -286,7 +286,7 @@ describe("costsCommand", () => {
286
286
  test("shows separator line", async () => {
287
287
  const dbPath = join(tempDir, ".overstory", "metrics.db");
288
288
  const store = createMetricsStore(dbPath);
289
- store.recordSession(makeMetrics({ agentName: "builder-1", beadId: "t1" }));
289
+ store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "t1" }));
290
290
  store.close();
291
291
 
292
292
  await costsCommand([]);
@@ -298,7 +298,7 @@ describe("costsCommand", () => {
298
298
  test("shows Total row", async () => {
299
299
  const dbPath = join(tempDir, ".overstory", "metrics.db");
300
300
  const store = createMetricsStore(dbPath);
301
- store.recordSession(makeMetrics({ agentName: "builder-1", beadId: "t1" }));
301
+ store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "t1" }));
302
302
  store.close();
303
303
 
304
304
  await costsCommand([]);
@@ -329,7 +329,7 @@ describe("costsCommand", () => {
329
329
  store.recordSession(
330
330
  makeMetrics({
331
331
  agentName: "builder-1",
332
- beadId: "t1",
332
+ taskId: "t1",
333
333
  inputTokens: 12345,
334
334
  outputTokens: 5678,
335
335
  }),
@@ -349,7 +349,7 @@ describe("costsCommand", () => {
349
349
  store.recordSession(
350
350
  makeMetrics({
351
351
  agentName: "builder-1",
352
- beadId: "t1",
352
+ taskId: "t1",
353
353
  estimatedCostUsd: 0.42,
354
354
  }),
355
355
  );
@@ -367,7 +367,7 @@ describe("costsCommand", () => {
367
367
  store.recordSession(
368
368
  makeMetrics({
369
369
  agentName: "builder-1",
370
- beadId: "t1",
370
+ taskId: "t1",
371
371
  inputTokens: 0,
372
372
  outputTokens: 0,
373
373
  cacheReadTokens: 0,
@@ -389,7 +389,7 @@ describe("costsCommand", () => {
389
389
  store.recordSession(
390
390
  makeMetrics({
391
391
  agentName: "builder-1",
392
- beadId: "t1",
392
+ taskId: "t1",
393
393
  estimatedCostUsd: null,
394
394
  }),
395
395
  );
@@ -408,8 +408,8 @@ describe("costsCommand", () => {
408
408
  test("filters sessions by agent name", async () => {
409
409
  const dbPath = join(tempDir, ".overstory", "metrics.db");
410
410
  const store = createMetricsStore(dbPath);
411
- store.recordSession(makeMetrics({ agentName: "builder-1", beadId: "t1", inputTokens: 100 }));
412
- store.recordSession(makeMetrics({ agentName: "scout-1", beadId: "t2", inputTokens: 200 }));
411
+ store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "t1", inputTokens: 100 }));
412
+ store.recordSession(makeMetrics({ agentName: "scout-1", taskId: "t2", inputTokens: 200 }));
413
413
  store.close();
414
414
 
415
415
  await costsCommand(["--json", "--agent", "builder-1"]);
@@ -423,7 +423,7 @@ describe("costsCommand", () => {
423
423
  test("returns empty for non-existent agent", async () => {
424
424
  const dbPath = join(tempDir, ".overstory", "metrics.db");
425
425
  const store = createMetricsStore(dbPath);
426
- store.recordSession(makeMetrics({ agentName: "builder-1", beadId: "t1" }));
426
+ store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "t1" }));
427
427
  store.close();
428
428
 
429
429
  await costsCommand(["--json", "--agent", "nonexistent"]);
@@ -441,12 +441,12 @@ describe("costsCommand", () => {
441
441
  const dbPath = join(tempDir, ".overstory", "metrics.db");
442
442
  const store = createMetricsStore(dbPath);
443
443
  store.recordSession(
444
- makeMetrics({ agentName: "builder-1", beadId: "task-001", runId: "run-2026-01-01" }),
444
+ makeMetrics({ agentName: "builder-1", taskId: "task-001", runId: "run-2026-01-01" }),
445
445
  );
446
446
  store.recordSession(
447
447
  makeMetrics({
448
448
  agentName: "scout-1",
449
- beadId: "task-002",
449
+ taskId: "task-002",
450
450
  capability: "scout",
451
451
  runId: "run-other",
452
452
  }),
@@ -465,7 +465,7 @@ describe("costsCommand", () => {
465
465
  const dbPath = join(tempDir, ".overstory", "metrics.db");
466
466
  const store = createMetricsStore(dbPath);
467
467
  store.recordSession(
468
- makeMetrics({ agentName: "builder-1", beadId: "t1", runId: "run-2026-01-01" }),
468
+ makeMetrics({ agentName: "builder-1", taskId: "t1", runId: "run-2026-01-01" }),
469
469
  );
470
470
  store.close();
471
471
 
@@ -484,7 +484,7 @@ describe("costsCommand", () => {
484
484
  const dbPath = join(tempDir, ".overstory", "metrics.db");
485
485
  const store = createMetricsStore(dbPath);
486
486
  store.recordSession(
487
- makeMetrics({ agentName: "builder-1", beadId: "t1", capability: "builder" }),
487
+ makeMetrics({ agentName: "builder-1", taskId: "t1", capability: "builder" }),
488
488
  );
489
489
  store.close();
490
490
 
@@ -498,7 +498,7 @@ describe("costsCommand", () => {
498
498
  const dbPath = join(tempDir, ".overstory", "metrics.db");
499
499
  const store = createMetricsStore(dbPath);
500
500
  store.recordSession(
501
- makeMetrics({ agentName: "builder-1", beadId: "t1", capability: "builder" }),
501
+ makeMetrics({ agentName: "builder-1", taskId: "t1", capability: "builder" }),
502
502
  );
503
503
  store.close();
504
504
 
@@ -514,7 +514,7 @@ describe("costsCommand", () => {
514
514
  store.recordSession(
515
515
  makeMetrics({
516
516
  agentName: "builder-1",
517
- beadId: "t1",
517
+ taskId: "t1",
518
518
  capability: "builder",
519
519
  inputTokens: 1000,
520
520
  }),
@@ -522,7 +522,7 @@ describe("costsCommand", () => {
522
522
  store.recordSession(
523
523
  makeMetrics({
524
524
  agentName: "builder-2",
525
- beadId: "t2",
525
+ taskId: "t2",
526
526
  capability: "builder",
527
527
  inputTokens: 2000,
528
528
  }),
@@ -530,7 +530,7 @@ describe("costsCommand", () => {
530
530
  store.recordSession(
531
531
  makeMetrics({
532
532
  agentName: "scout-1",
533
- beadId: "t3",
533
+ taskId: "t3",
534
534
  capability: "scout",
535
535
  inputTokens: 500,
536
536
  }),
@@ -548,10 +548,10 @@ describe("costsCommand", () => {
548
548
  test("shows correct session count per capability", async () => {
549
549
  const dbPath = join(tempDir, ".overstory", "metrics.db");
550
550
  const store = createMetricsStore(dbPath);
551
- store.recordSession(makeMetrics({ agentName: "b1", beadId: "t1", capability: "builder" }));
552
- store.recordSession(makeMetrics({ agentName: "b2", beadId: "t2", capability: "builder" }));
553
- store.recordSession(makeMetrics({ agentName: "b3", beadId: "t3", capability: "builder" }));
554
- store.recordSession(makeMetrics({ agentName: "s1", beadId: "t4", capability: "scout" }));
551
+ store.recordSession(makeMetrics({ agentName: "b1", taskId: "t1", capability: "builder" }));
552
+ store.recordSession(makeMetrics({ agentName: "b2", taskId: "t2", capability: "builder" }));
553
+ store.recordSession(makeMetrics({ agentName: "b3", taskId: "t3", capability: "builder" }));
554
+ store.recordSession(makeMetrics({ agentName: "s1", taskId: "t4", capability: "scout" }));
555
555
  store.close();
556
556
 
557
557
  await costsCommand(["--json", "--by-capability"]);
@@ -584,7 +584,7 @@ describe("costsCommand", () => {
584
584
  const dbPath = join(tempDir, ".overstory", "metrics.db");
585
585
  const store = createMetricsStore(dbPath);
586
586
  for (let i = 0; i < 10; i++) {
587
- store.recordSession(makeMetrics({ agentName: `agent-${i}`, beadId: `t-${i}` }));
587
+ store.recordSession(makeMetrics({ agentName: `agent-${i}`, taskId: `t-${i}` }));
588
588
  }
589
589
  store.close();
590
590
 
@@ -599,7 +599,7 @@ describe("costsCommand", () => {
599
599
  const dbPath = join(tempDir, ".overstory", "metrics.db");
600
600
  const store = createMetricsStore(dbPath);
601
601
  for (let i = 0; i < 25; i++) {
602
- store.recordSession(makeMetrics({ agentName: `agent-${i}`, beadId: `t-${i}` }));
602
+ store.recordSession(makeMetrics({ agentName: `agent-${i}`, taskId: `t-${i}` }));
603
603
  }
604
604
  store.close();
605
605
 
@@ -620,7 +620,7 @@ describe("costsCommand", () => {
620
620
  store.recordSession(
621
621
  makeMetrics({
622
622
  agentName: "builder-1",
623
- beadId: "t1",
623
+ taskId: "t1",
624
624
  inputTokens: 0,
625
625
  outputTokens: 0,
626
626
  cacheReadTokens: 0,
@@ -644,7 +644,7 @@ describe("costsCommand", () => {
644
644
  store.recordSession(
645
645
  makeMetrics({
646
646
  agentName: "builder-1",
647
- beadId: "t1",
647
+ taskId: "t1",
648
648
  estimatedCostUsd: null,
649
649
  }),
650
650
  );
@@ -664,7 +664,7 @@ describe("costsCommand", () => {
664
664
  store.recordSession(
665
665
  makeMetrics({
666
666
  agentName: "builder-1",
667
- beadId: "t1",
667
+ taskId: "t1",
668
668
  cacheReadTokens: 8000,
669
669
  cacheCreationTokens: 901,
670
670
  }),
@@ -684,7 +684,7 @@ describe("costsCommand", () => {
684
684
  store.recordSession(
685
685
  makeMetrics({
686
686
  agentName: "builder-1",
687
- beadId: "t1",
687
+ taskId: "t1",
688
688
  inputTokens: 100,
689
689
  outputTokens: 50,
690
690
  estimatedCostUsd: 0.1,
@@ -693,7 +693,7 @@ describe("costsCommand", () => {
693
693
  store.recordSession(
694
694
  makeMetrics({
695
695
  agentName: "scout-1",
696
- beadId: "t2",
696
+ taskId: "t2",
697
697
  capability: "scout",
698
698
  inputTokens: 200,
699
699
  outputTokens: 100,
@@ -716,10 +716,10 @@ describe("costsCommand", () => {
716
716
  const dbPath = join(tempDir, ".overstory", "metrics.db");
717
717
  const store = createMetricsStore(dbPath);
718
718
  store.recordSession(
719
- makeMetrics({ agentName: "builder-1", beadId: "t1", capability: "builder" }),
719
+ makeMetrics({ agentName: "builder-1", taskId: "t1", capability: "builder" }),
720
720
  );
721
721
  store.recordSession(
722
- makeMetrics({ agentName: "builder-2", beadId: "t2", capability: "builder" }),
722
+ makeMetrics({ agentName: "builder-2", taskId: "t2", capability: "builder" }),
723
723
  );
724
724
  store.close();
725
725
 
@@ -759,7 +759,7 @@ describe("costsCommand", () => {
759
759
  capability: "builder",
760
760
  worktreePath: "/tmp/wt1",
761
761
  branchName: "feat/task1",
762
- beadId: "task-001",
762
+ taskId: "task-001",
763
763
  tmuxSession: "tmux-001",
764
764
  state: "working",
765
765
  pid: 12345,
@@ -815,7 +815,7 @@ describe("costsCommand", () => {
815
815
  capability: "builder",
816
816
  worktreePath: "/tmp/wt1",
817
817
  branchName: "feat/task1",
818
- beadId: "task-001",
818
+ taskId: "task-001",
819
819
  tmuxSession: "tmux-001",
820
820
  state: "working",
821
821
  pid: 12345,
@@ -879,7 +879,7 @@ describe("costsCommand", () => {
879
879
  capability: "builder",
880
880
  worktreePath: "/tmp/wt1",
881
881
  branchName: "feat/task1",
882
- beadId: "task-001",
882
+ taskId: "task-001",
883
883
  tmuxSession: "tmux-001",
884
884
  state: "working",
885
885
  pid: 12345,
@@ -897,7 +897,7 @@ describe("costsCommand", () => {
897
897
  capability: "scout",
898
898
  worktreePath: "/tmp/wt2",
899
899
  branchName: "feat/task2",
900
- beadId: "task-002",
900
+ taskId: "task-002",
901
901
  tmuxSession: "tmux-002",
902
902
  state: "working",
903
903
  pid: 12346,
@@ -956,7 +956,7 @@ describe("costsCommand", () => {
956
956
  capability: "builder",
957
957
  worktreePath: "/tmp/wt1",
958
958
  branchName: "feat/task1",
959
- beadId: "task-001",
959
+ taskId: "task-001",
960
960
  tmuxSession: "tmux-001",
961
961
  state: "working",
962
962
  pid: 12345,
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { readdir, stat } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
+ import { Command } from "commander";
11
12
  import { loadConfig } from "../config.ts";
12
13
  import { ValidationError } from "../errors.ts";
13
14
  import { color } from "../logging/color.ts";
@@ -16,21 +17,6 @@ import { estimateCost, parseTranscriptUsage } from "../metrics/transcript.ts";
16
17
  import { openSessionStore } from "../sessions/compat.ts";
17
18
  import type { SessionMetrics } from "../types.ts";
18
19
 
19
- /**
20
- * Parse a named flag value from args.
21
- */
22
- function getFlag(args: string[], flag: string): string | undefined {
23
- const idx = args.indexOf(flag);
24
- if (idx === -1 || idx + 1 >= args.length) {
25
- return undefined;
26
- }
27
- return args[idx + 1];
28
- }
29
-
30
- function hasFlag(args: string[], flag: string): boolean {
31
- return args.includes(flag);
32
- }
33
-
34
20
  /** Format a number with thousands separators (e.g., 12345 -> "12,345"). */
35
21
  function formatNumber(n: number): string {
36
22
  return n.toLocaleString("en-US");
@@ -156,11 +142,11 @@ function printCostSummary(sessions: SessionMetrics[]): void {
156
142
  const w = process.stdout.write.bind(process.stdout);
157
143
  const separator = "\u2500".repeat(70);
158
144
 
159
- w(`${color.bold}Cost Summary${color.reset}\n`);
145
+ w(`${color.bold("Cost Summary")}\n`);
160
146
  w(`${"=".repeat(70)}\n`);
161
147
 
162
148
  if (sessions.length === 0) {
163
- w(`${color.dim}No session data found.${color.reset}\n`);
149
+ w(`${color.dim("No session data found.")}\n`);
164
150
  return;
165
151
  }
166
152
 
@@ -169,7 +155,7 @@ function printCostSummary(sessions: SessionMetrics[]): void {
169
155
  `${padLeft("Input", 10)}${padLeft("Output", 10)}` +
170
156
  `${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
171
157
  );
172
- w(`${color.dim}${separator}${color.reset}\n`);
158
+ w(`${color.dim(separator)}\n`);
173
159
 
174
160
  for (const s of sessions) {
175
161
  const cacheTotal = s.cacheReadTokens + s.cacheCreationTokens;
@@ -183,13 +169,17 @@ function printCostSummary(sessions: SessionMetrics[]): void {
183
169
  }
184
170
 
185
171
  const totals = computeTotals(sessions);
186
- w(`${color.dim}${separator}${color.reset}\n`);
172
+ w(`${color.dim(separator)}\n`);
187
173
  w(
188
- `${color.green}${color.bold}${padRight("Total", 31)}` +
189
- `${padLeft(formatNumber(totals.inputTokens), 10)}` +
190
- `${padLeft(formatNumber(totals.outputTokens), 10)}` +
191
- `${padLeft(formatNumber(totals.cacheTokens), 10)}` +
192
- `${padLeft(formatCost(totals.costUsd), 10)}${color.reset}\n`,
174
+ `${color.green(
175
+ color.bold(
176
+ padRight("Total", 31) +
177
+ padLeft(formatNumber(totals.inputTokens), 10) +
178
+ padLeft(formatNumber(totals.outputTokens), 10) +
179
+ padLeft(formatNumber(totals.cacheTokens), 10) +
180
+ padLeft(formatCost(totals.costUsd), 10),
181
+ ),
182
+ )}\n`,
193
183
  );
194
184
  }
195
185
 
@@ -198,11 +188,11 @@ function printByCapability(sessions: SessionMetrics[]): void {
198
188
  const w = process.stdout.write.bind(process.stdout);
199
189
  const separator = "\u2500".repeat(70);
200
190
 
201
- w(`${color.bold}Cost by Capability${color.reset}\n`);
191
+ w(`${color.bold("Cost by Capability")}\n`);
202
192
  w(`${"=".repeat(70)}\n`);
203
193
 
204
194
  if (sessions.length === 0) {
205
- w(`${color.dim}No session data found.${color.reset}\n`);
195
+ w(`${color.dim("No session data found.")}\n`);
206
196
  return;
207
197
  }
208
198
 
@@ -211,7 +201,7 @@ function printByCapability(sessions: SessionMetrics[]): void {
211
201
  `${padLeft("Input", 10)}${padLeft("Output", 10)}` +
212
202
  `${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
213
203
  );
214
- w(`${color.dim}${separator}${color.reset}\n`);
204
+ w(`${color.dim(separator)}\n`);
215
205
 
216
206
  const groups = groupByCapability(sessions);
217
207
 
@@ -227,47 +217,39 @@ function printByCapability(sessions: SessionMetrics[]): void {
227
217
  }
228
218
 
229
219
  const totals = computeTotals(sessions);
230
- w(`${color.dim}${separator}${color.reset}\n`);
220
+ w(`${color.dim(separator)}\n`);
231
221
  w(
232
- `${color.green}${color.bold}${padRight("Total", 14)}` +
233
- `${padLeft(formatNumber(sessions.length), 10)}` +
234
- `${padLeft(formatNumber(totals.inputTokens), 10)}` +
235
- `${padLeft(formatNumber(totals.outputTokens), 10)}` +
236
- `${padLeft(formatNumber(totals.cacheTokens), 10)}` +
237
- `${padLeft(formatCost(totals.costUsd), 10)}${color.reset}\n`,
222
+ `${color.green(
223
+ color.bold(
224
+ padRight("Total", 14) +
225
+ padLeft(formatNumber(sessions.length), 10) +
226
+ padLeft(formatNumber(totals.inputTokens), 10) +
227
+ padLeft(formatNumber(totals.outputTokens), 10) +
228
+ padLeft(formatNumber(totals.cacheTokens), 10) +
229
+ padLeft(formatCost(totals.costUsd), 10),
230
+ ),
231
+ )}\n`,
238
232
  );
239
233
  }
240
234
 
241
- const COSTS_HELP = `overstory costs -- Token/cost analysis and breakdown
242
-
243
- Usage: overstory costs [options]
244
-
245
- Options:
246
- --live Show real-time token usage for active agents
247
- --self Show cost for the current orchestrator session
248
- --agent <name> Filter by agent name
249
- --run <id> Filter by run ID
250
- --by-capability Group results by capability with subtotals
251
- --last <n> Number of recent sessions (default: 20)
252
- --json Output as JSON
253
- --help, -h Show this help`;
254
-
255
- /**
256
- * Entry point for `overstory costs [--agent <name>] [--run <id>] [--by-capability] [--last <n>] [--self] [--json]`.
257
- */
258
- export async function costsCommand(args: string[]): Promise<void> {
259
- if (args.includes("--help") || args.includes("-h")) {
260
- process.stdout.write(`${COSTS_HELP}\n`);
261
- return;
262
- }
235
+ interface CostsOpts {
236
+ live?: boolean;
237
+ self?: boolean;
238
+ byCapability?: boolean;
239
+ agent?: string;
240
+ run?: string;
241
+ last?: string;
242
+ json?: boolean;
243
+ }
263
244
 
264
- const json = hasFlag(args, "--json");
265
- const live = hasFlag(args, "--live");
266
- const self = hasFlag(args, "--self");
267
- const byCapability = hasFlag(args, "--by-capability");
268
- const agentName = getFlag(args, "--agent");
269
- const runId = getFlag(args, "--run");
270
- const lastStr = getFlag(args, "--last");
245
+ async function executeCosts(opts: CostsOpts): Promise<void> {
246
+ const json = opts.json ?? false;
247
+ const live = opts.live ?? false;
248
+ const self = opts.self ?? false;
249
+ const byCapability = opts.byCapability ?? false;
250
+ const agentName = opts.agent;
251
+ const runId = opts.run;
252
+ const lastStr = opts.last;
271
253
 
272
254
  if (lastStr !== undefined) {
273
255
  const parsed = Number.parseInt(lastStr, 10);
@@ -323,17 +305,17 @@ export async function costsCommand(args: string[]): Promise<void> {
323
305
  const w = process.stdout.write.bind(process.stdout);
324
306
  const separator = "\u2500".repeat(70);
325
307
 
326
- w(`${color.bold}Orchestrator Session Cost${color.reset}\n`);
308
+ w(`${color.bold("Orchestrator Session Cost")}\n`);
327
309
  w(`${"=".repeat(70)}\n`);
328
310
  w(`${padRight("Model:", 12)}${usage.modelUsed ?? "unknown"}\n`);
329
311
  w(`${padRight("Transcript:", 12)}${transcriptPath}\n`);
330
- w(`${color.dim}${separator}${color.reset}\n`);
312
+ w(`${color.dim(separator)}\n`);
331
313
  w(`${padRight("Input tokens:", 22)}${padLeft(formatNumber(usage.inputTokens), 12)}\n`);
332
314
  w(`${padRight("Output tokens:", 22)}${padLeft(formatNumber(usage.outputTokens), 12)}\n`);
333
315
  w(`${padRight("Cache tokens:", 22)}${padLeft(formatNumber(cacheTotal), 12)}\n`);
334
- w(`${color.dim}${separator}${color.reset}\n`);
316
+ w(`${color.dim(separator)}\n`);
335
317
  w(
336
- `${color.green}${color.bold}${padRight("Estimated cost:", 22)}${padLeft(formatCost(cost), 12)}${color.reset}\n`,
318
+ `${color.green(color.bold(padRight("Estimated cost:", 22) + padLeft(formatCost(cost), 12)))}\n`,
337
319
  );
338
320
  }
339
321
  return;
@@ -463,14 +445,14 @@ export async function costsCommand(args: string[]): Promise<void> {
463
445
  const w = process.stdout.write.bind(process.stdout);
464
446
  const separator = "\u2500".repeat(70);
465
447
 
466
- w(`${color.bold}Live Token Usage (${agentData.length} active agents)${color.reset}\n`);
448
+ w(`${color.bold(`Live Token Usage (${agentData.length} active agents)`)}\n`);
467
449
  w(`${"=".repeat(70)}\n`);
468
450
  w(
469
451
  `${padRight("Agent", 19)}${padRight("Capability", 12)}` +
470
452
  `${padLeft("Input", 10)}${padLeft("Output", 10)}` +
471
453
  `${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
472
454
  );
473
- w(`${color.dim}${separator}${color.reset}\n`);
455
+ w(`${color.dim(separator)}\n`);
474
456
 
475
457
  for (const agent of agentData) {
476
458
  const cacheTotal = agent.cacheReadTokens + agent.cacheCreationTokens;
@@ -483,13 +465,17 @@ export async function costsCommand(args: string[]): Promise<void> {
483
465
  );
484
466
  }
485
467
 
486
- w(`${color.dim}${separator}${color.reset}\n`);
468
+ w(`${color.dim(separator)}\n`);
487
469
  w(
488
- `${color.green}${color.bold}${padRight("Total", 31)}` +
489
- `${padLeft(formatNumber(totalInput), 10)}` +
490
- `${padLeft(formatNumber(totalOutput), 10)}` +
491
- `${padLeft(formatNumber(totalCacheTokens), 10)}` +
492
- `${padLeft(formatCost(totalCost), 10)}${color.reset}\n\n`,
470
+ `${color.green(
471
+ color.bold(
472
+ padRight("Total", 31) +
473
+ padLeft(formatNumber(totalInput), 10) +
474
+ padLeft(formatNumber(totalOutput), 10) +
475
+ padLeft(formatNumber(totalCacheTokens), 10) +
476
+ padLeft(formatCost(totalCost), 10),
477
+ ),
478
+ )}\n\n`,
493
479
  );
494
480
 
495
481
  // Format elapsed time
@@ -562,3 +548,38 @@ export async function costsCommand(args: string[]): Promise<void> {
562
548
  metricsStore.close();
563
549
  }
564
550
  }
551
+
552
+ export function createCostsCommand(): Command {
553
+ return new Command("costs")
554
+ .description("Token/cost analysis and breakdown")
555
+ .option("--live", "Show real-time token usage for active agents")
556
+ .option("--self", "Show cost for the current orchestrator session")
557
+ .option("--agent <name>", "Filter by agent name")
558
+ .option("--run <id>", "Filter by run ID")
559
+ .option("--by-capability", "Group results by capability with subtotals")
560
+ .option("--last <n>", "Number of recent sessions (default: 20)")
561
+ .option("--json", "Output as JSON")
562
+ .action(async (opts: CostsOpts) => {
563
+ await executeCosts(opts);
564
+ });
565
+ }
566
+
567
+ export async function costsCommand(args: string[]): Promise<void> {
568
+ const cmd = createCostsCommand();
569
+ cmd.exitOverride();
570
+ try {
571
+ await cmd.parseAsync(args, { from: "user" });
572
+ } catch (err: unknown) {
573
+ if (err && typeof err === "object" && "code" in err) {
574
+ const code = (err as { code: string }).code;
575
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
576
+ return;
577
+ }
578
+ if (code.startsWith("commander.")) {
579
+ const message = err instanceof Error ? err.message : String(err);
580
+ throw new ValidationError(message, { field: "args" });
581
+ }
582
+ }
583
+ throw err;
584
+ }
585
+ }
@@ -51,7 +51,7 @@ describe("dashboardCommand", () => {
51
51
  await dashboardCommand(["--help"]);
52
52
  const out = output();
53
53
 
54
- expect(out).toContain("overstory dashboard");
54
+ expect(out).toContain("dashboard");
55
55
  expect(out).toContain("--interval");
56
56
  expect(out).toContain("Ctrl+C");
57
57
  });
@@ -60,7 +60,7 @@ describe("dashboardCommand", () => {
60
60
  await dashboardCommand(["-h"]);
61
61
  const out = output();
62
62
 
63
- expect(out).toContain("overstory dashboard");
63
+ expect(out).toContain("dashboard");
64
64
  expect(out).toContain("--interval");
65
65
  expect(out).toContain("Ctrl+C");
66
66
  });