@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,199 @@
1
+ import { createServer } from "node:http";
2
+ import { dirname, resolve as resolvePath } from "node:path";
3
+
4
+ import { contentHash, deleteDraft } from "../generated/draft.js";
5
+ import { saveConfig, detectGitUser, getServerConfig } from "../generated/config.js";
6
+
7
+ import {
8
+ handleDraftRequest,
9
+ handleFavicon,
10
+ handleImageRequest,
11
+ handleUploadRequest,
12
+ } from "./handlers.js";
13
+ import { html, json, parseBody, requestUrl } from "./helpers.js";
14
+ import { createPiAIRuntime, handlePiAIRequest } from "./ai-runtime.js";
15
+
16
+ import { listenOnPort } from "./network.js";
17
+
18
+ import { getRepoInfo } from "./project.js";
19
+ import {
20
+ handleDocRequest,
21
+ handleDocExistsRequest,
22
+ handleFileBrowserRequest,
23
+ handleObsidianVaultsRequest,
24
+ handleObsidianFilesRequest,
25
+ handleObsidianDocRequest,
26
+ } from "./reference.js";
27
+ import { warmFileListCache } from "../generated/resolve-file.js";
28
+ import { createExternalAnnotationHandler } from "./external-annotations.js";
29
+
30
+ export interface AnnotateServerResult {
31
+ port: number;
32
+ portSource: "env" | "remote-default" | "random";
33
+ url: string;
34
+ waitForDecision: () => Promise<{ feedback: string; annotations: unknown[]; exit?: boolean; approved?: boolean; selectedMessageId?: string; feedbackScope?: "message" | "messages" }>;
35
+ stop: () => void;
36
+ }
37
+
38
+ export async function startAnnotateServer(options: {
39
+ markdown: string;
40
+ filePath: string;
41
+ htmlContent: string;
42
+ origin?: string;
43
+ mode?: string;
44
+ folderPath?: string;
45
+ recentMessages?: { messageId: string; text: string; timestamp?: string }[];
46
+ sharingEnabled?: boolean;
47
+ shareBaseUrl?: string;
48
+ pasteApiUrl?: string;
49
+ sourceInfo?: string;
50
+ sourceConverted?: boolean;
51
+ gate?: boolean;
52
+ rawHtml?: string;
53
+ renderHtml?: boolean;
54
+ }): Promise<AnnotateServerResult> {
55
+ // Side-channel pre-warm so /api/doc/exists POSTs land on warm cache.
56
+ void warmFileListCache(process.cwd(), "code");
57
+ const gitUser = detectGitUser();
58
+ const sharingEnabled =
59
+ options.sharingEnabled ?? process.env.PLANNOTATOR_SHARE !== "disabled";
60
+ const shareBaseUrl =
61
+ (options.shareBaseUrl ?? process.env.PLANNOTATOR_SHARE_URL) || undefined;
62
+ const pasteApiUrl =
63
+ (options.pasteApiUrl ?? process.env.PLANNOTATOR_PASTE_URL) || undefined;
64
+
65
+ let resolveDecision!: (result: {
66
+ feedback: string;
67
+ annotations: unknown[];
68
+ exit?: boolean;
69
+ approved?: boolean;
70
+ selectedMessageId?: string;
71
+ feedbackScope?: "message" | "messages";
72
+ }) => void;
73
+ const decisionPromise = new Promise<{
74
+ feedback: string;
75
+ annotations: unknown[];
76
+ exit?: boolean;
77
+ approved?: boolean;
78
+ selectedMessageId?: string;
79
+ feedbackScope?: "message" | "messages";
80
+ }>((r) => {
81
+ resolveDecision = r;
82
+ });
83
+
84
+ // Folder annotation has no stable markdown body, so key drafts by folder path instead.
85
+ const draftSource =
86
+ options.mode === "annotate-folder" && options.folderPath
87
+ ? `folder:${resolvePath(options.folderPath)}`
88
+ : options.renderHtml && options.rawHtml ? options.rawHtml : options.markdown;
89
+ const draftKey = contentHash(draftSource);
90
+
91
+ // Detect repo info (cached for this session)
92
+ const repoInfo = getRepoInfo();
93
+
94
+ const externalAnnotations = createExternalAnnotationHandler("plan");
95
+ const aiRuntime = await createPiAIRuntime();
96
+
97
+ const server = createServer(async (req, res) => {
98
+ const url = requestUrl(req);
99
+
100
+ if (await externalAnnotations.handle(req, res, url)) return;
101
+ if (url.pathname.startsWith("/api/ai/") && await handlePiAIRequest(req, res, url, aiRuntime)) return;
102
+
103
+ if (url.pathname === "/api/plan" && req.method === "GET") {
104
+ json(res, {
105
+ plan: options.markdown,
106
+ origin: options.origin ?? "pi",
107
+ mode: options.mode || "annotate",
108
+ filePath: options.filePath,
109
+ sourceInfo: options.sourceInfo,
110
+ sourceConverted: options.sourceConverted ?? false,
111
+ gate: options.gate ?? false,
112
+ renderAs: options.renderHtml && options.rawHtml ? 'html' : 'markdown',
113
+ ...(options.renderHtml && options.rawHtml ? { rawHtml: options.rawHtml } : {}),
114
+ sharingEnabled,
115
+ shareBaseUrl,
116
+ pasteApiUrl,
117
+ repoInfo,
118
+ projectRoot: options.folderPath || process.cwd(),
119
+ serverConfig: getServerConfig(gitUser),
120
+ ...(options.recentMessages ? { recentMessages: options.recentMessages } : {}),
121
+ });
122
+ } else if (url.pathname === "/api/config" && req.method === "POST") {
123
+ try {
124
+ const body = (await parseBody(req)) as { displayName?: string; diffOptions?: Record<string, unknown>; conventionalComments?: boolean };
125
+ const toSave: Record<string, unknown> = {};
126
+ if (body.displayName !== undefined) toSave.displayName = body.displayName;
127
+ if (body.diffOptions !== undefined) toSave.diffOptions = body.diffOptions;
128
+ if (body.conventionalComments !== undefined) toSave.conventionalComments = body.conventionalComments;
129
+ if (Object.keys(toSave).length > 0) saveConfig(toSave as Parameters<typeof saveConfig>[0]);
130
+ json(res, { ok: true });
131
+ } catch {
132
+ json(res, { error: "Invalid request" }, 400);
133
+ }
134
+ } else if (url.pathname === "/api/image") {
135
+ handleImageRequest(res, url);
136
+ } else if (url.pathname === "/api/upload" && req.method === "POST") {
137
+ await handleUploadRequest(req, res);
138
+ } else if (url.pathname === "/api/draft") {
139
+ await handleDraftRequest(req, res, draftKey);
140
+ } else if (url.pathname === "/api/doc" && req.method === "GET") {
141
+ // Inject source file's directory as base for relative path resolution.
142
+ // Skip for URL annotations — there's no local directory to resolve against.
143
+ if (!url.searchParams.has("base") && options.filePath && !/^https?:\/\//i.test(options.filePath)) {
144
+ url.searchParams.set("base", dirname(resolvePath(options.filePath)));
145
+ }
146
+ await handleDocRequest(res, url);
147
+ } else if (url.pathname === "/api/doc/exists" && req.method === "POST") {
148
+ await handleDocExistsRequest(res, req);
149
+ } else if (url.pathname === "/api/obsidian/vaults") {
150
+ handleObsidianVaultsRequest(res);
151
+ } else if (url.pathname === "/api/reference/obsidian/files" && req.method === "GET") {
152
+ handleObsidianFilesRequest(res, url);
153
+ } else if (url.pathname === "/api/reference/obsidian/doc" && req.method === "GET") {
154
+ handleObsidianDocRequest(res, url);
155
+ } else if (url.pathname === "/api/reference/files" && req.method === "GET") {
156
+ handleFileBrowserRequest(res, url);
157
+ } else if (url.pathname === "/favicon.svg") {
158
+ handleFavicon(res);
159
+ } else if (url.pathname === "/api/exit" && req.method === "POST") {
160
+ deleteDraft(draftKey);
161
+ resolveDecision({ feedback: "", annotations: [], exit: true });
162
+ json(res, { ok: true });
163
+ } else if (url.pathname === "/api/approve" && req.method === "POST") {
164
+ deleteDraft(draftKey);
165
+ resolveDecision({ feedback: "", annotations: [], approved: true });
166
+ json(res, { ok: true });
167
+ } else if (url.pathname === "/api/feedback" && req.method === "POST") {
168
+ try {
169
+ const body = await parseBody(req);
170
+ deleteDraft(draftKey);
171
+ resolveDecision({
172
+ feedback: (body.feedback as string) || "",
173
+ annotations: (body.annotations as unknown[]) || [],
174
+ selectedMessageId: typeof body.selectedMessageId === "string" ? body.selectedMessageId : undefined,
175
+ feedbackScope: body.feedbackScope === "messages" ? "messages" : body.feedbackScope === "message" ? "message" : undefined,
176
+ });
177
+ json(res, { ok: true });
178
+ } catch (err) {
179
+ const message = err instanceof Error ? err.message : "Failed to process feedback";
180
+ json(res, { error: message }, 500);
181
+ }
182
+ } else {
183
+ html(res, options.htmlContent);
184
+ }
185
+ });
186
+
187
+ const { port, portSource } = await listenOnPort(server);
188
+
189
+ return {
190
+ port,
191
+ portSource,
192
+ url: `http://localhost:${port}`,
193
+ waitForDecision: () => decisionPromise,
194
+ stop: () => {
195
+ aiRuntime?.dispose();
196
+ server.close();
197
+ },
198
+ };
199
+ }
@@ -0,0 +1,505 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+
4
+ import { contentHash, deleteDraft } from "../generated/draft.js";
5
+ import {
6
+ type ArchivedPlan,
7
+ generateSlug,
8
+ getPlanVersion,
9
+ getPlanVersionPath,
10
+ getVersionCount,
11
+ listArchivedPlans,
12
+ listVersions,
13
+ readArchivedPlan,
14
+ saveAnnotations,
15
+ saveFinalSnapshot,
16
+ saveToHistory,
17
+ } from "../generated/storage.js";
18
+ import { createEditorAnnotationHandler } from "./annotations.js";
19
+ import { createExternalAnnotationHandler } from "./external-annotations.js";
20
+ import {
21
+ handleDraftRequest,
22
+ handleFavicon,
23
+ handleImageRequest,
24
+ handleUploadRequest,
25
+ } from "./handlers.js";
26
+ import { html, json, parseBody, requestUrl } from "./helpers.js";
27
+ import { createPiAIRuntime, handlePiAIRequest } from "./ai-runtime.js";
28
+ import { openEditorDiff } from "./ide.js";
29
+ import {
30
+ type BearConfig,
31
+ type IntegrationResult,
32
+ type ObsidianConfig,
33
+ type OctarineConfig,
34
+ saveToBear,
35
+ saveToObsidian,
36
+ saveToOctarine,
37
+ } from "./integrations.js";
38
+ import { listenOnPort } from "./network.js";
39
+
40
+ import { loadConfig, saveConfig, detectGitUser, getServerConfig } from "../generated/config.js";
41
+ import { readImprovementHook, getImprovementHookExpectedPath } from "../generated/improvement-hooks.js";
42
+ import { composeImproveContext } from "../generated/pfm-reminder.js";
43
+ import { detectProjectName, getRepoInfo } from "./project.js";
44
+ import {
45
+ handleDocRequest,
46
+ handleDocExistsRequest,
47
+ handleFileBrowserRequest,
48
+ handleObsidianDocRequest,
49
+ handleObsidianFilesRequest,
50
+ handleObsidianVaultsRequest,
51
+ } from "./reference.js";
52
+ import { warmFileListCache } from "../generated/resolve-file.js";
53
+
54
+ export interface PlanReviewDecision {
55
+ approved: boolean;
56
+ feedback?: string;
57
+ savedPath?: string;
58
+ agentSwitch?: string;
59
+ permissionMode?: string;
60
+ }
61
+
62
+ export interface PlanServerResult {
63
+ reviewId: string;
64
+ port: number;
65
+ portSource: "env" | "remote-default" | "random";
66
+ url: string;
67
+ waitForDecision: () => Promise<PlanReviewDecision>;
68
+ onDecision: (listener: (result: PlanReviewDecision) => void | Promise<void>) => () => void;
69
+ waitForDone?: () => Promise<void>;
70
+ stop: () => void;
71
+ }
72
+
73
+ export async function startPlanReviewServer(options: {
74
+ plan: string;
75
+ htmlContent: string;
76
+ origin?: string;
77
+ permissionMode?: string;
78
+ sharingEnabled?: boolean;
79
+ shareBaseUrl?: string;
80
+ pasteApiUrl?: string;
81
+ mode?: "archive";
82
+ customPlanPath?: string | null;
83
+ }): Promise<PlanServerResult> {
84
+ // Side-channel pre-warm so /api/doc/exists POSTs land on warm cache.
85
+ void warmFileListCache(process.cwd(), "code");
86
+ const gitUser = detectGitUser();
87
+ const sharingEnabled =
88
+ options.sharingEnabled ?? process.env.PLANNOTATOR_SHARE !== "disabled";
89
+ const shareBaseUrl =
90
+ (options.shareBaseUrl ?? process.env.PLANNOTATOR_SHARE_URL) || undefined;
91
+ const pasteApiUrl =
92
+ (options.pasteApiUrl ?? process.env.PLANNOTATOR_PASTE_URL) || undefined;
93
+
94
+ // --- Archive mode setup ---
95
+ let archivePlans: ArchivedPlan[] = [];
96
+ let initialArchivePlan = "";
97
+ let resolveDone: (() => void) | undefined;
98
+ let donePromise: Promise<void> | undefined;
99
+
100
+ if (options.mode === "archive") {
101
+ archivePlans = listArchivedPlans(options.customPlanPath ?? undefined);
102
+ initialArchivePlan =
103
+ archivePlans.length > 0
104
+ ? (readArchivedPlan(
105
+ archivePlans[0].filename,
106
+ options.customPlanPath ?? undefined,
107
+ ) ?? "")
108
+ : "";
109
+ donePromise = new Promise<void>((resolve) => {
110
+ resolveDone = resolve;
111
+ });
112
+ }
113
+
114
+ // --- Plan review mode setup (skip in archive mode) ---
115
+ const repoInfo = options.mode !== "archive" ? getRepoInfo() : null;
116
+ const slug = options.mode !== "archive" ? generateSlug(options.plan) : "";
117
+ const project = options.mode !== "archive" ? detectProjectName() : "";
118
+ const historyResult =
119
+ options.mode !== "archive"
120
+ ? saveToHistory(project, slug, options.plan)
121
+ : { version: 0, path: "", isNew: false };
122
+ const previousPlan =
123
+ options.mode !== "archive" && historyResult.version > 1
124
+ ? getPlanVersion(project, slug, historyResult.version - 1)
125
+ : null;
126
+ const versionInfo =
127
+ options.mode !== "archive"
128
+ ? {
129
+ version: historyResult.version,
130
+ totalVersions: getVersionCount(project, slug),
131
+ project,
132
+ }
133
+ : null;
134
+
135
+ const reviewId = randomUUID();
136
+ let resolveDecision!: (result: PlanReviewDecision) => void;
137
+ const decisionListeners = new Set<(result: PlanReviewDecision) => void | Promise<void>>();
138
+ let decisionSettled = false;
139
+ const decisionPromise = new Promise<PlanReviewDecision>((r) => {
140
+ resolveDecision = r;
141
+ });
142
+ const publishDecision = (result: PlanReviewDecision): boolean => {
143
+ if (decisionSettled) return false;
144
+ decisionSettled = true;
145
+ resolveDecision(result);
146
+ for (const listener of decisionListeners) {
147
+ Promise.resolve(listener(result)).catch((error) => {
148
+ console.error("[Plan Review] Decision listener failed:", error);
149
+ });
150
+ }
151
+ return true;
152
+ };
153
+
154
+ // Draft key for annotation persistence
155
+ const draftKey = options.mode !== "archive" ? contentHash(options.plan) : "";
156
+
157
+ // Editor annotations (in-memory, VS Code integration — skip in archive mode)
158
+ const editorAnnotations = options.mode !== "archive" ? createEditorAnnotationHandler() : null;
159
+ const externalAnnotations = options.mode !== "archive" ? createExternalAnnotationHandler("plan") : null;
160
+ const aiRuntime = options.mode !== "archive" ? await createPiAIRuntime() : null;
161
+
162
+ // Lazy cache for in-session archive tab
163
+ let cachedArchivePlans: ArchivedPlan[] | null = null;
164
+
165
+ const server = createServer(async (req, res) => {
166
+ const url = requestUrl(req);
167
+
168
+ if (url.pathname === "/api/done" && req.method === "POST") {
169
+ resolveDone?.();
170
+ json(res, { ok: true });
171
+ } else if (url.pathname === "/api/archive/plans" && req.method === "GET") {
172
+ const customPath = url.searchParams.get("customPath") || undefined;
173
+ if (!cachedArchivePlans)
174
+ cachedArchivePlans = listArchivedPlans(customPath);
175
+ json(res, { plans: cachedArchivePlans });
176
+ } else if (url.pathname === "/api/archive/plan" && req.method === "GET") {
177
+ const filename = url.searchParams.get("filename");
178
+ const customPath = url.searchParams.get("customPath") || undefined;
179
+ if (!filename) {
180
+ json(res, { error: "Missing filename" }, 400);
181
+ return;
182
+ }
183
+ const markdown = readArchivedPlan(filename, customPath);
184
+ if (!markdown) {
185
+ json(res, { error: "Not found" }, 404);
186
+ return;
187
+ }
188
+ json(res, { markdown, filepath: filename });
189
+ } else if (url.pathname === "/api/plan/version") {
190
+ const vParam = url.searchParams.get("v");
191
+ if (!vParam) {
192
+ json(res, { error: "Missing v parameter" }, 400);
193
+ return;
194
+ }
195
+ const v = parseInt(vParam, 10);
196
+ if (Number.isNaN(v) || v < 1) {
197
+ json(res, { error: "Invalid version number" }, 400);
198
+ return;
199
+ }
200
+ const content = getPlanVersion(project, slug, v);
201
+ if (content === null) {
202
+ json(res, { error: "Version not found" }, 404);
203
+ return;
204
+ }
205
+ json(res, { plan: content, version: v });
206
+ } else if (url.pathname === "/api/plan/versions") {
207
+ json(res, { project, slug, versions: listVersions(project, slug) });
208
+ } else if (url.pathname === "/api/plan") {
209
+ if (options.mode === "archive") {
210
+ json(res, {
211
+ plan: initialArchivePlan,
212
+ origin: options.origin ?? "pi",
213
+ mode: "archive",
214
+ archivePlans,
215
+ sharingEnabled,
216
+ shareBaseUrl,
217
+ serverConfig: getServerConfig(gitUser),
218
+ });
219
+ } else {
220
+ json(res, {
221
+ plan: options.plan,
222
+ origin: options.origin ?? "pi",
223
+ permissionMode: options.permissionMode,
224
+ previousPlan,
225
+ versionInfo,
226
+ sharingEnabled,
227
+ shareBaseUrl,
228
+ pasteApiUrl,
229
+ repoInfo,
230
+ projectRoot: process.cwd(),
231
+ serverConfig: getServerConfig(gitUser),
232
+ });
233
+ }
234
+ } else if (url.pathname === "/api/hooks/status" && req.method === "GET") {
235
+ const config = loadConfig();
236
+ const hook = readImprovementHook("enterplanmode-improve");
237
+ const pfmEnabled = config.pfmReminder === true;
238
+ const composed = composeImproveContext({ pfmEnabled, improvementHookContent: hook?.content ?? null });
239
+ json(res, {
240
+ pfmReminder: { enabled: pfmEnabled },
241
+ improvementHook: {
242
+ present: !!hook,
243
+ filePath: hook?.filePath ?? getImprovementHookExpectedPath("enterplanmode-improve"),
244
+ fileSize: hook?.content?.length ?? null,
245
+ content: hook?.content ?? null,
246
+ },
247
+ composedLength: composed?.length ?? null,
248
+ });
249
+ } else if (url.pathname === "/api/config" && req.method === "POST") {
250
+ try {
251
+ const body = (await parseBody(req)) as { displayName?: string; diffOptions?: Record<string, unknown>; conventionalComments?: boolean; pfmReminder?: boolean };
252
+ const toSave: Record<string, unknown> = {};
253
+ if (body.displayName !== undefined) toSave.displayName = body.displayName;
254
+ if (body.diffOptions !== undefined) toSave.diffOptions = body.diffOptions;
255
+ if (body.conventionalComments !== undefined) toSave.conventionalComments = body.conventionalComments;
256
+ if (body.pfmReminder !== undefined) toSave.pfmReminder = body.pfmReminder;
257
+ if (Object.keys(toSave).length > 0) saveConfig(toSave as Parameters<typeof saveConfig>[0]);
258
+ json(res, { ok: true });
259
+ } catch {
260
+ json(res, { error: "Invalid request" }, 400);
261
+ }
262
+ } else if (url.pathname === "/api/image") {
263
+ handleImageRequest(res, url);
264
+ } else if (url.pathname === "/api/upload" && req.method === "POST") {
265
+ await handleUploadRequest(req, res);
266
+ } else if (url.pathname === "/api/draft") {
267
+ await handleDraftRequest(req, res, draftKey);
268
+ } else if (editorAnnotations && (await editorAnnotations.handle(req, res, url))) {
269
+ return;
270
+ } else if (externalAnnotations && (await externalAnnotations.handle(req, res, url))) {
271
+ return;
272
+ } else if (url.pathname.startsWith("/api/ai/") && await handlePiAIRequest(req, res, url, aiRuntime)) {
273
+ return;
274
+ } else if (url.pathname === "/api/doc" && req.method === "GET") {
275
+ await handleDocRequest(res, url);
276
+ } else if (url.pathname === "/api/doc/exists" && req.method === "POST") {
277
+ await handleDocExistsRequest(res, req);
278
+ } else if (url.pathname === "/api/obsidian/vaults") {
279
+ handleObsidianVaultsRequest(res);
280
+ } else if (url.pathname === "/api/reference/obsidian/files" && req.method === "GET") {
281
+ handleObsidianFilesRequest(res, url);
282
+ } else if (url.pathname === "/api/reference/obsidian/doc" && req.method === "GET") {
283
+ handleObsidianDocRequest(res, url);
284
+ } else if (url.pathname === "/api/reference/files" && req.method === "GET") {
285
+ handleFileBrowserRequest(res, url);
286
+ } else if (
287
+ url.pathname === "/api/plan/vscode-diff" &&
288
+ req.method === "POST"
289
+ ) {
290
+ try {
291
+ const body = await parseBody(req);
292
+ const baseVersion = body.baseVersion as number;
293
+ if (!baseVersion) {
294
+ json(res, { error: "Missing baseVersion" }, 400);
295
+ return;
296
+ }
297
+ const basePath = getPlanVersionPath(project, slug, baseVersion);
298
+ if (!basePath) {
299
+ json(res, { error: `Version ${baseVersion} not found` }, 404);
300
+ return;
301
+ }
302
+ const result = await openEditorDiff(basePath, historyResult.path);
303
+ if ("error" in result) {
304
+ json(res, { error: result.error }, 500);
305
+ return;
306
+ }
307
+ json(res, { ok: true });
308
+ } catch (err) {
309
+ json(
310
+ res,
311
+ {
312
+ error:
313
+ err instanceof Error
314
+ ? err.message
315
+ : "Failed to open VS Code diff",
316
+ },
317
+ 500,
318
+ );
319
+ }
320
+ } else if (url.pathname === "/api/agents" && req.method === "GET") {
321
+ json(res, { agents: [] });
322
+ } else if (url.pathname === "/favicon.svg") {
323
+ handleFavicon(res);
324
+ } else if (url.pathname === "/api/save-notes" && req.method === "POST") {
325
+ const results: {
326
+ obsidian?: IntegrationResult;
327
+ bear?: IntegrationResult;
328
+ octarine?: IntegrationResult;
329
+ } = {};
330
+ try {
331
+ const body = await parseBody(req);
332
+ const promises: Promise<void>[] = [];
333
+ const obsConfig = body.obsidian as ObsidianConfig | undefined;
334
+ const bearConfig = body.bear as BearConfig | undefined;
335
+ const octConfig = body.octarine as OctarineConfig | undefined;
336
+ if (obsConfig?.vaultPath && obsConfig?.plan) {
337
+ promises.push(
338
+ saveToObsidian(obsConfig).then((r) => {
339
+ results.obsidian = r;
340
+ }),
341
+ );
342
+ }
343
+ if (bearConfig?.plan) {
344
+ promises.push(
345
+ saveToBear(bearConfig).then((r) => {
346
+ results.bear = r;
347
+ }),
348
+ );
349
+ }
350
+ if (octConfig?.plan && octConfig?.workspace) {
351
+ promises.push(
352
+ saveToOctarine(octConfig).then((r) => {
353
+ results.octarine = r;
354
+ }),
355
+ );
356
+ }
357
+ await Promise.allSettled(promises);
358
+ for (const [name, result] of Object.entries(results)) {
359
+ if (!result?.success && result)
360
+ console.error(`[${name}] Save failed: ${result.error}`);
361
+ }
362
+ } catch (err) {
363
+ console.error(`[Save Notes] Error:`, err);
364
+ json(res, { error: "Save failed" }, 500);
365
+ return;
366
+ }
367
+ json(res, { ok: true, results });
368
+ } else if (url.pathname === "/api/approve" && req.method === "POST") {
369
+ if (decisionSettled) {
370
+ json(res, { ok: true, duplicate: true });
371
+ return;
372
+ }
373
+ let feedback: string | undefined;
374
+ let agentSwitch: string | undefined;
375
+ let requestedPermissionMode: string | undefined;
376
+ let planSaveEnabled = true;
377
+ let planSaveCustomPath: string | undefined;
378
+ try {
379
+ const body = await parseBody(req);
380
+ if (body.feedback) feedback = body.feedback as string;
381
+ if (body.agentSwitch) agentSwitch = body.agentSwitch as string;
382
+ if (body.permissionMode)
383
+ requestedPermissionMode = body.permissionMode as string;
384
+ if (body.planSave !== undefined) {
385
+ const ps = body.planSave as { enabled: boolean; customPath?: string };
386
+ planSaveEnabled = ps.enabled;
387
+ planSaveCustomPath = ps.customPath;
388
+ }
389
+ // Run note integrations in parallel
390
+ const integrationResults: Record<string, IntegrationResult> = {};
391
+ const integrationPromises: Promise<void>[] = [];
392
+ const obsConfig = body.obsidian as ObsidianConfig | undefined;
393
+ const bearConfig = body.bear as BearConfig | undefined;
394
+ const octConfig = body.octarine as OctarineConfig | undefined;
395
+ if (obsConfig?.vaultPath && obsConfig?.plan) {
396
+ integrationPromises.push(
397
+ saveToObsidian(obsConfig).then((r) => {
398
+ integrationResults.obsidian = r;
399
+ }),
400
+ );
401
+ }
402
+ if (bearConfig?.plan) {
403
+ integrationPromises.push(
404
+ saveToBear(bearConfig).then((r) => {
405
+ integrationResults.bear = r;
406
+ }),
407
+ );
408
+ }
409
+ if (octConfig?.plan && octConfig?.workspace) {
410
+ integrationPromises.push(
411
+ saveToOctarine(octConfig).then((r) => {
412
+ integrationResults.octarine = r;
413
+ }),
414
+ );
415
+ }
416
+ await Promise.allSettled(integrationPromises);
417
+ for (const [name, result] of Object.entries(integrationResults)) {
418
+ if (!result?.success && result)
419
+ console.error(`[${name}] Save failed: ${result.error}`);
420
+ }
421
+ } catch (err) {
422
+ console.error(`[Integration] Error:`, err);
423
+ }
424
+ // Save annotations and final snapshot
425
+ let savedPath: string | undefined;
426
+ if (planSaveEnabled) {
427
+ const annotations = feedback || "";
428
+ if (annotations) saveAnnotations(slug, annotations, planSaveCustomPath);
429
+ savedPath = saveFinalSnapshot(
430
+ slug,
431
+ "approved",
432
+ options.plan,
433
+ annotations,
434
+ planSaveCustomPath,
435
+ );
436
+ }
437
+ deleteDraft(draftKey);
438
+ const effectivePermissionMode = requestedPermissionMode || options.permissionMode;
439
+ publishDecision({
440
+ approved: true,
441
+ feedback,
442
+ savedPath,
443
+ agentSwitch,
444
+ permissionMode: effectivePermissionMode,
445
+ });
446
+ json(res, { ok: true, savedPath });
447
+ } else if (url.pathname === "/api/deny" && req.method === "POST") {
448
+ if (decisionSettled) {
449
+ json(res, { ok: true, duplicate: true });
450
+ return;
451
+ }
452
+ let feedback = "Plan rejected by user";
453
+ let planSaveEnabled = true;
454
+ let planSaveCustomPath: string | undefined;
455
+ try {
456
+ const body = await parseBody(req);
457
+ feedback = (body.feedback as string) || feedback;
458
+ if (body.planSave !== undefined) {
459
+ const ps = body.planSave as { enabled: boolean; customPath?: string };
460
+ planSaveEnabled = ps.enabled;
461
+ planSaveCustomPath = ps.customPath;
462
+ }
463
+ } catch {
464
+ /* use default feedback */
465
+ }
466
+ let savedPath: string | undefined;
467
+ if (planSaveEnabled) {
468
+ saveAnnotations(slug, feedback, planSaveCustomPath);
469
+ savedPath = saveFinalSnapshot(
470
+ slug,
471
+ "denied",
472
+ options.plan,
473
+ feedback,
474
+ planSaveCustomPath,
475
+ );
476
+ }
477
+ deleteDraft(draftKey);
478
+ publishDecision({ approved: false, feedback, savedPath });
479
+ json(res, { ok: true, savedPath });
480
+ } else {
481
+ html(res, options.htmlContent);
482
+ }
483
+ });
484
+
485
+ const { port, portSource } = await listenOnPort(server);
486
+
487
+ return {
488
+ reviewId,
489
+ port,
490
+ portSource,
491
+ url: `http://localhost:${port}`,
492
+ waitForDecision: () => decisionPromise,
493
+ onDecision: (listener) => {
494
+ decisionListeners.add(listener);
495
+ return () => {
496
+ decisionListeners.delete(listener);
497
+ };
498
+ },
499
+ ...(donePromise && { waitForDone: () => donePromise }),
500
+ stop: () => {
501
+ aiRuntime?.dispose();
502
+ server.close();
503
+ },
504
+ };
505
+ }