@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -0,0 +1,449 @@
1
+ import { access } from "node:fs/promises"
2
+ import { execFile as execFileCb } from "node:child_process"
3
+ import { promisify } from "node:util"
4
+ import path from "node:path"
5
+
6
+ const execFile = promisify(execFileCb)
7
+
8
+ const MUTATION_TOOLS = new Set(["write", "edit", "patch", "multiedit", "notebookedit"])
9
+ const JS_SYNTAX_EXTENSIONS = new Set([".js", ".mjs", ".cjs"])
10
+ const TS_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"])
11
+
12
+ export const EDIT_DIAGNOSTICS_CONTRACT = "kkcode/edit-diagnostics@1"
13
+ export const MUTATION_OBSERVABILITY_CONTRACT = "kkcode/mutation-observability@1"
14
+
15
+ function toArray(value) {
16
+ return Array.isArray(value) ? value : []
17
+ }
18
+
19
+ function uniq(items) {
20
+ return [...new Set(items.filter(Boolean))]
21
+ }
22
+
23
+ function pluralize(count, noun) {
24
+ return `${count} ${noun}${count === 1 ? "" : "s"}`
25
+ }
26
+
27
+ function relativeFile(cwd, filePath) {
28
+ if (!filePath) return ""
29
+ const absolute = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath)
30
+ const relative = path.relative(cwd, absolute)
31
+ return relative && !relative.startsWith("..") ? relative : filePath
32
+ }
33
+
34
+ function diagnosticFingerprint(diagnostic = {}) {
35
+ return [
36
+ diagnostic.provider || "",
37
+ diagnostic.file || "",
38
+ diagnostic.code || "",
39
+ diagnostic.severity || "",
40
+ diagnostic.line ?? "",
41
+ diagnostic.column ?? "",
42
+ diagnostic.message || ""
43
+ ].join("|")
44
+ }
45
+
46
+ function normalizeDiagnostic(diagnostic = {}, cwd = process.cwd()) {
47
+ const file = diagnostic.file ? relativeFile(cwd, diagnostic.file) : ""
48
+ return {
49
+ provider: String(diagnostic.provider || "unknown"),
50
+ file,
51
+ severity: String(diagnostic.severity || "error"),
52
+ code: diagnostic.code ? String(diagnostic.code) : null,
53
+ message: String(diagnostic.message || "").trim(),
54
+ line: Number.isFinite(diagnostic.line) ? Number(diagnostic.line) : null,
55
+ column: Number.isFinite(diagnostic.column) ? Number(diagnostic.column) : null
56
+ }
57
+ }
58
+
59
+ function sortDiagnostics(diagnostics = []) {
60
+ return [...diagnostics].sort((left, right) => {
61
+ const byFile = String(left.file || "").localeCompare(String(right.file || ""))
62
+ if (byFile !== 0) return byFile
63
+ const byLine = Number(left.line || 0) - Number(right.line || 0)
64
+ if (byLine !== 0) return byLine
65
+ const byColumn = Number(left.column || 0) - Number(right.column || 0)
66
+ if (byColumn !== 0) return byColumn
67
+ return String(left.message || "").localeCompare(String(right.message || ""))
68
+ })
69
+ }
70
+
71
+ function summarizeDiagnosticsDelta({ baseline = [], current = [], delta, available, reason = "" }) {
72
+ if (!available) {
73
+ return {
74
+ status: "unavailable",
75
+ text: reason ? `diagnostics unavailable (${reason})` : "diagnostics unavailable"
76
+ }
77
+ }
78
+
79
+ const addedCount = delta.added.length
80
+ const resolvedCount = delta.resolved.length
81
+ const persistedCount = delta.persisted.length
82
+ const currentCount = current.length
83
+ const baselineCount = baseline.length
84
+
85
+ if (addedCount === 0 && resolvedCount === 0 && currentCount === 0) {
86
+ return {
87
+ status: "clean",
88
+ text: baselineCount === 0
89
+ ? "clean (no diagnostics before or after)"
90
+ : "clean (all prior diagnostics resolved)"
91
+ }
92
+ }
93
+
94
+ if (addedCount > 0 && resolvedCount > 0) {
95
+ return {
96
+ status: "mixed",
97
+ text: `introduced ${pluralize(addedCount, "diagnostic")}, resolved ${pluralize(resolvedCount, "diagnostic")}, ${pluralize(persistedCount, "diagnostic")} still present`
98
+ }
99
+ }
100
+
101
+ if (addedCount > 0) {
102
+ return {
103
+ status: "regressed",
104
+ text: `introduced ${pluralize(addedCount, "diagnostic")}; ${pluralize(persistedCount, "diagnostic")} still present`
105
+ }
106
+ }
107
+
108
+ if (resolvedCount > 0) {
109
+ return {
110
+ status: "improved",
111
+ text: currentCount === 0
112
+ ? `resolved ${pluralize(resolvedCount, "diagnostic")}; workspace is clean`
113
+ : `resolved ${pluralize(resolvedCount, "diagnostic")}; ${pluralize(currentCount, "diagnostic")} remain`
114
+ }
115
+ }
116
+
117
+ return {
118
+ status: "unchanged",
119
+ text: currentCount === 0
120
+ ? "clean (no diagnostics changed)"
121
+ : `no diagnostic changes (${pluralize(currentCount, "diagnostic")} remain)`
122
+ }
123
+ }
124
+
125
+ export function diffDiagnostics(baseline = [], current = []) {
126
+ const baselineMap = new Map(baseline.map((item) => [diagnosticFingerprint(item), item]))
127
+ const currentMap = new Map(current.map((item) => [diagnosticFingerprint(item), item]))
128
+
129
+ const added = []
130
+ const persisted = []
131
+ const resolved = []
132
+
133
+ for (const [key, diagnostic] of currentMap) {
134
+ if (baselineMap.has(key)) persisted.push(diagnostic)
135
+ else added.push(diagnostic)
136
+ }
137
+
138
+ for (const [key, diagnostic] of baselineMap) {
139
+ if (!currentMap.has(key)) resolved.push(diagnostic)
140
+ }
141
+
142
+ return {
143
+ added: sortDiagnostics(added),
144
+ persisted: sortDiagnostics(persisted),
145
+ resolved: sortDiagnostics(resolved),
146
+ unchanged: added.length === 0 && resolved.length === 0
147
+ }
148
+ }
149
+
150
+ async function exists(target) {
151
+ try {
152
+ await access(target)
153
+ return true
154
+ } catch {
155
+ return false
156
+ }
157
+ }
158
+
159
+ function createProviderStatus({ name, available, checkedFiles = [], diagnostics = [], reason = "" }) {
160
+ return {
161
+ name,
162
+ available,
163
+ checkedFiles: uniq(checkedFiles),
164
+ diagnostics: diagnostics.length,
165
+ ...(reason ? { reason } : {})
166
+ }
167
+ }
168
+
169
+ async function collectNodeSyntaxDiagnostics({ cwd, files }) {
170
+ const checkedFiles = uniq(files.filter((file) => JS_SYNTAX_EXTENSIONS.has(path.extname(file).toLowerCase())))
171
+ if (checkedFiles.length === 0) {
172
+ return {
173
+ diagnostics: [],
174
+ providers: [createProviderStatus({ name: "node-syntax", available: false, reason: "no JavaScript syntax-checkable files" })]
175
+ }
176
+ }
177
+
178
+ const diagnostics = []
179
+ for (const file of checkedFiles) {
180
+ const absolute = path.resolve(cwd, file)
181
+ try {
182
+ await execFile(process.execPath, ["--check", absolute], {
183
+ cwd,
184
+ timeout: 15000,
185
+ encoding: "utf8"
186
+ })
187
+ } catch (error) {
188
+ const output = String(error?.stderr || error?.stdout || error?.message || "").trim()
189
+ const lineMatch = output.match(/:(\d+)\s*\n/) || output.match(/:(\d+)\s*$/m)
190
+ diagnostics.push(normalizeDiagnostic({
191
+ provider: "node-syntax",
192
+ file,
193
+ severity: "error",
194
+ code: "node-check",
195
+ message: output || "JavaScript syntax error",
196
+ line: lineMatch ? Number(lineMatch[1]) : null,
197
+ column: null
198
+ }, cwd))
199
+ }
200
+ }
201
+
202
+ return {
203
+ diagnostics,
204
+ providers: [createProviderStatus({
205
+ name: "node-syntax",
206
+ available: true,
207
+ checkedFiles,
208
+ diagnostics
209
+ })]
210
+ }
211
+ }
212
+
213
+ function parseTypeScriptDiagnostics(output, cwd) {
214
+ const diagnostics = []
215
+ const lines = String(output || "").split(/\r?\n/)
216
+ for (const line of lines) {
217
+ const match = line.match(/^(.*)\((\d+),(\d+)\): error (TS\d+): (.+)$/)
218
+ || line.match(/^(.*):(\d+):(\d+) - error (TS\d+): (.+)$/)
219
+ if (!match) continue
220
+ diagnostics.push(normalizeDiagnostic({
221
+ provider: "typescript-project",
222
+ file: match[1],
223
+ severity: "error",
224
+ code: match[4],
225
+ message: match[5],
226
+ line: Number(match[2]),
227
+ column: Number(match[3])
228
+ }, cwd))
229
+ }
230
+ return diagnostics
231
+ }
232
+
233
+ async function collectTypeScriptDiagnostics({ cwd, files }) {
234
+ const checkedFiles = uniq(files.filter((file) => TS_EXTENSIONS.has(path.extname(file).toLowerCase())))
235
+ if (checkedFiles.length === 0) {
236
+ return {
237
+ diagnostics: [],
238
+ providers: [createProviderStatus({ name: "typescript-project", available: false, reason: "no TypeScript files in edit set" })]
239
+ }
240
+ }
241
+
242
+ const tsconfigPath = path.join(cwd, "tsconfig.json")
243
+ if (!(await exists(tsconfigPath))) {
244
+ return {
245
+ diagnostics: [],
246
+ providers: [createProviderStatus({ name: "typescript-project", available: false, checkedFiles, reason: "missing tsconfig.json" })]
247
+ }
248
+ }
249
+
250
+ try {
251
+ await execFile("npx", ["tsc", "--noEmit", "--pretty", "false"], {
252
+ cwd,
253
+ timeout: 20000,
254
+ encoding: "utf8"
255
+ })
256
+ return {
257
+ diagnostics: [],
258
+ providers: [createProviderStatus({ name: "typescript-project", available: true, checkedFiles, diagnostics: [] })]
259
+ }
260
+ } catch (error) {
261
+ const output = String(error?.stdout || error?.stderr || error?.message || "").trim()
262
+ const diagnostics = parseTypeScriptDiagnostics(output, cwd)
263
+ return {
264
+ diagnostics,
265
+ providers: [createProviderStatus({
266
+ name: "typescript-project",
267
+ available: true,
268
+ checkedFiles,
269
+ diagnostics,
270
+ reason: diagnostics.length === 0 ? "typecheck failed without parseable diagnostics" : ""
271
+ })]
272
+ }
273
+ }
274
+ }
275
+
276
+ export async function collectDiagnosticsSnapshot({ cwd = process.cwd(), files = [] } = {}) {
277
+ const normalizedFiles = uniq(files.map((file) => relativeFile(cwd, file)))
278
+ const nodeSyntax = await collectNodeSyntaxDiagnostics({ cwd, files: normalizedFiles })
279
+ const typescript = await collectTypeScriptDiagnostics({ cwd, files: normalizedFiles })
280
+ const diagnostics = sortDiagnostics([
281
+ ...toArray(nodeSyntax.diagnostics),
282
+ ...toArray(typescript.diagnostics)
283
+ ])
284
+ const providers = [...toArray(nodeSyntax.providers), ...toArray(typescript.providers)]
285
+ const available = providers.some((provider) => provider.available)
286
+
287
+ return {
288
+ files: normalizedFiles,
289
+ diagnostics,
290
+ providers,
291
+ available
292
+ }
293
+ }
294
+
295
+ export function buildEditDiagnosticsReport({ cwd = process.cwd(), files = [], baseline = {}, current = {}, reason = "" } = {}) {
296
+ const baselineDiagnostics = sortDiagnostics(toArray(baseline.diagnostics || []).map((item) => normalizeDiagnostic(item, cwd)))
297
+ const currentDiagnostics = sortDiagnostics(toArray(current.diagnostics || []).map((item) => normalizeDiagnostic(item, cwd)))
298
+ const delta = diffDiagnostics(baselineDiagnostics, currentDiagnostics)
299
+ const available = Boolean(baseline.available || current.available || baselineDiagnostics.length || currentDiagnostics.length)
300
+ const summary = summarizeDiagnosticsDelta({
301
+ baseline: baselineDiagnostics,
302
+ current: currentDiagnostics,
303
+ delta,
304
+ available,
305
+ reason
306
+ })
307
+
308
+ return {
309
+ contract: EDIT_DIAGNOSTICS_CONTRACT,
310
+ files: uniq(files.map((file) => relativeFile(cwd, file))),
311
+ available,
312
+ baseline: {
313
+ count: baselineDiagnostics.length,
314
+ diagnostics: baselineDiagnostics,
315
+ providers: toArray(baseline.providers)
316
+ },
317
+ current: {
318
+ count: currentDiagnostics.length,
319
+ diagnostics: currentDiagnostics,
320
+ providers: toArray(current.providers)
321
+ },
322
+ delta,
323
+ summary
324
+ }
325
+ }
326
+
327
+ function normalizeMutationChanges(metadata = {}) {
328
+ if (metadata?.observability?.contract === MUTATION_OBSERVABILITY_CONTRACT) {
329
+ return toArray(metadata.observability.changes)
330
+ }
331
+
332
+ const fromMutations = toArray(metadata.mutations).map((item) => ({
333
+ path: String(item?.filePath || item?.path || "").trim(),
334
+ operation: String(item?.operation || "multiedit"),
335
+ addedLines: Math.max(0, Number(item?.addedLines || 0)),
336
+ removedLines: Math.max(0, Number(item?.removedLines || 0))
337
+ }))
338
+
339
+ const fromMutation = metadata.mutation
340
+ ? [{
341
+ path: String(metadata.mutation.filePath || metadata.mutation.path || "").trim(),
342
+ operation: String(metadata.mutation.operation || "mutation"),
343
+ addedLines: Math.max(0, Number(metadata.mutation.addedLines || 0)),
344
+ removedLines: Math.max(0, Number(metadata.mutation.removedLines || 0))
345
+ }]
346
+ : []
347
+
348
+ const fromFileChanges = toArray(metadata.fileChanges).map((item) => ({
349
+ path: String(item?.path || "").trim(),
350
+ operation: String(item?.tool || item?.operation || "mutation"),
351
+ addedLines: Math.max(0, Number(item?.addedLines || 0)),
352
+ removedLines: Math.max(0, Number(item?.removedLines || 0))
353
+ }))
354
+
355
+ return uniq([...fromMutations, ...fromMutation, ...fromFileChanges]
356
+ .filter((item) => item.path)
357
+ .map((item) => JSON.stringify(item))).map((item) => JSON.parse(item))
358
+ }
359
+
360
+ export function buildMutationObservability(metadata = {}) {
361
+ const changes = normalizeMutationChanges(metadata)
362
+ if (changes.length === 0) {
363
+ return {
364
+ contract: MUTATION_OBSERVABILITY_CONTRACT,
365
+ changes: [],
366
+ totals: {
367
+ filesChanged: 0,
368
+ addedLines: 0,
369
+ removedLines: 0
370
+ },
371
+ operations: [],
372
+ summary: "no file mutations recorded"
373
+ }
374
+ }
375
+
376
+ const totals = changes.reduce((acc, item) => ({
377
+ filesChanged: acc.filesChanged + 1,
378
+ addedLines: acc.addedLines + Math.max(0, Number(item.addedLines || 0)),
379
+ removedLines: acc.removedLines + Math.max(0, Number(item.removedLines || 0))
380
+ }), { filesChanged: 0, addedLines: 0, removedLines: 0 })
381
+ const operations = uniq(changes.map((item) => item.operation))
382
+ const operationText = operations.length === 1
383
+ ? `via ${operations[0]}`
384
+ : `across ${pluralize(operations.length, "operation")}`
385
+ const summary = `${pluralize(totals.filesChanged, "file")} changed ${operationText} (+${totals.addedLines}/-${totals.removedLines})`
386
+
387
+ return {
388
+ contract: MUTATION_OBSERVABILITY_CONTRACT,
389
+ changes,
390
+ totals,
391
+ operations,
392
+ summary
393
+ }
394
+ }
395
+
396
+ export function extractTouchedFiles({ args = {}, metadata = {} } = {}) {
397
+ const files = []
398
+ if (args?.path) files.push(String(args.path))
399
+ for (const change of toArray(args?.changes)) {
400
+ if (change?.path) files.push(String(change.path))
401
+ }
402
+ for (const change of toArray(metadata?.fileChanges)) {
403
+ if (change?.path) files.push(String(change.path))
404
+ }
405
+ if (metadata?.mutation?.filePath) files.push(String(metadata.mutation.filePath))
406
+ for (const change of toArray(metadata?.mutations)) {
407
+ if (change?.filePath) files.push(String(change.filePath))
408
+ }
409
+ return uniq(files)
410
+ }
411
+
412
+ export function isMutationTool(toolName) {
413
+ return MUTATION_TOOLS.has(String(toolName || ""))
414
+ }
415
+
416
+ export function isDiagnosticsEligibleFile(filePath) {
417
+ const extension = path.extname(String(filePath || "")).toLowerCase()
418
+ return JS_SYNTAX_EXTENSIONS.has(extension) || TS_EXTENSIONS.has(extension)
419
+ }
420
+
421
+ export function extractEditFeedbackFromToolEvents(toolEvents = []) {
422
+ return toArray(toolEvents)
423
+ .filter((event) => isMutationTool(event?.name))
424
+ .map((event) => {
425
+ const metadata = event?.metadata && typeof event.metadata === "object" ? event.metadata : {}
426
+ const observability = metadata.observability?.contract === MUTATION_OBSERVABILITY_CONTRACT
427
+ ? metadata.observability
428
+ : buildMutationObservability(metadata)
429
+ const diagnostics = metadata.diagnostics?.contract === EDIT_DIAGNOSTICS_CONTRACT
430
+ ? metadata.diagnostics
431
+ : null
432
+ const files = uniq([
433
+ ...extractTouchedFiles({ args: event?.args, metadata }),
434
+ ...toArray(observability?.changes).map((item) => item.path),
435
+ ...toArray(diagnostics?.files)
436
+ ])
437
+
438
+ if (!observability.changes.length && !diagnostics) return null
439
+
440
+ return {
441
+ tool: String(event?.name || ""),
442
+ status: String(event?.status || ""),
443
+ files,
444
+ observability,
445
+ diagnostics
446
+ }
447
+ })
448
+ .filter(Boolean)
449
+ }
@@ -1,42 +1,42 @@
1
- import { createMetricsCollector } from "./metrics.mjs"
2
- import { createTracer } from "./tracer.mjs"
3
-
4
- let metrics = null
5
- let tracer = null
6
- let unsubscribes = []
7
-
8
- export function initialize(eventBus) {
9
- if (metrics) return // idempotent
10
-
11
- metrics = createMetricsCollector()
12
- tracer = createTracer()
13
-
14
- unsubscribes.push(
15
- eventBus.registerSink(async (event) => {
16
- metrics.handleEvent(event)
17
- tracer.handleEvent(event)
18
- })
19
- )
20
- }
21
-
22
- export function shutdown() {
23
- for (const unsub of unsubscribes) unsub()
24
- unsubscribes = []
25
- metrics = null
26
- tracer = null
27
- }
28
-
29
- export function getMetrics() {
30
- return metrics ? metrics.getSnapshot() : null
31
- }
32
-
33
- export function getTraces() {
34
- return tracer ? tracer.getTraces() : []
35
- }
36
-
37
- export function exportReport() {
38
- return {
39
- metrics: metrics ? metrics.getSnapshot() : null,
40
- traces: tracer ? tracer.exportTraces("json") : "[]"
41
- }
42
- }
1
+ import { createMetricsCollector } from "./metrics.mjs"
2
+ import { createTracer } from "./tracer.mjs"
3
+
4
+ let metrics = null
5
+ let tracer = null
6
+ let unsubscribes = []
7
+
8
+ export function initialize(eventBus) {
9
+ if (metrics) return // idempotent
10
+
11
+ metrics = createMetricsCollector()
12
+ tracer = createTracer()
13
+
14
+ unsubscribes.push(
15
+ eventBus.registerSink(async (event) => {
16
+ metrics.handleEvent(event)
17
+ tracer.handleEvent(event)
18
+ })
19
+ )
20
+ }
21
+
22
+ export function shutdown() {
23
+ for (const unsub of unsubscribes) unsub()
24
+ unsubscribes = []
25
+ metrics = null
26
+ tracer = null
27
+ }
28
+
29
+ export function getMetrics() {
30
+ return metrics ? metrics.getSnapshot() : null
31
+ }
32
+
33
+ export function getTraces() {
34
+ return tracer ? tracer.getTraces() : []
35
+ }
36
+
37
+ export function exportReport() {
38
+ return {
39
+ metrics: metrics ? metrics.getSnapshot() : null,
40
+ traces: tracer ? tracer.exportTraces("json") : "[]"
41
+ }
42
+ }