@kkelly-offical/kkcode 0.1.7 → 0.2.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 (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -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/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2981
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +298 -298
  96. package/src/session/engine.mjs +417 -232
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1097
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -900
  105. package/src/session/loop.mjs +1005 -930
  106. package/src/session/prompt/agent.txt +25 -25
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +31 -31
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +196 -195
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -519
  116. package/src/session/system-prompt.mjs +308 -273
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +99 -93
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. package/src/util/template.mjs +6 -1
@@ -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
+ }