@ilya-lesikov/pi-pi 0.1.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 (271) hide show
  1. package/3p/pi-ask-user/index.ts +1835 -0
  2. package/3p/pi-ask-user/package.json +50 -0
  3. package/3p/pi-ask-user/single-select-layout.ts +203 -0
  4. package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
  5. package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
  6. package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
  7. package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
  8. package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
  9. package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
  10. package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
  11. package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
  12. package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
  13. package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
  14. package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
  15. package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
  16. package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
  17. package/3p/pi-lsp/package.json +54 -0
  18. package/3p/pi-plannotator/AGENTS.md +583 -0
  19. package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
  20. package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
  21. package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
  22. package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
  23. package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
  24. package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
  25. package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
  26. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
  27. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
  28. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
  29. package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
  30. package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
  31. package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
  32. package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
  33. package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
  34. package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
  35. package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
  36. package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
  37. package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
  38. package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
  39. package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
  40. package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
  41. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
  42. package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
  43. package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
  44. package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
  45. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
  46. package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
  47. package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
  48. package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
  49. package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
  50. package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
  51. package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
  52. package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
  53. package/3p/pi-plannotator/package.json +53 -0
  54. package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
  55. package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
  56. package/3p/pi-plannotator/packages/ai/context.ts +250 -0
  57. package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
  58. package/3p/pi-plannotator/packages/ai/index.ts +105 -0
  59. package/3p/pi-plannotator/packages/ai/package.json +21 -0
  60. package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
  61. package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
  62. package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
  63. package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
  64. package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
  65. package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
  66. package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
  67. package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
  68. package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
  69. package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
  70. package/3p/pi-plannotator/packages/ai/types.ts +379 -0
  71. package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
  72. package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
  73. package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
  74. package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
  75. package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
  76. package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
  77. package/3p/pi-plannotator/packages/server/browser.ts +245 -0
  78. package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
  79. package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
  80. package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
  81. package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
  82. package/3p/pi-plannotator/packages/server/config.ts +8 -0
  83. package/3p/pi-plannotator/packages/server/draft.ts +1 -0
  84. package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
  85. package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
  86. package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
  87. package/3p/pi-plannotator/packages/server/git.ts +141 -0
  88. package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
  89. package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
  90. package/3p/pi-plannotator/packages/server/ide.ts +43 -0
  91. package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
  92. package/3p/pi-plannotator/packages/server/image.ts +66 -0
  93. package/3p/pi-plannotator/packages/server/index.ts +648 -0
  94. package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
  95. package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
  96. package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
  97. package/3p/pi-plannotator/packages/server/jj.ts +86 -0
  98. package/3p/pi-plannotator/packages/server/p4.ts +417 -0
  99. package/3p/pi-plannotator/packages/server/package.json +40 -0
  100. package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
  101. package/3p/pi-plannotator/packages/server/pr.ts +146 -0
  102. package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
  103. package/3p/pi-plannotator/packages/server/project.ts +45 -0
  104. package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
  105. package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
  106. package/3p/pi-plannotator/packages/server/remote.ts +74 -0
  107. package/3p/pi-plannotator/packages/server/repo.ts +80 -0
  108. package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
  109. package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
  110. package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
  111. package/3p/pi-plannotator/packages/server/review.ts +1318 -0
  112. package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
  113. package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
  114. package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
  115. package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
  116. package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
  117. package/3p/pi-plannotator/packages/server/storage.ts +17 -0
  118. package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
  119. package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
  120. package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
  121. package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
  122. package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
  123. package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
  124. package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
  125. package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
  126. package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
  127. package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
  128. package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
  129. package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
  130. package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
  131. package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
  132. package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
  133. package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
  134. package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
  135. package/3p/pi-plannotator/packages/shared/config.ts +262 -0
  136. package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
  137. package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
  138. package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
  139. package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
  140. package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
  141. package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
  142. package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
  143. package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
  144. package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
  145. package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
  146. package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
  147. package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
  148. package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
  149. package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
  150. package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
  151. package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
  152. package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
  153. package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
  154. package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
  155. package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
  156. package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
  157. package/3p/pi-plannotator/packages/shared/package.json +55 -0
  158. package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
  159. package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
  160. package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
  161. package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
  162. package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
  163. package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
  164. package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
  165. package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
  166. package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
  167. package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
  168. package/3p/pi-plannotator/packages/shared/project.ts +71 -0
  169. package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
  170. package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
  171. package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
  172. package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
  173. package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
  174. package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
  175. package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
  176. package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
  177. package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
  178. package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
  179. package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
  180. package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
  181. package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
  182. package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
  183. package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
  184. package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
  185. package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
  186. package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
  187. package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
  188. package/3p/pi-plannotator/packages/shared/types.ts +29 -0
  189. package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
  190. package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
  191. package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
  192. package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
  193. package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
  194. package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
  195. package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
  196. package/3p/pi-subagents/package.json +50 -0
  197. package/3p/pi-subagents/src/agent-manager.ts +413 -0
  198. package/3p/pi-subagents/src/agent-runner.ts +502 -0
  199. package/3p/pi-subagents/src/agent-types.ts +248 -0
  200. package/3p/pi-subagents/src/context.ts +58 -0
  201. package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
  202. package/3p/pi-subagents/src/custom-agents.ts +137 -0
  203. package/3p/pi-subagents/src/default-agents.ts +144 -0
  204. package/3p/pi-subagents/src/env.ts +33 -0
  205. package/3p/pi-subagents/src/group-join.ts +141 -0
  206. package/3p/pi-subagents/src/index.ts +1811 -0
  207. package/3p/pi-subagents/src/invocation-config.ts +40 -0
  208. package/3p/pi-subagents/src/memory.ts +165 -0
  209. package/3p/pi-subagents/src/model-resolver.ts +81 -0
  210. package/3p/pi-subagents/src/output-file.ts +77 -0
  211. package/3p/pi-subagents/src/prompts.ts +85 -0
  212. package/3p/pi-subagents/src/skill-loader.ts +79 -0
  213. package/3p/pi-subagents/src/types.ts +111 -0
  214. package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
  215. package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
  216. package/3p/pi-subagents/src/worktree.ts +162 -0
  217. package/3p/pi-tasks/package.json +52 -0
  218. package/3p/pi-tasks/src/auto-clear.ts +91 -0
  219. package/3p/pi-tasks/src/index.ts +1150 -0
  220. package/3p/pi-tasks/src/process-tracker.ts +140 -0
  221. package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
  222. package/3p/pi-tasks/src/task-store.ts +324 -0
  223. package/3p/pi-tasks/src/tasks-config.ts +27 -0
  224. package/3p/pi-tasks/src/types.ts +40 -0
  225. package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
  226. package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
  227. package/AGENTS.md +28 -0
  228. package/LICENSE +201 -0
  229. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
  230. package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
  231. package/extensions/orchestrator/agents/explore.ts +33 -0
  232. package/extensions/orchestrator/agents/librarian.ts +43 -0
  233. package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
  234. package/extensions/orchestrator/agents/planner.ts +61 -0
  235. package/extensions/orchestrator/agents/registry.ts +156 -0
  236. package/extensions/orchestrator/agents/task.ts +50 -0
  237. package/extensions/orchestrator/agents/tool-routing.ts +84 -0
  238. package/extensions/orchestrator/ast-search.ts +85 -0
  239. package/extensions/orchestrator/cbm.ts +330 -0
  240. package/extensions/orchestrator/command-handlers.test.ts +163 -0
  241. package/extensions/orchestrator/command-handlers.ts +116 -0
  242. package/extensions/orchestrator/commands.test.ts +81 -0
  243. package/extensions/orchestrator/commands.ts +75 -0
  244. package/extensions/orchestrator/config.test.ts +221 -0
  245. package/extensions/orchestrator/config.ts +230 -0
  246. package/extensions/orchestrator/context.test.ts +293 -0
  247. package/extensions/orchestrator/context.ts +206 -0
  248. package/extensions/orchestrator/event-handlers.test.ts +190 -0
  249. package/extensions/orchestrator/event-handlers.ts +1416 -0
  250. package/extensions/orchestrator/exa.ts +104 -0
  251. package/extensions/orchestrator/flant-infra.ts +486 -0
  252. package/extensions/orchestrator/index.ts +80 -0
  253. package/extensions/orchestrator/integration.test.ts +1214 -0
  254. package/extensions/orchestrator/orchestrator.test.ts +252 -0
  255. package/extensions/orchestrator/orchestrator.ts +538 -0
  256. package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
  257. package/extensions/orchestrator/phases/brainstorm.ts +273 -0
  258. package/extensions/orchestrator/phases/implementation.ts +40 -0
  259. package/extensions/orchestrator/phases/machine.test.ts +293 -0
  260. package/extensions/orchestrator/phases/machine.ts +209 -0
  261. package/extensions/orchestrator/phases/planning.ts +255 -0
  262. package/extensions/orchestrator/phases/review.ts +193 -0
  263. package/extensions/orchestrator/plannotator.ts +56 -0
  264. package/extensions/orchestrator/pp-menu.ts +866 -0
  265. package/extensions/orchestrator/state.test.ts +343 -0
  266. package/extensions/orchestrator/state.ts +237 -0
  267. package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
  268. package/extensions/orchestrator/validate-artifacts.ts +272 -0
  269. package/extensions/orchestrator/vendor.d.ts +26 -0
  270. package/package.json +73 -0
  271. package/scripts/postinstall.sh +18 -0
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Cross-platform browser opening utility
3
+ */
4
+
5
+ import { $ } from "bun";
6
+ import { spawn } from "node:child_process";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import fs from "node:fs";
10
+ import { getPlannotatorDataDir } from "@plannotator/shared/data-dir";
11
+ import { loadConfig, resolveUseGlimpse } from "@plannotator/shared/config";
12
+
13
+ const IPC_REGISTRY = path.join(getPlannotatorDataDir(), "vscode-ipc.json");
14
+
15
+ /**
16
+ * Common "no-op" values for $BROWSER used by headless/background environments
17
+ * (e.g. Claude Code's agent view sets BROWSER=true) to signal "do not actually
18
+ * launch a browser". Treating these as if the variable were unset prevents
19
+ * silently shelling out to e.g. `true <url>`, which exits 0 without opening
20
+ * anything and leaves the Plannotator server hanging on waitForDecision().
21
+ */
22
+ const NOOP_BROWSER_VALUES = new Set(["true", "false", "none", ":", "0", "1"]);
23
+
24
+ export function isNoOpBrowserSentinel(value: string | undefined): boolean {
25
+ if (!value) return false;
26
+ return NOOP_BROWSER_VALUES.has(value.trim().toLowerCase());
27
+ }
28
+
29
+ /**
30
+ * Try opening URL via VS Code extension IPC registry.
31
+ * Falls back when env vars (PLANNOTATOR_BROWSER) aren't available to the process.
32
+ */
33
+ async function tryVscodeIpc(url: string): Promise<boolean> {
34
+ try {
35
+ const registry: Record<string, number> = JSON.parse(
36
+ fs.readFileSync(IPC_REGISTRY, "utf-8"),
37
+ );
38
+ const cwd = process.cwd();
39
+ // Find the best matching workspace (longest prefix match)
40
+ let bestMatch = "";
41
+ let bestPort = 0;
42
+ for (const [workspace, port] of Object.entries(registry)) {
43
+ if (cwd.startsWith(workspace) && workspace.length > bestMatch.length) {
44
+ bestMatch = workspace;
45
+ bestPort = port;
46
+ }
47
+ }
48
+ if (!bestPort) return false;
49
+ const ipcUrl = new URL("/open", `http://127.0.0.1:${bestPort}`);
50
+ ipcUrl.searchParams.set("url", url);
51
+ const resp = await fetch(ipcUrl.toString());
52
+ return resp.ok;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Check if running in WSL (Windows Subsystem for Linux)
60
+ */
61
+ export async function isWSL(): Promise<boolean> {
62
+ if (process.platform !== "linux") {
63
+ return false;
64
+ }
65
+
66
+ if (os.release().toLowerCase().includes("microsoft")) {
67
+ return true;
68
+ }
69
+
70
+ // Fallback: check /proc/version for WSL signature (if available)
71
+ try {
72
+ const file = Bun.file("/proc/version");
73
+ if (await file.exists()) {
74
+ const content = await file.text();
75
+ return (
76
+ content.toLowerCase().includes("wsl") ||
77
+ content.toLowerCase().includes("microsoft")
78
+ );
79
+ }
80
+ } catch {
81
+ // Ignore errors reading /proc/version
82
+ }
83
+ return false;
84
+ }
85
+
86
+ /**
87
+ * Open a URL in the browser
88
+ *
89
+ * Uses PLANNOTATOR_BROWSER env var if set, otherwise uses system default.
90
+ * - macOS: Set to app name ("Google Chrome") or path ("/Applications/Firefox.app")
91
+ * - Linux/Windows/WSL: Set to executable path ("/usr/bin/firefox")
92
+ *
93
+ * Fails silently if browser can't be opened
94
+ */
95
+ export function shouldTryRemoteBrowserFallback(isRemote: boolean): boolean {
96
+ if (!isRemote) return false;
97
+ const plannotatorBrowser = process.env.PLANNOTATOR_BROWSER;
98
+ const browser = process.env.BROWSER;
99
+ // Treat headless sentinels (e.g. BROWSER=true from Claude Code's agent view)
100
+ // as if no real browser handler were configured, so the IPC fallback still runs.
101
+ const hasRealHandler =
102
+ (plannotatorBrowser && !isNoOpBrowserSentinel(plannotatorBrowser)) ||
103
+ (browser && !isNoOpBrowserSentinel(browser));
104
+ return !hasRealHandler;
105
+ }
106
+
107
+ function buildGlimpseHtml(url: string): string {
108
+ const encodedUrl = JSON.stringify(url);
109
+ return `<!doctype html>
110
+ <html>
111
+ <head>
112
+ <meta charset="utf-8" />
113
+ <title>Plannotator</title>
114
+ <style>
115
+ html, body { width: 100%; height: 100%; margin: 0; }
116
+ body { overflow: hidden; background: #0f1115; }
117
+ </style>
118
+ </head>
119
+ <body>
120
+ <script>
121
+ location.replace(${encodedUrl});
122
+ </script>
123
+ </body>
124
+ </html>`;
125
+ }
126
+
127
+ async function openGlimpse(url: string): Promise<boolean> {
128
+ const glimpseCli = Bun.which("glimpseui");
129
+ if (!glimpseCli) return false;
130
+
131
+ const args = [
132
+ "--width",
133
+ String(Number(process.env.PLANNOTATOR_GLIMPSE_WIDTH || 1280)),
134
+ "--height",
135
+ String(Number(process.env.PLANNOTATOR_GLIMPSE_HEIGHT || 900)),
136
+ "--title",
137
+ "Plannotator",
138
+ "--open-links",
139
+ ];
140
+ const html = buildGlimpseHtml(url);
141
+
142
+ // On Windows, `glimpseui` resolves to an npm script shim, not an exe, which
143
+ // spawn() can't launch without a shell. `shell: true` would break the stdin
144
+ // HTML pipe below, so run the package entry with node directly instead.
145
+ let command = glimpseCli;
146
+ let spawnArgs = args;
147
+ if (process.platform === "win32" && !/\.exe$/i.test(glimpseCli)) {
148
+ const node = Bun.which("node");
149
+ const entry = path.join(
150
+ path.dirname(glimpseCli),
151
+ "node_modules",
152
+ "glimpseui",
153
+ "bin",
154
+ "glimpse.mjs"
155
+ );
156
+ if (node && fs.existsSync(entry)) {
157
+ command = node;
158
+ spawnArgs = [entry, ...args];
159
+ }
160
+ }
161
+
162
+ return await new Promise<boolean>((resolve) => {
163
+ let settled = false;
164
+ let successTimer: ReturnType<typeof setTimeout> | undefined;
165
+ const finish = (opened: boolean) => {
166
+ if (settled) return;
167
+ settled = true;
168
+ if (successTimer) clearTimeout(successTimer);
169
+ resolve(opened);
170
+ };
171
+
172
+ const child = spawn(command, spawnArgs, {
173
+ detached: true,
174
+ stdio: ["pipe", "ignore", "ignore"],
175
+ });
176
+ successTimer = setTimeout(() => {
177
+ child.unref();
178
+ finish(true);
179
+ }, 750);
180
+
181
+ child.once("error", () => finish(false));
182
+ child.once("exit", () => finish(false));
183
+ child.stdin.once("error", () => finish(false));
184
+ child.stdin.end(html);
185
+ });
186
+ }
187
+
188
+ export async function openBrowser(
189
+ url: string,
190
+ options?: { isRemote?: boolean; useGlimpse?: boolean }
191
+ ): Promise<boolean> {
192
+ try {
193
+ const rawPlannotatorBrowser = process.env.PLANNOTATOR_BROWSER;
194
+ const rawBrowser = process.env.BROWSER;
195
+ const plannotatorBrowser = isNoOpBrowserSentinel(rawPlannotatorBrowser)
196
+ ? undefined
197
+ : rawPlannotatorBrowser;
198
+ const envBrowser = isNoOpBrowserSentinel(rawBrowser) ? undefined : rawBrowser;
199
+ const browser = plannotatorBrowser || envBrowser;
200
+ const isRemote = options?.isRemote ?? false;
201
+ if (shouldTryRemoteBrowserFallback(isRemote)) {
202
+ const openedViaIpc = await tryVscodeIpc(url);
203
+ if (openedViaIpc) {
204
+ return true;
205
+ }
206
+ }
207
+
208
+ if (options?.useGlimpse && !browser && !isRemote && resolveUseGlimpse(loadConfig())) {
209
+ const openedViaGlimpse = await openGlimpse(url);
210
+ if (openedViaGlimpse) {
211
+ return true;
212
+ }
213
+ }
214
+
215
+ const platform = process.platform;
216
+ const wsl = await isWSL();
217
+
218
+ if (browser) {
219
+ if (plannotatorBrowser && platform === "darwin") {
220
+ if (plannotatorBrowser.includes("/") && !plannotatorBrowser.endsWith(".app")) {
221
+ await $`${plannotatorBrowser} ${url}`.quiet();
222
+ } else {
223
+ await $`open -a ${plannotatorBrowser} ${url}`.quiet();
224
+ }
225
+ } else if ((platform === "win32" || wsl) && plannotatorBrowser) {
226
+ await $`cmd.exe /c start "" ${plannotatorBrowser} ${url}`.quiet();
227
+ } else {
228
+ await $`${browser} ${url}`.quiet();
229
+ }
230
+ } else {
231
+ // Default system browser
232
+ if (platform === "win32" || wsl) {
233
+ await $`cmd.exe /c start ${url}`.quiet();
234
+ } else if (platform === "darwin") {
235
+ await $`open ${url}`.quiet();
236
+ } else {
237
+ await $`xdg-open ${url}`.quiet();
238
+ }
239
+ }
240
+ return true;
241
+ } catch {
242
+ // Shell-based open failed — try VS Code IPC registry as fallback
243
+ return tryVscodeIpc(url);
244
+ }
245
+ }
@@ -0,0 +1,352 @@
1
+ import { toRelativePath } from "./path-utils";
2
+
3
+ /**
4
+ * Claude Code Review Agent — prompt, command builder, and JSONL output parser.
5
+ *
6
+ * Claude has its own review model (severity-based findings with reasoning traces)
7
+ * separate from Codex's priority-based model. The transform layer normalizes
8
+ * both into the shared annotation format.
9
+ *
10
+ * Claude uses --json-schema (inline JSON + Ajv validation with retries) and
11
+ * --output-format stream-json for live JSONL streaming. The final event is
12
+ * type:"result" with structured_output containing validated findings.
13
+ */
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export type ClaudeSeverity = "important" | "nit" | "pre_existing";
20
+
21
+ export interface ClaudeFinding {
22
+ severity: ClaudeSeverity;
23
+ file: string;
24
+ line: number;
25
+ end_line: number;
26
+ description: string;
27
+ reasoning: string;
28
+ }
29
+
30
+ export interface ClaudeReviewOutput {
31
+ findings: ClaudeFinding[];
32
+ summary: {
33
+ important: number;
34
+ nit: number;
35
+ pre_existing: number;
36
+ };
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Schema — Claude's own severity-based model
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export const CLAUDE_REVIEW_SCHEMA_JSON = JSON.stringify({
44
+ type: "object",
45
+ properties: {
46
+ findings: {
47
+ type: "array",
48
+ items: {
49
+ type: "object",
50
+ properties: {
51
+ severity: { type: "string", enum: ["important", "nit", "pre_existing"] },
52
+ file: { type: "string" },
53
+ line: { type: "integer" },
54
+ end_line: { type: "integer" },
55
+ description: { type: "string" },
56
+ reasoning: { type: "string" },
57
+ },
58
+ required: ["severity", "file", "line", "end_line", "description", "reasoning"],
59
+ additionalProperties: false,
60
+ },
61
+ },
62
+ summary: {
63
+ type: "object",
64
+ properties: {
65
+ important: { type: "integer" },
66
+ nit: { type: "integer" },
67
+ pre_existing: { type: "integer" },
68
+ },
69
+ required: ["important", "nit", "pre_existing"],
70
+ additionalProperties: false,
71
+ },
72
+ },
73
+ required: ["findings", "summary"],
74
+ additionalProperties: false,
75
+ });
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Review prompt — converges open-source Claude Code review + remote service
79
+ // ---------------------------------------------------------------------------
80
+
81
+ export const CLAUDE_REVIEW_PROMPT = `# Claude Code Review System Prompt
82
+
83
+ ## Identity
84
+ You are a code review system. Your job is to find bugs that would break
85
+ production. You are not a linter, formatter, or style checker unless
86
+ project guidance files explicitly expand your scope.
87
+
88
+ ## Pipeline
89
+
90
+ Step 1: Gather context
91
+ - Retrieve the PR diff or local diff (gh pr diff, git diff, or jj diff)
92
+ - Read CLAUDE.md and REVIEW.md at the repo root and in every directory
93
+ containing modified files
94
+ - Build a map of which rules apply to which file paths
95
+ - Identify any skip rules (paths, patterns, or file types to ignore)
96
+
97
+ Step 2: Launch 4 parallel review agents
98
+
99
+ Agent 1 — Bug + Regression (Opus-level reasoning)
100
+ Scan for logic errors, regressions, broken edge cases, build failures,
101
+ and code that will produce wrong results. Focus on the diff but read
102
+ surrounding code to understand call sites and data flow. Flag only
103
+ issues where the code is demonstrably wrong — not stylistic concerns,
104
+ not missing tests, not "could be cleaner."
105
+
106
+ Agent 2 — Security + Deep Analysis (Opus-level reasoning)
107
+ Look for security vulnerabilities with concrete exploit paths, race
108
+ conditions, incorrect assumptions about trust boundaries, and subtle
109
+ issues in introduced code. Read surrounding code for context. Do not
110
+ flag theoretical risks without a plausible path to harm.
111
+
112
+ Agent 3 — Code Quality + Reusability (Sonnet-level reasoning)
113
+ Look for code smells, unnecessary duplication, missed opportunities to
114
+ reuse existing utilities or patterns in the codebase, overly complex
115
+ implementations that could be simpler, and elegance issues. Read the
116
+ surrounding codebase to understand existing patterns before flagging.
117
+ Only flag issues a senior engineer would care about.
118
+
119
+ Agent 4 — Guideline Compliance (Haiku-level reasoning)
120
+ Audit changes against rules from CLAUDE.md and REVIEW.md gathered in
121
+ Step 1. Only flag clear, unambiguous violations where you can cite the
122
+ exact rule broken. If a PR makes a CLAUDE.md statement outdated, flag
123
+ that the docs need updating. Respect all skip rules — never flag files
124
+ or patterns that guidance says to ignore.
125
+
126
+ All agents:
127
+ - Do not duplicate each other's findings
128
+ - Do not flag issues in paths excluded by guidance files
129
+ - Provide file, line number, and a concise description for each candidate
130
+
131
+ Step 3: Validate each candidate finding
132
+ For each candidate, launch a validation agent. The validator:
133
+ - Traces the actual code path to confirm the issue is real
134
+ - Checks whether the issue is handled elsewhere (try/catch, upstream
135
+ guard, fallback logic, type system guarantees)
136
+ - Confirms the finding is not a false positive with high confidence
137
+ - If validation fails, drop the finding silently
138
+ - If validation passes, write a clear reasoning chain explaining how
139
+ the issue was confirmed — this becomes the \`reasoning\` field
140
+
141
+ Step 4: Classify each validated finding
142
+ Assign exactly one severity:
143
+
144
+ important — A bug that should be fixed before merging. Build failures,
145
+ clear logic errors, security vulnerabilities with exploit paths, data
146
+ loss risks, race conditions with observable consequences.
147
+
148
+ nit — A minor issue worth fixing but non-blocking. Style deviations
149
+ from project guidelines, code quality concerns, edge cases that are
150
+ unlikely but worth noting, convention violations that don't affect
151
+ correctness.
152
+
153
+ pre_existing — A bug that exists in the surrounding codebase but was
154
+ NOT introduced by this PR. Only flag when directly relevant to the
155
+ changed code path.
156
+
157
+ Step 5: Deduplicate and rank
158
+ - Merge findings that describe the same underlying issue from different
159
+ agents — keep the most specific description and the highest severity
160
+ - Sort by severity: important → nit → pre_existing
161
+ - Within each severity, sort by file path and line number
162
+
163
+ Step 6: Return structured JSON output matching the schema.
164
+ If no issues are found, return an empty findings array with zeroed summary.
165
+
166
+ ## Hard constraints
167
+ - Never approve or block the PR
168
+ - Never comment on formatting or code style unless guidance files say to
169
+ - Never flag missing test coverage unless guidance files say to
170
+ - Never invent rules — only enforce what CLAUDE.md or REVIEW.md state
171
+ - Never flag issues in skipped paths or generated files unless guidance
172
+ explicitly includes them
173
+ - Prefer silence over false positives — when in doubt, drop the finding
174
+ - Do NOT post any comments to GitHub or GitLab
175
+ - Do NOT use gh pr comment or any commenting tool
176
+ - Your only output is the structured JSON findings`;
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Command builder
180
+ // ---------------------------------------------------------------------------
181
+
182
+ export interface ClaudeCommandResult {
183
+ command: string[];
184
+ /** Prompt text to write to stdin (Claude reads prompt from stdin, not argv). */
185
+ stdinPrompt: string;
186
+ }
187
+
188
+ /**
189
+ * Build the `claude -p` command. Prompt is passed via stdin, not as a
190
+ * positional arg — avoids quoting issues, argv limits, and variadic flag conflicts.
191
+ */
192
+ export function buildClaudeCommand(prompt: string, model: string = "claude-opus-4-7", effort?: string): ClaudeCommandResult {
193
+ const allowedTools = [
194
+ "Agent", "Read", "Glob", "Grep",
195
+ // GitHub CLI
196
+ "Bash(gh pr view:*)", "Bash(gh pr diff:*)", "Bash(gh pr list:*)",
197
+ "Bash(gh issue view:*)", "Bash(gh issue list:*)",
198
+ "Bash(gh api repos/*/*/pulls/*)", "Bash(gh api repos/*/*/pulls/*/files*)",
199
+ "Bash(gh api repos/*/*/pulls/*/comments*)", "Bash(gh api repos/*/*/issues/*/comments*)",
200
+ // GitLab CLI
201
+ "Bash(glab mr view:*)", "Bash(glab mr diff:*)", "Bash(glab mr list:*)",
202
+ "Bash(glab api:*)",
203
+ // Git (read-only)
204
+ "Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)",
205
+ "Bash(git show:*)", "Bash(git blame:*)", "Bash(git branch:*)",
206
+ "Bash(git grep:*)", "Bash(git ls-remote:*)", "Bash(git ls-tree:*)",
207
+ "Bash(git merge-base:*)", "Bash(git remote:*)", "Bash(git rev-parse:*)",
208
+ "Bash(git show-ref:*)", "Bash(git -C:*)",
209
+ // JJ (read-only)
210
+ "Bash(jj status:*)", "Bash(jj diff:*)", "Bash(jj log:*)",
211
+ "Bash(jj show:*)", "Bash(jj file show:*)", "Bash(jj cat:*)",
212
+ "Bash(jj bookmark list:*)",
213
+ "Bash(wc:*)",
214
+ ].join(",");
215
+
216
+ const disallowedTools = [
217
+ "Edit", "Write", "NotebookEdit", "WebFetch", "WebSearch",
218
+ "Bash(python:*)", "Bash(python3:*)", "Bash(node:*)", "Bash(npx:*)",
219
+ "Bash(bun:*)", "Bash(bunx:*)", "Bash(sh:*)", "Bash(bash:*)", "Bash(zsh:*)",
220
+ "Bash(curl:*)", "Bash(wget:*)",
221
+ ].join(",");
222
+
223
+ return {
224
+ command: [
225
+ "claude", "-p",
226
+ "--permission-mode", "dontAsk",
227
+ "--output-format", "stream-json",
228
+ "--verbose",
229
+ "--json-schema", CLAUDE_REVIEW_SCHEMA_JSON,
230
+ "--no-session-persistence",
231
+ "--model", model,
232
+ ...(effort ? ["--effort", effort] : []),
233
+ "--tools", "Agent,Bash,Read,Glob,Grep",
234
+ "--allowedTools", allowedTools,
235
+ "--disallowedTools", disallowedTools,
236
+ ],
237
+ stdinPrompt: prompt,
238
+ };
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // JSONL stream output parser
243
+ // ---------------------------------------------------------------------------
244
+
245
+ /**
246
+ * Parse Claude Code's stream-json output (JSONL).
247
+ * Extracts structured_output from the final type:"result" event.
248
+ */
249
+ export function parseClaudeStreamOutput(stdout: string): ClaudeReviewOutput | null {
250
+ if (!stdout.trim()) return null;
251
+
252
+ const lines = stdout.trim().split('\n');
253
+ for (let i = lines.length - 1; i >= 0; i--) {
254
+ const line = lines[i].trim();
255
+ if (!line) continue;
256
+
257
+ try {
258
+ const event = JSON.parse(line);
259
+
260
+ if (event.type === 'result') {
261
+ if (event.is_error) return null;
262
+
263
+ const output = event.structured_output;
264
+ if (!output || !Array.isArray(output.findings)) return null;
265
+
266
+ return output as ClaudeReviewOutput;
267
+ }
268
+ } catch {
269
+ // Not valid JSON — skip
270
+ }
271
+ }
272
+
273
+ return null;
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Finding transform — Claude findings → external annotations
278
+ // ---------------------------------------------------------------------------
279
+
280
+ /** Transform Claude findings into the external annotation format. */
281
+ export function transformClaudeFindings(
282
+ findings: ClaudeFinding[],
283
+ source: string,
284
+ cwd?: string,
285
+ pathTransform?: (path: string) => string,
286
+ ): Array<{
287
+ source: string;
288
+ filePath: string;
289
+ lineStart: number;
290
+ lineEnd: number;
291
+ type: string;
292
+ side: string;
293
+ scope: string;
294
+ text: string;
295
+ severity: ClaudeSeverity;
296
+ reasoning: string;
297
+ author: string;
298
+ }> {
299
+ return findings
300
+ .filter(f => f.file && typeof f.line === "number")
301
+ .map(f => ({
302
+ source,
303
+ filePath: pathTransform
304
+ ? pathTransform(toRelativePath(f.file, cwd))
305
+ : toRelativePath(f.file, cwd),
306
+ lineStart: f.line,
307
+ lineEnd: f.end_line ?? f.line,
308
+ type: "comment",
309
+ side: "new",
310
+ scope: "line",
311
+ text: `[${f.severity}] ${f.description}`,
312
+ severity: f.severity,
313
+ reasoning: f.reasoning,
314
+ author: "Claude Code",
315
+ }));
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Live log formatter
320
+ // ---------------------------------------------------------------------------
321
+
322
+ /**
323
+ * Extract log-worthy content from a JSONL line for the LiveLogViewer.
324
+ * Returns a human-readable string, or null if the line should be skipped.
325
+ */
326
+ export function formatClaudeLogEvent(line: string): string | null {
327
+ try {
328
+ const event = JSON.parse(line);
329
+
330
+ // Skip the final result event — handled separately
331
+ if (event.type === 'result') return null;
332
+
333
+ // Assistant messages (the agent's thinking/responses)
334
+ if (event.type === 'assistant' && event.message?.content) {
335
+ const parts = Array.isArray(event.message.content) ? event.message.content : [event.message.content];
336
+ const texts = parts
337
+ .filter((p: any) => p.type === 'text' && p.text)
338
+ .map((p: any) => p.text);
339
+ if (texts.length > 0) return texts.join('\n');
340
+
341
+ // Tool use events (only reached if no text parts found)
342
+ const tools = parts.filter((p: any) => p.type === 'tool_use');
343
+ if (tools.length > 0) {
344
+ return tools.map((t: any) => `[${t.name}] ${typeof t.input === 'string' ? t.input.slice(0, 100) : JSON.stringify(t.input).slice(0, 100)}`).join('\n');
345
+ }
346
+ }
347
+
348
+ return null;
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Code navigation — Bun runtime adapter and request handler.
3
+ */
4
+
5
+ import {
6
+ type CodeNavRequest,
7
+ type CodeNavRuntime,
8
+ type CodeNavResponse,
9
+ resolveCodeNav,
10
+ validateCodeNavRequest,
11
+ extractChangedFiles,
12
+ } from "@plannotator/shared/code-nav";
13
+
14
+ export type { CodeNavRequest, CodeNavResponse };
15
+
16
+ const bunCodeNavRuntime: CodeNavRuntime = {
17
+ async runCommand(command, args, options) {
18
+ let proc;
19
+ try {
20
+ proc = Bun.spawn([command, ...args], {
21
+ cwd: options?.cwd,
22
+ stdout: "pipe",
23
+ stderr: "pipe",
24
+ });
25
+ } catch {
26
+ return { stdout: "", stderr: "command not found", exitCode: 1 };
27
+ }
28
+
29
+ let timer: ReturnType<typeof setTimeout> | undefined;
30
+ if (options?.timeoutMs) {
31
+ timer = setTimeout(() => proc.kill(), options.timeoutMs);
32
+ }
33
+
34
+ const [stdout, stderr, exitCode] = await Promise.all([
35
+ new Response(proc.stdout).text(),
36
+ new Response(proc.stderr).text(),
37
+ proc.exited,
38
+ ]);
39
+
40
+ if (timer) clearTimeout(timer);
41
+ return { stdout, stderr, exitCode };
42
+ },
43
+ };
44
+
45
+ export async function handleCodeNavResolve(
46
+ req: Request,
47
+ cwd: string,
48
+ changedFiles: string[],
49
+ ): Promise<Response> {
50
+ try {
51
+ const body = (await req.json()) as CodeNavRequest;
52
+ const error = validateCodeNavRequest(body);
53
+ if (error) {
54
+ return Response.json({ error }, { status: 400 });
55
+ }
56
+
57
+ const result = await resolveCodeNav(
58
+ bunCodeNavRuntime,
59
+ body,
60
+ cwd,
61
+ changedFiles,
62
+ );
63
+
64
+ return Response.json(result);
65
+ } catch (err) {
66
+ return Response.json(
67
+ { error: err instanceof Error ? err.message : "Code navigation failed" },
68
+ { status: 500 },
69
+ );
70
+ }
71
+ }
72
+
73
+ export { extractChangedFiles };