@kata-sh/cli 0.1.0 → 0.1.2

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 (199) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/app-paths.d.ts +4 -0
  4. package/dist/app-paths.js +6 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +56 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +95 -0
  9. package/dist/resource-loader.d.ts +18 -0
  10. package/dist/resource-loader.js +50 -0
  11. package/dist/wizard.d.ts +15 -0
  12. package/dist/wizard.js +159 -0
  13. package/package.json +50 -21
  14. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  15. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  16. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  17. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  18. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  19. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  20. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  21. package/pkg/package.json +8 -0
  22. package/scripts/postinstall.js +45 -0
  23. package/src/resources/AGENTS.md +108 -0
  24. package/src/resources/KATA-WORKFLOW.md +661 -0
  25. package/src/resources/agents/researcher.md +29 -0
  26. package/src/resources/agents/scout.md +56 -0
  27. package/src/resources/agents/worker.md +31 -0
  28. package/src/resources/extensions/ask-user-questions.ts +200 -0
  29. package/src/resources/extensions/bg-shell/index.ts +2758 -0
  30. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  31. package/src/resources/extensions/browser-tools/core.js +1057 -0
  32. package/src/resources/extensions/browser-tools/index.ts +4916 -0
  33. package/src/resources/extensions/browser-tools/package.json +20 -0
  34. package/src/resources/extensions/context7/index.ts +428 -0
  35. package/src/resources/extensions/context7/package.json +11 -0
  36. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  37. package/src/resources/extensions/github/formatters.ts +207 -0
  38. package/src/resources/extensions/github/gh-api.ts +537 -0
  39. package/src/resources/extensions/github/index.ts +778 -0
  40. package/src/resources/extensions/kata/activity-log.ts +88 -0
  41. package/src/resources/extensions/kata/auto.ts +2786 -0
  42. package/src/resources/extensions/kata/commands.ts +355 -0
  43. package/src/resources/extensions/kata/crash-recovery.ts +85 -0
  44. package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
  45. package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
  46. package/src/resources/extensions/kata/doctor.ts +683 -0
  47. package/src/resources/extensions/kata/files.ts +730 -0
  48. package/src/resources/extensions/kata/gitignore.ts +165 -0
  49. package/src/resources/extensions/kata/guided-flow.ts +976 -0
  50. package/src/resources/extensions/kata/index.ts +556 -0
  51. package/src/resources/extensions/kata/metrics.ts +397 -0
  52. package/src/resources/extensions/kata/observability-validator.ts +408 -0
  53. package/src/resources/extensions/kata/package.json +11 -0
  54. package/src/resources/extensions/kata/paths.ts +346 -0
  55. package/src/resources/extensions/kata/preferences.ts +695 -0
  56. package/src/resources/extensions/kata/prompt-loader.ts +50 -0
  57. package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
  58. package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
  59. package/src/resources/extensions/kata/prompts/discuss.md +151 -0
  60. package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
  61. package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
  62. package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
  63. package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
  64. package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
  65. package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
  66. package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
  67. package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
  68. package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
  69. package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
  70. package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
  71. package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
  72. package/src/resources/extensions/kata/prompts/queue.md +85 -0
  73. package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
  74. package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
  75. package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
  76. package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
  77. package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
  78. package/src/resources/extensions/kata/prompts/system.md +341 -0
  79. package/src/resources/extensions/kata/session-forensics.ts +550 -0
  80. package/src/resources/extensions/kata/skill-discovery.ts +137 -0
  81. package/src/resources/extensions/kata/state.ts +509 -0
  82. package/src/resources/extensions/kata/templates/context.md +76 -0
  83. package/src/resources/extensions/kata/templates/decisions.md +8 -0
  84. package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
  85. package/src/resources/extensions/kata/templates/plan.md +133 -0
  86. package/src/resources/extensions/kata/templates/preferences.md +15 -0
  87. package/src/resources/extensions/kata/templates/project.md +31 -0
  88. package/src/resources/extensions/kata/templates/reassessment.md +28 -0
  89. package/src/resources/extensions/kata/templates/requirements.md +81 -0
  90. package/src/resources/extensions/kata/templates/research.md +46 -0
  91. package/src/resources/extensions/kata/templates/roadmap.md +118 -0
  92. package/src/resources/extensions/kata/templates/slice-context.md +58 -0
  93. package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
  94. package/src/resources/extensions/kata/templates/state.md +19 -0
  95. package/src/resources/extensions/kata/templates/task-plan.md +52 -0
  96. package/src/resources/extensions/kata/templates/task-summary.md +57 -0
  97. package/src/resources/extensions/kata/templates/uat.md +54 -0
  98. package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
  99. package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
  100. package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
  101. package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
  102. package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
  103. package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
  104. package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
  105. package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
  106. package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
  107. package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
  108. package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
  109. package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
  110. package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
  111. package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
  112. package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
  113. package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
  114. package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
  115. package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
  116. package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
  117. package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
  118. package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
  119. package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
  120. package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
  121. package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
  122. package/src/resources/extensions/kata/types.ts +159 -0
  123. package/src/resources/extensions/kata/unit-runtime.ts +163 -0
  124. package/src/resources/extensions/kata/workspace-index.ts +203 -0
  125. package/src/resources/extensions/kata/worktree.ts +182 -0
  126. package/src/resources/extensions/mac-tools/index.ts +852 -0
  127. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  128. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  129. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  130. package/src/resources/extensions/search-the-web/format.ts +258 -0
  131. package/src/resources/extensions/search-the-web/http.ts +238 -0
  132. package/src/resources/extensions/search-the-web/index.ts +68 -0
  133. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  134. package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
  135. package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
  136. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  137. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  138. package/src/resources/extensions/shared/interview-ui.ts +822 -0
  139. package/src/resources/extensions/shared/next-action-ui.ts +235 -0
  140. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  141. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  142. package/src/resources/extensions/shared/ui.ts +400 -0
  143. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  144. package/src/resources/extensions/slash-commands/audit.ts +92 -0
  145. package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
  146. package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
  147. package/src/resources/extensions/slash-commands/index.ts +12 -0
  148. package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
  149. package/src/resources/extensions/subagent/agents.ts +126 -0
  150. package/src/resources/extensions/subagent/index.ts +1293 -0
  151. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  152. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  153. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  154. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  155. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  156. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  157. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  158. package/src/resources/skills/swiftui/SKILL.md +208 -0
  159. package/src/resources/skills/swiftui/references/animations.md +921 -0
  160. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  161. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  162. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  163. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  164. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  165. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  166. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  167. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  168. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  169. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  170. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  171. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  172. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  173. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  174. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  175. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
  176. package/dist/commands/task.d.ts +0 -9
  177. package/dist/commands/task.d.ts.map +0 -1
  178. package/dist/commands/task.js +0 -129
  179. package/dist/commands/task.js.map +0 -1
  180. package/dist/commands/task.test.d.ts +0 -2
  181. package/dist/commands/task.test.d.ts.map +0 -1
  182. package/dist/commands/task.test.js +0 -169
  183. package/dist/commands/task.test.js.map +0 -1
  184. package/dist/e2e/task-e2e.test.d.ts +0 -2
  185. package/dist/e2e/task-e2e.test.d.ts.map +0 -1
  186. package/dist/e2e/task-e2e.test.js +0 -173
  187. package/dist/e2e/task-e2e.test.js.map +0 -1
  188. package/dist/index.d.ts +0 -3
  189. package/dist/index.d.ts.map +0 -1
  190. package/dist/index.js +0 -93
  191. package/dist/index.js.map +0 -1
  192. package/dist/slug.d.ts +0 -2
  193. package/dist/slug.d.ts.map +0 -1
  194. package/dist/slug.js +0 -12
  195. package/dist/slug.js.map +0 -1
  196. package/dist/slug.test.d.ts +0 -2
  197. package/dist/slug.test.d.ts.map +0 -1
  198. package/dist/slug.test.js +0 -32
  199. package/dist/slug.test.js.map +0 -1
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Kata Metrics — Token & Cost Tracking
3
+ *
4
+ * Accumulates per-unit usage data across auto-mode sessions.
5
+ * Data is extracted from session entries before each context wipe,
6
+ * written to .kata/metrics.json, and surfaced in the dashboard.
7
+ *
8
+ * Data flow:
9
+ * 1. Before newSession() wipes context, snapshotUnitMetrics() scans
10
+ * session entries for AssistantMessage usage data
11
+ * 2. The unit record is appended to the in-memory ledger and flushed to disk
12
+ * 3. The dashboard overlay and progress widget read from the in-memory ledger
13
+ * 4. On crash recovery or fresh start, the ledger is loaded from disk
14
+ */
15
+
16
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
19
+ import { kataRoot } from "./paths.js";
20
+
21
+ // ─── Types ────────────────────────────────────────────────────────────────────
22
+
23
+ export interface TokenCounts {
24
+ input: number;
25
+ output: number;
26
+ cacheRead: number;
27
+ cacheWrite: number;
28
+ total: number;
29
+ }
30
+
31
+ export interface UnitMetrics {
32
+ type: string; // e.g. "research-milestone", "execute-task"
33
+ id: string; // e.g. "M001/S01/T01"
34
+ model: string; // model ID used
35
+ startedAt: number; // ms timestamp
36
+ finishedAt: number; // ms timestamp
37
+ tokens: TokenCounts;
38
+ cost: number; // total USD cost
39
+ toolCalls: number;
40
+ assistantMessages: number;
41
+ userMessages: number;
42
+ }
43
+
44
+ export interface MetricsLedger {
45
+ version: 1;
46
+ projectStartedAt: number;
47
+ units: UnitMetrics[];
48
+ }
49
+
50
+ // ─── Phase classification ─────────────────────────────────────────────────────
51
+
52
+ export type MetricsPhase =
53
+ | "research"
54
+ | "planning"
55
+ | "execution"
56
+ | "completion"
57
+ | "reassessment";
58
+
59
+ export function classifyUnitPhase(unitType: string): MetricsPhase {
60
+ switch (unitType) {
61
+ case "research-milestone":
62
+ case "research-slice":
63
+ return "research";
64
+ case "plan-milestone":
65
+ case "plan-slice":
66
+ return "planning";
67
+ case "execute-task":
68
+ return "execution";
69
+ case "complete-slice":
70
+ return "completion";
71
+ case "reassess-roadmap":
72
+ return "reassessment";
73
+ default:
74
+ return "execution";
75
+ }
76
+ }
77
+
78
+ // ─── In-memory state ──────────────────────────────────────────────────────────
79
+
80
+ let ledger: MetricsLedger | null = null;
81
+ let basePath: string = "";
82
+
83
+ // ─── Public API ───────────────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Initialize the metrics system for a given project.
87
+ * Loads existing ledger from disk if present.
88
+ */
89
+ export function initMetrics(base: string): void {
90
+ basePath = base;
91
+ ledger = loadLedger(base);
92
+ }
93
+
94
+ /**
95
+ * Reset in-memory state. Called when auto-mode stops.
96
+ */
97
+ export function resetMetrics(): void {
98
+ ledger = null;
99
+ basePath = "";
100
+ }
101
+
102
+ /**
103
+ * Snapshot usage metrics from the current session before it's wiped.
104
+ * Scans session entries for AssistantMessage usage data.
105
+ */
106
+ export function snapshotUnitMetrics(
107
+ ctx: ExtensionContext,
108
+ unitType: string,
109
+ unitId: string,
110
+ startedAt: number,
111
+ model: string,
112
+ ): UnitMetrics | null {
113
+ if (!ledger) return null;
114
+
115
+ const entries = ctx.sessionManager.getEntries();
116
+ if (!entries || entries.length === 0) return null;
117
+
118
+ const tokens: TokenCounts = {
119
+ input: 0,
120
+ output: 0,
121
+ cacheRead: 0,
122
+ cacheWrite: 0,
123
+ total: 0,
124
+ };
125
+ let cost = 0;
126
+ let toolCalls = 0;
127
+ let assistantMessages = 0;
128
+ let userMessages = 0;
129
+
130
+ for (const entry of entries) {
131
+ if (entry.type !== "message") continue;
132
+ const msg = (entry as any).message;
133
+ if (!msg) continue;
134
+
135
+ if (msg.role === "assistant") {
136
+ assistantMessages++;
137
+ if (msg.usage) {
138
+ tokens.input += msg.usage.input ?? 0;
139
+ tokens.output += msg.usage.output ?? 0;
140
+ tokens.cacheRead += msg.usage.cacheRead ?? 0;
141
+ tokens.cacheWrite += msg.usage.cacheWrite ?? 0;
142
+ tokens.total += msg.usage.totalTokens ?? 0;
143
+ if (msg.usage.cost) {
144
+ cost += msg.usage.cost.total ?? 0;
145
+ }
146
+ }
147
+ // Count tool calls in this message
148
+ if (msg.content && Array.isArray(msg.content)) {
149
+ for (const block of msg.content) {
150
+ if (block.type === "tool_call") toolCalls++;
151
+ }
152
+ }
153
+ } else if (msg.role === "user") {
154
+ userMessages++;
155
+ }
156
+ }
157
+
158
+ const unit: UnitMetrics = {
159
+ type: unitType,
160
+ id: unitId,
161
+ model,
162
+ startedAt,
163
+ finishedAt: Date.now(),
164
+ tokens,
165
+ cost,
166
+ toolCalls,
167
+ assistantMessages,
168
+ userMessages,
169
+ };
170
+
171
+ ledger.units.push(unit);
172
+ saveLedger(basePath, ledger);
173
+
174
+ return unit;
175
+ }
176
+
177
+ /**
178
+ * Get the current ledger (read-only).
179
+ */
180
+ export function getLedger(): MetricsLedger | null {
181
+ return ledger;
182
+ }
183
+
184
+ // ─── Aggregation helpers ──────────────────────────────────────────────────────
185
+
186
+ export interface PhaseAggregate {
187
+ phase: MetricsPhase;
188
+ units: number;
189
+ tokens: TokenCounts;
190
+ cost: number;
191
+ duration: number; // ms
192
+ }
193
+
194
+ export interface SliceAggregate {
195
+ sliceId: string;
196
+ units: number;
197
+ tokens: TokenCounts;
198
+ cost: number;
199
+ duration: number;
200
+ }
201
+
202
+ export interface ModelAggregate {
203
+ model: string;
204
+ units: number;
205
+ tokens: TokenCounts;
206
+ cost: number;
207
+ }
208
+
209
+ export interface ProjectTotals {
210
+ units: number;
211
+ tokens: TokenCounts;
212
+ cost: number;
213
+ duration: number;
214
+ toolCalls: number;
215
+ assistantMessages: number;
216
+ userMessages: number;
217
+ }
218
+
219
+ function emptyTokens(): TokenCounts {
220
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
221
+ }
222
+
223
+ function addTokens(a: TokenCounts, b: TokenCounts): TokenCounts {
224
+ return {
225
+ input: a.input + b.input,
226
+ output: a.output + b.output,
227
+ cacheRead: a.cacheRead + b.cacheRead,
228
+ cacheWrite: a.cacheWrite + b.cacheWrite,
229
+ total: a.total + b.total,
230
+ };
231
+ }
232
+
233
+ export function aggregateByPhase(units: UnitMetrics[]): PhaseAggregate[] {
234
+ const map = new Map<MetricsPhase, PhaseAggregate>();
235
+ for (const u of units) {
236
+ const phase = classifyUnitPhase(u.type);
237
+ let agg = map.get(phase);
238
+ if (!agg) {
239
+ agg = { phase, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
240
+ map.set(phase, agg);
241
+ }
242
+ agg.units++;
243
+ agg.tokens = addTokens(agg.tokens, u.tokens);
244
+ agg.cost += u.cost;
245
+ agg.duration += u.finishedAt - u.startedAt;
246
+ }
247
+ // Return in a stable order
248
+ const order: MetricsPhase[] = [
249
+ "research",
250
+ "planning",
251
+ "execution",
252
+ "completion",
253
+ "reassessment",
254
+ ];
255
+ return order.map((p) => map.get(p)).filter((a): a is PhaseAggregate => !!a);
256
+ }
257
+
258
+ export function aggregateBySlice(units: UnitMetrics[]): SliceAggregate[] {
259
+ const map = new Map<string, SliceAggregate>();
260
+ for (const u of units) {
261
+ const parts = u.id.split("/");
262
+ // Slice ID is parts[0]/parts[1] if it exists, else parts[0]
263
+ const sliceId = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
264
+ let agg = map.get(sliceId);
265
+ if (!agg) {
266
+ agg = { sliceId, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
267
+ map.set(sliceId, agg);
268
+ }
269
+ agg.units++;
270
+ agg.tokens = addTokens(agg.tokens, u.tokens);
271
+ agg.cost += u.cost;
272
+ agg.duration += u.finishedAt - u.startedAt;
273
+ }
274
+ return Array.from(map.values()).sort((a, b) =>
275
+ a.sliceId.localeCompare(b.sliceId),
276
+ );
277
+ }
278
+
279
+ export function aggregateByModel(units: UnitMetrics[]): ModelAggregate[] {
280
+ const map = new Map<string, ModelAggregate>();
281
+ for (const u of units) {
282
+ let agg = map.get(u.model);
283
+ if (!agg) {
284
+ agg = { model: u.model, units: 0, tokens: emptyTokens(), cost: 0 };
285
+ map.set(u.model, agg);
286
+ }
287
+ agg.units++;
288
+ agg.tokens = addTokens(agg.tokens, u.tokens);
289
+ agg.cost += u.cost;
290
+ }
291
+ return Array.from(map.values()).sort((a, b) => b.cost - a.cost);
292
+ }
293
+
294
+ export function getProjectTotals(units: UnitMetrics[]): ProjectTotals {
295
+ const totals: ProjectTotals = {
296
+ units: units.length,
297
+ tokens: emptyTokens(),
298
+ cost: 0,
299
+ duration: 0,
300
+ toolCalls: 0,
301
+ assistantMessages: 0,
302
+ userMessages: 0,
303
+ };
304
+ for (const u of units) {
305
+ totals.tokens = addTokens(totals.tokens, u.tokens);
306
+ totals.cost += u.cost;
307
+ totals.duration += u.finishedAt - u.startedAt;
308
+ totals.toolCalls += u.toolCalls;
309
+ totals.assistantMessages += u.assistantMessages;
310
+ totals.userMessages += u.userMessages;
311
+ }
312
+ return totals;
313
+ }
314
+
315
+ // ─── Formatting helpers ───────────────────────────────────────────────────────
316
+
317
+ export function formatCost(cost: number): string {
318
+ if (cost < 0.01) return `$${cost.toFixed(4)}`;
319
+ if (cost < 1) return `$${cost.toFixed(3)}`;
320
+ return `$${cost.toFixed(2)}`;
321
+ }
322
+
323
+ /**
324
+ * Compute a projected remaining cost based on completed slice averages.
325
+ *
326
+ * Filters to slice-level entries (sliceId contains "/") to exclude bare milestone
327
+ * aggregates from the average. Returns [] when fewer than 2 slice-level entries
328
+ * exist (insufficient data for a reliable projection).
329
+ *
330
+ * If `budgetCeiling` is provided and `totalCost >= budgetCeiling`, a warning line
331
+ * is appended to the result.
332
+ */
333
+ export function formatCostProjection(
334
+ completedSlices: SliceAggregate[],
335
+ remainingCount: number,
336
+ budgetCeiling?: number,
337
+ ): string[] {
338
+ const sliceLevel = completedSlices.filter((s) => s.sliceId.includes("/"));
339
+ if (sliceLevel.length < 2) return [];
340
+
341
+ const totalCost = sliceLevel.reduce((sum, s) => sum + s.cost, 0);
342
+ const avgCost = totalCost / sliceLevel.length;
343
+ const projected = avgCost * remainingCount;
344
+
345
+ const projLine = `Projected remaining: ${formatCost(projected)} (${formatCost(avgCost)}/slice avg × ${remainingCount} remaining)`;
346
+ const result: string[] = [projLine];
347
+
348
+ if (budgetCeiling !== undefined && totalCost >= budgetCeiling) {
349
+ result.push(
350
+ `Budget ceiling ${formatCost(budgetCeiling)} reached (spent ${formatCost(totalCost)})`,
351
+ );
352
+ }
353
+
354
+ return result;
355
+ }
356
+
357
+ export function formatTokenCount(count: number): string {
358
+ if (count < 1000) return `${count}`;
359
+ if (count < 1_000_000) return `${(count / 1000).toFixed(1)}k`;
360
+ return `${(count / 1_000_000).toFixed(2)}M`;
361
+ }
362
+
363
+ // ─── Disk I/O ─────────────────────────────────────────────────────────────────
364
+
365
+ function metricsPath(base: string): string {
366
+ return join(kataRoot(base), "metrics.json");
367
+ }
368
+
369
+ function loadLedger(base: string): MetricsLedger {
370
+ try {
371
+ const raw = readFileSync(metricsPath(base), "utf-8");
372
+ const parsed = JSON.parse(raw);
373
+ if (parsed.version === 1 && Array.isArray(parsed.units)) {
374
+ return parsed as MetricsLedger;
375
+ }
376
+ } catch {
377
+ // File doesn't exist or is corrupt — start fresh
378
+ }
379
+ return {
380
+ version: 1,
381
+ projectStartedAt: Date.now(),
382
+ units: [],
383
+ };
384
+ }
385
+
386
+ function saveLedger(base: string, data: MetricsLedger): void {
387
+ try {
388
+ mkdirSync(kataRoot(base), { recursive: true });
389
+ writeFileSync(
390
+ metricsPath(base),
391
+ JSON.stringify(data, null, 2) + "\n",
392
+ "utf-8",
393
+ );
394
+ } catch {
395
+ // Don't let metrics failures break auto-mode
396
+ }
397
+ }