@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.0

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 (246) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
  3. package/dist/cli.js +9554 -12379
  4. package/dist/template-8vdv6xb4.js +1671 -0
  5. package/dist/template-f8wx9vfn.css +1355 -0
  6. package/dist/template-qat058wr.html +55 -0
  7. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  8. package/dist/types/advisor/advise-tool.d.ts +0 -7
  9. package/dist/types/advisor/runtime.d.ts +8 -11
  10. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  11. package/dist/types/async/job-manager.d.ts +11 -0
  12. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  13. package/dist/types/cleanse/agent.d.ts +19 -0
  14. package/dist/types/cleanse/balance.d.ts +7 -0
  15. package/dist/types/cleanse/checkers.d.ts +13 -0
  16. package/dist/types/cleanse/index.d.ts +16 -0
  17. package/dist/types/cleanse/loop.d.ts +16 -0
  18. package/dist/types/cleanse/parsers.d.ts +13 -0
  19. package/dist/types/cleanse/progress.d.ts +14 -0
  20. package/dist/types/cleanse/types.d.ts +64 -0
  21. package/dist/types/cli/args.d.ts +2 -0
  22. package/dist/types/cli/session-picker.d.ts +14 -7
  23. package/dist/types/collab/guest.d.ts +21 -2
  24. package/dist/types/commands/cleanse.d.ts +23 -0
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/config/settings-schema.d.ts +63 -8
  27. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  28. package/dist/types/cursor.d.ts +193 -8
  29. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  30. package/dist/types/edit/hashline/diff.d.ts +9 -1
  31. package/dist/types/edit/index.d.ts +8 -1
  32. package/dist/types/edit/streaming.d.ts +8 -1
  33. package/dist/types/export/html/index.d.ts +2 -0
  34. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  35. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
  38. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  39. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  40. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  41. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  42. package/dist/types/lsp/client.d.ts +2 -0
  43. package/dist/types/main.d.ts +2 -0
  44. package/dist/types/mcp/manager.d.ts +33 -4
  45. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  47. package/dist/types/modes/components/session-selector.d.ts +6 -0
  48. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  50. package/dist/types/modes/components/usage-row.d.ts +2 -0
  51. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  52. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  53. package/dist/types/modes/interactive-mode.d.ts +8 -5
  54. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  55. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  56. package/dist/types/modes/theme/theme.d.ts +12 -1
  57. package/dist/types/modes/types.d.ts +2 -1
  58. package/dist/types/sdk.d.ts +1 -0
  59. package/dist/types/session/agent-session-events.d.ts +2 -0
  60. package/dist/types/session/agent-session-types.d.ts +36 -1
  61. package/dist/types/session/agent-session.d.ts +4 -2
  62. package/dist/types/session/async-job-delivery.d.ts +8 -0
  63. package/dist/types/session/claude-session-store.d.ts +13 -0
  64. package/dist/types/session/codex-session-store.d.ts +14 -0
  65. package/dist/types/session/credential-pin.d.ts +58 -0
  66. package/dist/types/session/foreign-session-import.d.ts +15 -0
  67. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  68. package/dist/types/session/foreign-session-store.d.ts +23 -0
  69. package/dist/types/session/model-controls.d.ts +3 -3
  70. package/dist/types/session/session-advisors.d.ts +47 -2
  71. package/dist/types/session/session-entries.d.ts +19 -1
  72. package/dist/types/session/session-handoff.d.ts +4 -1
  73. package/dist/types/session/session-loader.d.ts +2 -0
  74. package/dist/types/session/session-maintenance.d.ts +1 -1
  75. package/dist/types/session/session-manager.d.ts +21 -0
  76. package/dist/types/session/session-tools.d.ts +25 -1
  77. package/dist/types/session/turn-recovery.d.ts +10 -4
  78. package/dist/types/system-prompt.d.ts +2 -1
  79. package/dist/types/task/executor.d.ts +11 -2
  80. package/dist/types/task/isolation-ownership.d.ts +34 -0
  81. package/dist/types/task/render.d.ts +7 -0
  82. package/dist/types/thinking.d.ts +18 -6
  83. package/dist/types/tools/acp-bridge.d.ts +28 -5
  84. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
  85. package/dist/types/tools/browser/tab-worker.d.ts +1 -4
  86. package/dist/types/tools/grep.d.ts +17 -1
  87. package/dist/types/tools/hub/launch.d.ts +5 -1
  88. package/dist/types/tools/index.d.ts +4 -1
  89. package/dist/types/tools/path-utils.d.ts +19 -0
  90. package/dist/types/tools/read.d.ts +2 -0
  91. package/dist/types/tools/xdev.d.ts +2 -1
  92. package/dist/types/tui/output-block.d.ts +5 -5
  93. package/dist/types/utils/changelog.d.ts +19 -0
  94. package/dist/types/utils/git.d.ts +10 -0
  95. package/dist/types/utils/jj.d.ts +11 -4
  96. package/dist/types/web/search/types.d.ts +2 -2
  97. package/package.json +16 -12
  98. package/scripts/bundle-dist.ts +3 -1
  99. package/src/advisor/advise-tool.ts +0 -11
  100. package/src/advisor/runtime.ts +138 -24
  101. package/src/advisor/transcript-recorder.ts +56 -0
  102. package/src/async/job-manager.ts +30 -7
  103. package/src/auto-thinking/classifier.ts +48 -10
  104. package/src/cleanse/agent.ts +226 -0
  105. package/src/cleanse/balance.ts +79 -0
  106. package/src/cleanse/checkers.ts +996 -0
  107. package/src/cleanse/index.ts +190 -0
  108. package/src/cleanse/loop.ts +51 -0
  109. package/src/cleanse/parsers.ts +726 -0
  110. package/src/cleanse/progress.ts +50 -0
  111. package/src/cleanse/prompts/assignment.md +47 -0
  112. package/src/cleanse/types.ts +72 -0
  113. package/src/cli/args.ts +6 -0
  114. package/src/cli/flag-tables.ts +2 -0
  115. package/src/cli/gallery-fixtures/fs.ts +25 -0
  116. package/src/cli/session-picker.ts +34 -17
  117. package/src/cli/ttsr-cli.ts +19 -1
  118. package/src/cli/update-cli.ts +3 -0
  119. package/src/cli/usage-cli.ts +29 -4
  120. package/src/cli/worktree-cli.ts +28 -11
  121. package/src/cli-commands.ts +1 -0
  122. package/src/cli.ts +11 -1
  123. package/src/collab/guest.ts +37 -4
  124. package/src/collab/host.ts +1 -0
  125. package/src/commands/cleanse.ts +45 -0
  126. package/src/commands/launch.ts +6 -0
  127. package/src/config/model-registry.ts +103 -25
  128. package/src/config/settings-schema.ts +65 -6
  129. package/src/config/settings.ts +25 -0
  130. package/src/cursor-bridge-tools.ts +81 -0
  131. package/src/cursor.ts +463 -12
  132. package/src/discovery/claude-plugins.ts +144 -34
  133. package/src/edit/edit-clipboard.ts +23 -0
  134. package/src/edit/hashline/diff.ts +49 -10
  135. package/src/edit/hashline/execute.ts +38 -6
  136. package/src/edit/hashline/filesystem.ts +27 -3
  137. package/src/edit/index.ts +12 -2
  138. package/src/edit/renderer.ts +4 -4
  139. package/src/edit/streaming.ts +17 -2
  140. package/src/export/html/index.ts +17 -10
  141. package/src/export/html/template.js +1 -1
  142. package/src/extensibility/extensions/runner.ts +70 -0
  143. package/src/extensibility/extensions/types.ts +30 -0
  144. package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
  145. package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
  146. package/src/internal-urls/local-protocol.ts +3 -2
  147. package/src/internal-urls/mcp-protocol.ts +4 -1
  148. package/src/launch/broker.ts +14 -4
  149. package/src/launch/terminal-output-worker-client.ts +53 -0
  150. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  151. package/src/launch/terminal-output-worker.ts +23 -0
  152. package/src/lsp/client.ts +18 -3
  153. package/src/lsp/index.ts +4 -0
  154. package/src/lsp/render.ts +1 -1
  155. package/src/main.ts +127 -56
  156. package/src/mcp/manager.ts +120 -18
  157. package/src/modes/acp/acp-agent.ts +27 -18
  158. package/src/modes/acp/acp-event-mapper.ts +38 -4
  159. package/src/modes/components/chat-transcript-builder.ts +30 -11
  160. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  161. package/src/modes/components/read-tool-group.ts +159 -17
  162. package/src/modes/components/session-selector.ts +19 -3
  163. package/src/modes/components/settings-selector.ts +9 -1
  164. package/src/modes/components/status-line/component.ts +381 -54
  165. package/src/modes/components/tool-execution.ts +78 -31
  166. package/src/modes/components/usage-row.ts +10 -5
  167. package/src/modes/controllers/event-controller.ts +26 -4
  168. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  169. package/src/modes/controllers/selector-controller.ts +94 -36
  170. package/src/modes/controllers/tan-command-controller.ts +14 -0
  171. package/src/modes/interactive-mode.ts +67 -64
  172. package/src/modes/prompt-action-autocomplete.ts +5 -3
  173. package/src/modes/rpc/rpc-client.ts +18 -0
  174. package/src/modes/rpc/rpc-mode.ts +16 -1
  175. package/src/modes/rpc/rpc-types.ts +11 -0
  176. package/src/modes/theme/theme.ts +151 -6
  177. package/src/modes/types.ts +2 -1
  178. package/src/modes/utils/ui-helpers.ts +38 -21
  179. package/src/prompts/goals/guided-goal-interview.md +41 -6
  180. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  181. package/src/prompts/system/plan-mode-active.md +2 -2
  182. package/src/prompts/system/system-prompt.md +3 -0
  183. package/src/prompts/system/vibe-mode-active.md +4 -1
  184. package/src/prompts/system/xdev-mount-notice.md +1 -1
  185. package/src/prompts/tools/bash.md +15 -17
  186. package/src/prompts/tools/browser.md +1 -0
  187. package/src/prompts/tools/checkpoint.md +1 -1
  188. package/src/prompts/tools/glob.md +7 -6
  189. package/src/prompts/tools/goal.md +1 -1
  190. package/src/prompts/tools/grep.md +7 -6
  191. package/src/sdk.ts +178 -15
  192. package/src/session/agent-session-events.ts +1 -0
  193. package/src/session/agent-session-types.ts +40 -1
  194. package/src/session/agent-session.ts +330 -179
  195. package/src/session/async-job-delivery.ts +8 -0
  196. package/src/session/claude-session-store.ts +426 -0
  197. package/src/session/codex-session-store.ts +673 -0
  198. package/src/session/credential-pin.ts +93 -0
  199. package/src/session/foreign-session-import.ts +52 -0
  200. package/src/session/foreign-session-jsonl.ts +29 -0
  201. package/src/session/foreign-session-store.ts +26 -0
  202. package/src/session/model-controls.ts +15 -7
  203. package/src/session/session-advisors.ts +142 -23
  204. package/src/session/session-entries.ts +21 -1
  205. package/src/session/session-handoff.ts +20 -3
  206. package/src/session/session-history-format.ts +31 -6
  207. package/src/session/session-loader.ts +25 -9
  208. package/src/session/session-maintenance.ts +59 -11
  209. package/src/session/session-manager.ts +61 -0
  210. package/src/session/session-tools.ts +107 -5
  211. package/src/session/settings-stream-fn.ts +1 -0
  212. package/src/session/turn-recovery.ts +97 -51
  213. package/src/slash-commands/builtin-registry.ts +136 -5
  214. package/src/system-prompt.ts +3 -2
  215. package/src/task/executor.ts +17 -4
  216. package/src/task/isolation-ownership.ts +106 -0
  217. package/src/task/render.ts +20 -4
  218. package/src/task/worktree.ts +8 -0
  219. package/src/thinking.ts +29 -10
  220. package/src/tools/acp-bridge.ts +52 -8
  221. package/src/tools/ask.ts +3 -3
  222. package/src/tools/ast-edit.ts +9 -2
  223. package/src/tools/browser/cmux/cmux-tab.ts +9 -14
  224. package/src/tools/browser/tab-worker.ts +12 -35
  225. package/src/tools/browser.ts +7 -2
  226. package/src/tools/checkpoint.ts +0 -13
  227. package/src/tools/gh-renderer.ts +3 -3
  228. package/src/tools/grep.ts +64 -8
  229. package/src/tools/hub/launch.ts +10 -15
  230. package/src/tools/index.ts +38 -5
  231. package/src/tools/path-utils.ts +88 -0
  232. package/src/tools/read.ts +3 -0
  233. package/src/tools/write.ts +44 -3
  234. package/src/tools/xdev.ts +39 -14
  235. package/src/tui/code-cell.ts +4 -4
  236. package/src/tui/output-block.ts +25 -8
  237. package/src/utils/changelog.ts +144 -4
  238. package/src/utils/git.ts +61 -14
  239. package/src/utils/jj.ts +19 -13
  240. package/src/utils/shell-snapshot-fn-env.sh +5 -2
  241. package/src/web/search/providers/codex.ts +34 -0
  242. package/src/web/search/render.ts +2 -2
  243. package/src/web/search/types.ts +1 -1
  244. package/dist/types/goals/guided-setup.d.ts +0 -30
  245. package/src/goals/guided-setup.ts +0 -171
  246. package/src/prompts/goals/guided-goal-system.md +0 -33
@@ -0,0 +1,726 @@
1
+ import * as path from "node:path";
2
+ import { isRecord, sanitizeText } from "@oh-my-pi/pi-utils";
3
+ import type { CleanseDiagnostic, CleanseSeverity } from "./types";
4
+
5
+ /** Machine and fallback output formats understood by cleanse. */
6
+ export type CleanseParserKind =
7
+ | "rust"
8
+ | "rust-test"
9
+ | "go"
10
+ | "go-test"
11
+ | "ruff"
12
+ | "pyright"
13
+ | "eslint"
14
+ | "biome"
15
+ | "rubocop"
16
+ | "phpstan"
17
+ | "psalm"
18
+ | "swiftlint"
19
+ | "dart"
20
+ | "credo"
21
+ | "shellcheck"
22
+ | "hlint"
23
+ | "terraform"
24
+ | "tflint"
25
+ | "generic";
26
+
27
+ /** Captured checker process output passed to a format parser. */
28
+ export interface CleanseParserInput {
29
+ checker: string;
30
+ projectCwd: string;
31
+ checkerCwd: string;
32
+ stdout: string;
33
+ stderr: string;
34
+ }
35
+
36
+ interface DiagnosticFields {
37
+ file?: string;
38
+ line?: number;
39
+ column?: number;
40
+ endLine?: number;
41
+ endColumn?: number;
42
+ code?: string;
43
+ severity?: CleanseSeverity | string | number;
44
+ message?: string;
45
+ suggestion?: string;
46
+ }
47
+
48
+ interface ParsedLocation {
49
+ file: string;
50
+ line?: number;
51
+ column?: number;
52
+ }
53
+
54
+ /** Parse one checker invocation into normalized, project-relative diagnostics. */
55
+ export function parseCleanseDiagnostics(kind: CleanseParserKind, input: CleanseParserInput): CleanseDiagnostic[] {
56
+ const parsed = (() => {
57
+ switch (kind) {
58
+ case "rust":
59
+ return parseRust(input);
60
+ case "rust-test":
61
+ return parseRustTest(input);
62
+ case "go":
63
+ return parseGo(input);
64
+ case "go-test":
65
+ return parseGoTest(input);
66
+ case "ruff":
67
+ return parseRuff(input);
68
+ case "pyright":
69
+ return parsePyright(input);
70
+ case "eslint":
71
+ return parseEslint(input);
72
+ case "biome":
73
+ return parseBiome(input);
74
+ case "rubocop":
75
+ return parseRubocop(input);
76
+ case "phpstan":
77
+ return parsePhpstan(input);
78
+ case "psalm":
79
+ return parsePsalm(input);
80
+ case "swiftlint":
81
+ return parseSwiftlint(input);
82
+ case "dart":
83
+ return parseDart(input);
84
+ case "credo":
85
+ return parseCredo(input);
86
+ case "shellcheck":
87
+ return parseShellcheck(input);
88
+ case "hlint":
89
+ return parseHlint(input);
90
+ case "terraform":
91
+ return parseTerraform(input);
92
+ case "tflint":
93
+ return parseTflint(input);
94
+ case "generic":
95
+ return parseGeneric(input);
96
+ }
97
+ })();
98
+ return deduplicateDiagnostics(parsed);
99
+ }
100
+
101
+ function toRecord(value: unknown): Record<string, unknown> | undefined {
102
+ return isRecord(value) ? value : undefined;
103
+ }
104
+
105
+ function toArray(value: unknown): unknown[] {
106
+ return Array.isArray(value) ? value : [];
107
+ }
108
+
109
+ function stringField(record: Record<string, unknown> | undefined, key: string): string | undefined {
110
+ const value = record?.[key];
111
+ return typeof value === "string" && value.trim() ? value : undefined;
112
+ }
113
+
114
+ function numberField(record: Record<string, unknown> | undefined, key: string): number | undefined {
115
+ const value = record?.[key];
116
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
117
+ }
118
+
119
+ function nestedRecord(record: Record<string, unknown> | undefined, key: string): Record<string, unknown> | undefined {
120
+ return toRecord(record?.[key]);
121
+ }
122
+
123
+ function normalizeSeverity(value: DiagnosticFields["severity"]): CleanseSeverity {
124
+ if (typeof value === "number") return value >= 2 ? "error" : value === 1 ? "warning" : "info";
125
+ const normalized = String(value ?? "warning").toLowerCase();
126
+ if (normalized.includes("error") || normalized === "fatal") return "error";
127
+ if (normalized.includes("warn") || normalized === "convention" || normalized === "refactor") return "warning";
128
+ return "info";
129
+ }
130
+
131
+ function normalizeFile(rawFile: string, input: CleanseParserInput): string | undefined {
132
+ let file = rawFile.trim().replace(/^['"]|['"]$/g, "");
133
+ if (!file || file.startsWith("<")) return undefined;
134
+ if (file.startsWith("file://")) {
135
+ try {
136
+ file = decodeURIComponent(new URL(file).pathname);
137
+ } catch {
138
+ return undefined;
139
+ }
140
+ }
141
+ const absolute = path.isAbsolute(file) ? path.normalize(file) : path.resolve(input.checkerCwd, file);
142
+ const relative = path.relative(input.projectCwd, absolute);
143
+ if (!relative || relative === ".") return undefined;
144
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return undefined;
145
+ return relative.split(path.sep).join("/");
146
+ }
147
+
148
+ function makeDiagnostic(input: CleanseParserInput, fields: DiagnosticFields): CleanseDiagnostic | undefined {
149
+ const message = sanitizeText(fields.message ?? "")
150
+ .replace(/\s+/g, " ")
151
+ .trim();
152
+ if (!message) return undefined;
153
+ const file = fields.file ? normalizeFile(fields.file, input) : undefined;
154
+ if (fields.file && !file) return undefined;
155
+ return {
156
+ checker: input.checker,
157
+ file,
158
+ line: positiveInteger(fields.line),
159
+ column: positiveInteger(fields.column),
160
+ endLine: positiveInteger(fields.endLine),
161
+ endColumn: positiveInteger(fields.endColumn),
162
+ code: fields.code?.trim() || undefined,
163
+ severity: normalizeSeverity(fields.severity),
164
+ message,
165
+ suggestion: fields.suggestion?.trim() || undefined,
166
+ };
167
+ }
168
+
169
+ function positiveInteger(value: number | undefined): number | undefined {
170
+ return value !== undefined && Number.isInteger(value) && value > 0 ? value : undefined;
171
+ }
172
+
173
+ function addDiagnostic(target: CleanseDiagnostic[], input: CleanseParserInput, fields: DiagnosticFields): void {
174
+ const diagnostic = makeDiagnostic(input, fields);
175
+ if (diagnostic) target.push(diagnostic);
176
+ }
177
+
178
+ function parseJsonValues(text: string): unknown[] {
179
+ const sanitized = sanitizeText(text).trim();
180
+ if (!sanitized) return [];
181
+ try {
182
+ const parsed: unknown = JSON.parse(sanitized);
183
+ return [parsed];
184
+ } catch {}
185
+ const values: unknown[] = [];
186
+ let start = -1;
187
+ let depth = 0;
188
+ let inString = false;
189
+ let escaped = false;
190
+ for (let index = 0; index < sanitized.length; index += 1) {
191
+ const char = sanitized[index];
192
+ if (start < 0) {
193
+ if (char !== "{" && char !== "[") continue;
194
+ start = index;
195
+ depth = 1;
196
+ inString = false;
197
+ escaped = false;
198
+ continue;
199
+ }
200
+ if (inString) {
201
+ if (escaped) escaped = false;
202
+ else if (char === "\\") escaped = true;
203
+ else if (char === '"') inString = false;
204
+ continue;
205
+ }
206
+ if (char === '"') inString = true;
207
+ else if (char === "{" || char === "[") depth += 1;
208
+ else if (char === "}" || char === "]") depth -= 1;
209
+ if (depth !== 0) continue;
210
+ try {
211
+ const parsed: unknown = JSON.parse(sanitized.slice(start, index + 1));
212
+ values.push(parsed);
213
+ } catch {}
214
+ start = -1;
215
+ }
216
+ return values;
217
+ }
218
+
219
+ function allJsonValues(input: CleanseParserInput): unknown[] {
220
+ return [...parseJsonValues(input.stdout), ...parseJsonValues(input.stderr)];
221
+ }
222
+
223
+ function parseRust(input: CleanseParserInput): CleanseDiagnostic[] {
224
+ const diagnostics: CleanseDiagnostic[] = [];
225
+ for (const value of parseJsonValues(input.stdout)) {
226
+ const envelope = toRecord(value);
227
+ if (stringField(envelope, "reason") !== "compiler-message") continue;
228
+ const message = nestedRecord(envelope, "message");
229
+ const level = stringField(message, "level");
230
+ if (level !== "error" && level !== "warning") continue;
231
+ const spans = toArray(message?.spans)
232
+ .map(toRecord)
233
+ .filter(entry => entry !== undefined);
234
+ const primary = spans.find(span => span.is_primary === true) ?? spans[0];
235
+ const code = nestedRecord(message, "code");
236
+ addDiagnostic(diagnostics, input, {
237
+ file: stringField(primary, "file_name"),
238
+ line: numberField(primary, "line_start"),
239
+ column: numberField(primary, "column_start"),
240
+ endLine: numberField(primary, "line_end"),
241
+ endColumn: numberField(primary, "column_end"),
242
+ code: stringField(code, "code"),
243
+ severity: level,
244
+ message: stringField(message, "message"),
245
+ suggestion: stringField(primary, "suggested_replacement"),
246
+ });
247
+ }
248
+ return diagnostics;
249
+ }
250
+
251
+ function parseRustTest(input: CleanseParserInput): CleanseDiagnostic[] {
252
+ const diagnostics = parseRust(input);
253
+ const text = `${input.stdout}\n${input.stderr}`;
254
+ for (const line of text.split("\n")) {
255
+ const panic = /thread '([^']+)' panicked at (.*?):(\d+):(\d+):/.exec(line);
256
+ if (!panic) continue;
257
+ addDiagnostic(diagnostics, input, {
258
+ file: panic[2],
259
+ line: Number.parseInt(panic[3], 10),
260
+ column: Number.parseInt(panic[4], 10),
261
+ severity: "error",
262
+ code: "test-failure",
263
+ message: `test ${panic[1]} panicked`,
264
+ });
265
+ }
266
+ return diagnostics;
267
+ }
268
+
269
+ function parseGo(input: CleanseParserInput): CleanseDiagnostic[] {
270
+ const diagnostics: CleanseDiagnostic[] = [];
271
+ const visit = (value: unknown, analyzer?: string): void => {
272
+ if (Array.isArray(value)) {
273
+ for (const child of value) visit(child, analyzer);
274
+ return;
275
+ }
276
+ const record = toRecord(value);
277
+ if (!record) return;
278
+ const position = stringField(record, "posn") ?? stringField(record, "position");
279
+ const message = stringField(record, "message");
280
+ if (position && message) {
281
+ const location = parseLocation(position);
282
+ if (location) {
283
+ addDiagnostic(diagnostics, input, {
284
+ ...location,
285
+ code: stringField(record, "category") ?? analyzer,
286
+ severity: "warning",
287
+ message,
288
+ });
289
+ }
290
+ return;
291
+ }
292
+ for (const key in record) visit(record[key], key);
293
+ };
294
+ for (const value of allJsonValues(input)) visit(value);
295
+ return diagnostics.length > 0 ? diagnostics : parseGeneric(input);
296
+ }
297
+
298
+ function parseGoTest(input: CleanseParserInput): CleanseDiagnostic[] {
299
+ const diagnostics: CleanseDiagnostic[] = [];
300
+ for (const value of parseJsonValues(input.stdout)) {
301
+ const event = toRecord(value);
302
+ const output = stringField(event, "Output");
303
+ if (output) {
304
+ for (const line of output.split("\n")) {
305
+ const location = /^\s*(?:\.\/)?(.+?\.go):(\d+)(?::(\d+))?:\s*(.+?)\s*$/.exec(line);
306
+ if (!location) continue;
307
+ addDiagnostic(diagnostics, input, {
308
+ file: location[1],
309
+ line: Number.parseInt(location[2], 10),
310
+ column: location[3] ? Number.parseInt(location[3], 10) : undefined,
311
+ severity: "error",
312
+ code: "test-failure",
313
+ message: location[4],
314
+ });
315
+ }
316
+ }
317
+ const testName = stringField(event, "Test");
318
+ if (stringField(event, "Action") === "fail" && testName) {
319
+ addDiagnostic(diagnostics, input, {
320
+ severity: "error",
321
+ code: "test-failure",
322
+ message: `test ${testName} failed`,
323
+ });
324
+ }
325
+ }
326
+ return diagnostics.length > 0 ? diagnostics : parseGeneric(input);
327
+ }
328
+
329
+ function parseRuff(input: CleanseParserInput): CleanseDiagnostic[] {
330
+ const diagnostics: CleanseDiagnostic[] = [];
331
+ for (const root of allJsonValues(input)) {
332
+ for (const value of toArray(root)) {
333
+ const record = toRecord(value);
334
+ const location = nestedRecord(record, "location");
335
+ const endLocation = nestedRecord(record, "end_location");
336
+ const fix = nestedRecord(record, "fix");
337
+ addDiagnostic(diagnostics, input, {
338
+ file: stringField(record, "filename"),
339
+ line: numberField(location, "row"),
340
+ column: numberField(location, "column"),
341
+ endLine: numberField(endLocation, "row"),
342
+ endColumn: numberField(endLocation, "column"),
343
+ code: stringField(record, "code"),
344
+ severity: "warning",
345
+ message: stringField(record, "message"),
346
+ suggestion: stringField(fix, "message"),
347
+ });
348
+ }
349
+ }
350
+ return diagnostics;
351
+ }
352
+
353
+ function parsePyright(input: CleanseParserInput): CleanseDiagnostic[] {
354
+ const diagnostics: CleanseDiagnostic[] = [];
355
+ for (const rootValue of allJsonValues(input)) {
356
+ const root = toRecord(rootValue);
357
+ for (const value of toArray(root?.generalDiagnostics)) {
358
+ const record = toRecord(value);
359
+ const range = nestedRecord(record, "range");
360
+ const start = nestedRecord(range, "start");
361
+ const end = nestedRecord(range, "end");
362
+ addDiagnostic(diagnostics, input, {
363
+ file: stringField(record, "file"),
364
+ line: zeroBased(numberField(start, "line")),
365
+ column: zeroBased(numberField(start, "character")),
366
+ endLine: zeroBased(numberField(end, "line")),
367
+ endColumn: zeroBased(numberField(end, "character")),
368
+ code: stringField(record, "rule"),
369
+ severity: stringField(record, "severity"),
370
+ message: stringField(record, "message"),
371
+ });
372
+ }
373
+ }
374
+ return diagnostics;
375
+ }
376
+
377
+ function zeroBased(value: number | undefined): number | undefined {
378
+ return value === undefined ? undefined : value + 1;
379
+ }
380
+
381
+ function parseEslint(input: CleanseParserInput): CleanseDiagnostic[] {
382
+ const diagnostics: CleanseDiagnostic[] = [];
383
+ for (const root of allJsonValues(input)) {
384
+ for (const fileValue of toArray(root)) {
385
+ const file = toRecord(fileValue);
386
+ for (const messageValue of toArray(file?.messages)) {
387
+ const message = toRecord(messageValue);
388
+ const fix = nestedRecord(message, "fix");
389
+ addDiagnostic(diagnostics, input, {
390
+ file: stringField(file, "filePath"),
391
+ line: numberField(message, "line"),
392
+ column: numberField(message, "column"),
393
+ endLine: numberField(message, "endLine"),
394
+ endColumn: numberField(message, "endColumn"),
395
+ code: stringField(message, "ruleId"),
396
+ severity: numberField(message, "severity"),
397
+ message: stringField(message, "message"),
398
+ suggestion: fix ? "automatic fix available" : undefined,
399
+ });
400
+ }
401
+ }
402
+ }
403
+ return diagnostics;
404
+ }
405
+
406
+ function parseBiome(input: CleanseParserInput): CleanseDiagnostic[] {
407
+ const diagnostics: CleanseDiagnostic[] = [];
408
+ for (const rootValue of allJsonValues(input)) {
409
+ const root = toRecord(rootValue);
410
+ for (const value of toArray(root?.diagnostics)) {
411
+ const record = toRecord(value);
412
+ const location = nestedRecord(record, "location");
413
+ const pathRecord = nestedRecord(location, "path");
414
+ const message =
415
+ stringField(record, "description") ?? stringField(record, "message") ?? stringField(record, "title");
416
+ addDiagnostic(diagnostics, input, {
417
+ file: stringField(pathRecord, "file") ?? stringField(location, "path"),
418
+ code: stringField(record, "category"),
419
+ severity: stringField(record, "severity"),
420
+ message,
421
+ });
422
+ }
423
+ }
424
+ return diagnostics;
425
+ }
426
+
427
+ function parseRubocop(input: CleanseParserInput): CleanseDiagnostic[] {
428
+ const diagnostics: CleanseDiagnostic[] = [];
429
+ for (const rootValue of allJsonValues(input)) {
430
+ const root = toRecord(rootValue);
431
+ for (const fileValue of toArray(root?.files)) {
432
+ const file = toRecord(fileValue);
433
+ for (const offenseValue of toArray(file?.offenses)) {
434
+ const offense = toRecord(offenseValue);
435
+ const location = nestedRecord(offense, "location");
436
+ addDiagnostic(diagnostics, input, {
437
+ file: stringField(file, "path"),
438
+ line: numberField(location, "start_line"),
439
+ column: numberField(location, "start_column"),
440
+ endLine: numberField(location, "last_line"),
441
+ endColumn: numberField(location, "last_column"),
442
+ code: stringField(offense, "cop_name"),
443
+ severity: stringField(offense, "severity"),
444
+ message: stringField(offense, "message"),
445
+ suggestion: offense?.corrected === true ? "automatic correction available" : undefined,
446
+ });
447
+ }
448
+ }
449
+ }
450
+ return diagnostics;
451
+ }
452
+
453
+ function parsePhpstan(input: CleanseParserInput): CleanseDiagnostic[] {
454
+ const diagnostics: CleanseDiagnostic[] = [];
455
+ for (const rootValue of allJsonValues(input)) {
456
+ const root = toRecord(rootValue);
457
+ const files = toRecord(root?.files);
458
+ if (files) {
459
+ for (const fileName in files) {
460
+ const file = toRecord(files[fileName]);
461
+ for (const messageValue of toArray(file?.messages)) {
462
+ const message = toRecord(messageValue);
463
+ addDiagnostic(diagnostics, input, {
464
+ file: fileName,
465
+ line: numberField(message, "line"),
466
+ code: stringField(message, "identifier"),
467
+ severity: "error",
468
+ message: stringField(message, "message"),
469
+ });
470
+ }
471
+ }
472
+ }
473
+ for (const error of toArray(root?.errors)) {
474
+ if (typeof error === "string") addDiagnostic(diagnostics, input, { severity: "error", message: error });
475
+ }
476
+ }
477
+ return diagnostics;
478
+ }
479
+
480
+ function parsePsalm(input: CleanseParserInput): CleanseDiagnostic[] {
481
+ const diagnostics: CleanseDiagnostic[] = [];
482
+ for (const root of allJsonValues(input)) {
483
+ for (const value of toArray(root)) {
484
+ const record = toRecord(value);
485
+ const shortcode = numberField(record, "shortcode");
486
+ addDiagnostic(diagnostics, input, {
487
+ file: stringField(record, "file_name") ?? stringField(record, "file_path"),
488
+ line: numberField(record, "line_from"),
489
+ column: numberField(record, "column_from"),
490
+ endLine: numberField(record, "line_to"),
491
+ endColumn: numberField(record, "column_to"),
492
+ code: stringField(record, "type") ?? (shortcode === undefined ? undefined : String(shortcode)),
493
+ severity: stringField(record, "severity"),
494
+ message: stringField(record, "message"),
495
+ });
496
+ }
497
+ }
498
+ return diagnostics;
499
+ }
500
+
501
+ function parseSwiftlint(input: CleanseParserInput): CleanseDiagnostic[] {
502
+ const diagnostics: CleanseDiagnostic[] = [];
503
+ for (const root of allJsonValues(input)) {
504
+ for (const value of toArray(root)) {
505
+ const record = toRecord(value);
506
+ addDiagnostic(diagnostics, input, {
507
+ file: stringField(record, "file"),
508
+ line: numberField(record, "line"),
509
+ column: numberField(record, "character"),
510
+ code: stringField(record, "rule_id"),
511
+ severity: stringField(record, "severity"),
512
+ message: stringField(record, "reason"),
513
+ });
514
+ }
515
+ }
516
+ return diagnostics;
517
+ }
518
+
519
+ function parseDart(input: CleanseParserInput): CleanseDiagnostic[] {
520
+ const diagnostics: CleanseDiagnostic[] = [];
521
+ for (const line of sanitizeText(`${input.stdout}\n${input.stderr}`).split("\n")) {
522
+ const fields = line.split("|");
523
+ if (fields.length < 8) continue;
524
+ addDiagnostic(diagnostics, input, {
525
+ severity: fields[0],
526
+ code: fields[2],
527
+ file: fields[3],
528
+ line: Number.parseInt(fields[4], 10),
529
+ column: Number.parseInt(fields[5], 10),
530
+ message: fields.slice(7).join("|"),
531
+ });
532
+ }
533
+ return diagnostics;
534
+ }
535
+
536
+ function parseCredo(input: CleanseParserInput): CleanseDiagnostic[] {
537
+ const diagnostics: CleanseDiagnostic[] = [];
538
+ for (const rootValue of allJsonValues(input)) {
539
+ const root = toRecord(rootValue);
540
+ for (const value of toArray(root?.issues)) {
541
+ const record = toRecord(value);
542
+ addDiagnostic(diagnostics, input, {
543
+ file: stringField(record, "filename"),
544
+ line: numberField(record, "line_no") ?? numberField(record, "line"),
545
+ column: numberField(record, "column"),
546
+ code: stringField(record, "check"),
547
+ severity: stringField(record, "priority") ?? stringField(record, "category"),
548
+ message: stringField(record, "message"),
549
+ });
550
+ }
551
+ }
552
+ return diagnostics;
553
+ }
554
+
555
+ function parseShellcheck(input: CleanseParserInput): CleanseDiagnostic[] {
556
+ const diagnostics: CleanseDiagnostic[] = [];
557
+ for (const rootValue of allJsonValues(input)) {
558
+ const rootRecord = toRecord(rootValue);
559
+ const values = Array.isArray(rootValue) ? rootValue : toArray(rootRecord?.comments);
560
+ for (const value of values) {
561
+ const record = toRecord(value);
562
+ const code = numberField(record, "code");
563
+ addDiagnostic(diagnostics, input, {
564
+ file: stringField(record, "file"),
565
+ line: numberField(record, "line"),
566
+ column: numberField(record, "column"),
567
+ endLine: numberField(record, "endLine"),
568
+ endColumn: numberField(record, "endColumn"),
569
+ code: code === undefined ? undefined : `SC${code}`,
570
+ severity: stringField(record, "level"),
571
+ message: stringField(record, "message"),
572
+ suggestion: record?.fix ? "automatic fix available" : undefined,
573
+ });
574
+ }
575
+ }
576
+ return diagnostics;
577
+ }
578
+
579
+ function parseHlint(input: CleanseParserInput): CleanseDiagnostic[] {
580
+ const diagnostics: CleanseDiagnostic[] = [];
581
+ for (const root of allJsonValues(input)) {
582
+ for (const value of toArray(root)) {
583
+ const record = toRecord(value);
584
+ addDiagnostic(diagnostics, input, {
585
+ file: stringField(record, "file"),
586
+ line: numberField(record, "startLine"),
587
+ column: numberField(record, "startColumn"),
588
+ endLine: numberField(record, "endLine"),
589
+ endColumn: numberField(record, "endColumn"),
590
+ code: stringField(record, "hint"),
591
+ severity: stringField(record, "severity"),
592
+ message: stringField(record, "hint") ?? stringField(record, "from"),
593
+ suggestion: stringField(record, "to"),
594
+ });
595
+ }
596
+ }
597
+ return diagnostics;
598
+ }
599
+
600
+ function parseTerraform(input: CleanseParserInput): CleanseDiagnostic[] {
601
+ const diagnostics: CleanseDiagnostic[] = [];
602
+ for (const rootValue of allJsonValues(input)) {
603
+ const root = toRecord(rootValue);
604
+ for (const value of toArray(root?.diagnostics)) {
605
+ const record = toRecord(value);
606
+ const range = nestedRecord(record, "range");
607
+ const start = nestedRecord(range, "start");
608
+ const end = nestedRecord(range, "end");
609
+ const summary = stringField(record, "summary");
610
+ const detail = stringField(record, "detail");
611
+ addDiagnostic(diagnostics, input, {
612
+ file: stringField(range, "filename"),
613
+ line: numberField(start, "line"),
614
+ column: numberField(start, "column"),
615
+ endLine: numberField(end, "line"),
616
+ endColumn: numberField(end, "column"),
617
+ severity: stringField(record, "severity"),
618
+ message: [summary, detail].filter(Boolean).join(": "),
619
+ });
620
+ }
621
+ }
622
+ return diagnostics;
623
+ }
624
+
625
+ function parseTflint(input: CleanseParserInput): CleanseDiagnostic[] {
626
+ const diagnostics: CleanseDiagnostic[] = [];
627
+ for (const rootValue of allJsonValues(input)) {
628
+ const root = toRecord(rootValue);
629
+ for (const value of toArray(root?.issues)) {
630
+ const record = toRecord(value);
631
+ const rule = nestedRecord(record, "rule");
632
+ const range = nestedRecord(record, "range");
633
+ const start = nestedRecord(range, "start");
634
+ const end = nestedRecord(range, "end");
635
+ addDiagnostic(diagnostics, input, {
636
+ file: stringField(range, "filename"),
637
+ line: numberField(start, "line"),
638
+ column: numberField(start, "column"),
639
+ endLine: numberField(end, "line"),
640
+ endColumn: numberField(end, "column"),
641
+ code: stringField(rule, "name"),
642
+ severity: stringField(rule, "severity"),
643
+ message: stringField(record, "message"),
644
+ });
645
+ }
646
+ for (const errorValue of toArray(root?.errors)) {
647
+ const error = toRecord(errorValue);
648
+ addDiagnostic(diagnostics, input, {
649
+ severity: "error",
650
+ message: stringField(error, "message"),
651
+ });
652
+ }
653
+ }
654
+ return diagnostics;
655
+ }
656
+
657
+ function parseGeneric(input: CleanseParserInput): CleanseDiagnostic[] {
658
+ const diagnostics: CleanseDiagnostic[] = [];
659
+ const text = sanitizeText(`${input.stdout}\n${input.stderr}`);
660
+ for (const line of text.split("\n")) {
661
+ const trimmed = line.trim();
662
+ if (!trimmed) continue;
663
+ const msvc =
664
+ /^(.*?)\((\d+),(\d+)\):\s*(error|warning|info)(?:\s+([A-Za-z]+\d+))?:\s*(.*?)(?:\s+\[[^\]]+\])?$/i.exec(
665
+ trimmed,
666
+ );
667
+ if (msvc) {
668
+ addDiagnostic(diagnostics, input, {
669
+ file: msvc[1],
670
+ line: Number.parseInt(msvc[2], 10),
671
+ column: Number.parseInt(msvc[3], 10),
672
+ severity: msvc[4],
673
+ code: msvc[5],
674
+ message: msvc[6],
675
+ });
676
+ continue;
677
+ }
678
+ const gcc = /^(.*?):(\d+)(?::(\d+))?:\s*(error|warning|info|note)(?:\s*\[([^\]]+)\])?:\s*(.*)$/i.exec(trimmed);
679
+ if (!gcc) continue;
680
+ addDiagnostic(diagnostics, input, {
681
+ file: gcc[1],
682
+ line: Number.parseInt(gcc[2], 10),
683
+ column: gcc[3] ? Number.parseInt(gcc[3], 10) : undefined,
684
+ severity: gcc[4],
685
+ code: gcc[5],
686
+ message: gcc[6],
687
+ });
688
+ }
689
+ return diagnostics;
690
+ }
691
+
692
+ function parseLocation(value: string): ParsedLocation | undefined {
693
+ const parenthesized = /^(.*?)\((\d+),(\d+)\)$/.exec(value.trim());
694
+ if (parenthesized) {
695
+ return {
696
+ file: parenthesized[1],
697
+ line: Number.parseInt(parenthesized[2], 10),
698
+ column: Number.parseInt(parenthesized[3], 10),
699
+ };
700
+ }
701
+ const colon = /^(.*?):(\d+)(?::(\d+))?$/.exec(value.trim());
702
+ if (!colon) return undefined;
703
+ return {
704
+ file: colon[1],
705
+ line: Number.parseInt(colon[2], 10),
706
+ column: colon[3] ? Number.parseInt(colon[3], 10) : undefined,
707
+ };
708
+ }
709
+
710
+ function deduplicateDiagnostics(diagnostics: CleanseDiagnostic[]): CleanseDiagnostic[] {
711
+ const seen = new Set<string>();
712
+ const unique: CleanseDiagnostic[] = [];
713
+ for (const diagnostic of diagnostics) {
714
+ const key = [
715
+ diagnostic.file ?? "",
716
+ diagnostic.line ?? "",
717
+ diagnostic.column ?? "",
718
+ diagnostic.code ?? "",
719
+ diagnostic.message,
720
+ ].join("\u0000");
721
+ if (seen.has(key)) continue;
722
+ seen.add(key);
723
+ unique.push(diagnostic);
724
+ }
725
+ return unique;
726
+ }