@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,648 @@
1
+ /**
2
+ * Plannotator Shared Server
3
+ *
4
+ * Provides a consistent server implementation for both Claude Code and OpenCode plugins.
5
+ *
6
+ * Environment variables:
7
+ * PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local
8
+ * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
9
+ * PLANNOTATOR_ORIGIN - Explicit origin override; validated against AGENT_CONFIG
10
+ * in packages/shared/agents.ts. Supported values:
11
+ * "claude-code", "opencode", "codex", "copilot-cli",
12
+ * "gemini-cli", "pi".
13
+ */
14
+
15
+ import type { Origin } from "@plannotator/shared/agents";
16
+ import { resolve } from "path";
17
+ import { isRemoteSession, getServerHostname, getServerPort } from "./remote";
18
+ import { openEditorDiff } from "./ide";
19
+ import {
20
+ saveToObsidian,
21
+ saveToBear,
22
+ saveToOctarine,
23
+ type ObsidianConfig,
24
+ type BearConfig,
25
+ type OctarineConfig,
26
+ type IntegrationResult,
27
+ } from "./integrations";
28
+ import {
29
+ generateSlug,
30
+ savePlan,
31
+ saveAnnotations,
32
+ saveFinalSnapshot,
33
+ saveToHistory,
34
+ getPlanVersion,
35
+ getPlanVersionPath,
36
+ getVersionCount,
37
+ listVersions,
38
+ listArchivedPlans,
39
+ readArchivedPlan,
40
+ type ArchivedPlan,
41
+ } from "./storage";
42
+ import { getRepoInfo } from "./repo";
43
+ import { detectProjectName } from "./project";
44
+ import { loadConfig, saveConfig, detectGitUser, getServerConfig } from "./config";
45
+ import { readImprovementHook, getImprovementHookExpectedPath } from "@plannotator/shared/improvement-hooks";
46
+ import { composeImproveContext } from "@plannotator/shared/pfm-reminder";
47
+ import { handleImage, handleUpload, handleAgents, handleServerReady, handleDraftSave, handleDraftLoad, handleDraftDelete, handleFavicon, type OpencodeClient } from "./shared-handlers";
48
+ import { contentHash, deleteDraft } from "./draft";
49
+ import { handleDoc, handleDocExists, handleObsidianVaults, handleObsidianFiles, handleObsidianDoc, handleFileBrowserFiles } from "./reference-handlers";
50
+ import { warmFileListCache } from "@plannotator/shared/resolve-file";
51
+ import { createEditorAnnotationHandler } from "./editor-annotations";
52
+ import { createExternalAnnotationHandler } from "./external-annotations";
53
+ import { isWSL } from "./browser";
54
+ import { AI_QUERY_ENDPOINT, createAIRuntime } from "./ai-runtime";
55
+ import type { AIEndpoints } from "@plannotator/ai";
56
+
57
+ // Re-export utilities
58
+ export { isRemoteSession, getServerPort } from "./remote";
59
+ export { openBrowser } from "./browser";
60
+ export * from "./integrations";
61
+ export * from "./storage";
62
+ export { handleServerReady } from "./shared-handlers";
63
+ export { type VaultNode, buildFileTree } from "@plannotator/shared/reference-common";
64
+
65
+ // --- Types ---
66
+
67
+ export interface ServerOptions {
68
+ /** The plan markdown content */
69
+ plan: string;
70
+ /** Origin identifier (e.g., "claude-code", "opencode") */
71
+ origin: Origin;
72
+ /** HTML content to serve for the UI */
73
+ htmlContent: string;
74
+ /** Current permission mode to preserve (Claude Code only) */
75
+ permissionMode?: string;
76
+ /** Whether URL sharing is enabled (default: true) */
77
+ sharingEnabled?: boolean;
78
+ /** Custom base URL for share links (default: https://share.plannotator.ai) */
79
+ shareBaseUrl?: string;
80
+ /** Base URL of the paste service API for short URL sharing */
81
+ pasteApiUrl?: string;
82
+ /** Called when server starts with the URL, remote status, and port */
83
+ onReady?: (url: string, isRemote: boolean, port: number) => void;
84
+ /** OpenCode client for querying available agents (OpenCode only) */
85
+ opencodeClient?: OpencodeClient;
86
+ /** When set to "archive", server runs in read-only archive browser mode */
87
+ mode?: "archive";
88
+ /** Custom plan save path — used by archive mode to find saved plans */
89
+ customPlanPath?: string | null;
90
+ }
91
+
92
+ export interface ServerResult {
93
+ /** The port the server is running on */
94
+ port: number;
95
+ /** The full URL to access the server */
96
+ url: string;
97
+ /** Whether running in remote mode */
98
+ isRemote: boolean;
99
+ /** Wait for user decision (approve/deny) */
100
+ waitForDecision: () => Promise<{
101
+ approved: boolean;
102
+ feedback?: string;
103
+ savedPath?: string;
104
+ agentSwitch?: string;
105
+ permissionMode?: string;
106
+ }>;
107
+ /** Wait for user to close (archive mode only) */
108
+ waitForDone?: () => Promise<void>;
109
+ /** Stop the server */
110
+ stop: () => void;
111
+ }
112
+
113
+ // --- Server Implementation ---
114
+
115
+ const MAX_RETRIES = 5;
116
+ const RETRY_DELAY_MS = 500;
117
+
118
+ /**
119
+ * Start the Plannotator server
120
+ *
121
+ * Handles:
122
+ * - Remote detection and port configuration
123
+ * - All API routes (/api/plan, /api/approve, /api/deny, etc.)
124
+ * - Obsidian/Bear integrations
125
+ * - Port conflict retries
126
+ */
127
+ export async function startPlannotatorServer(
128
+ options: ServerOptions
129
+ ): Promise<ServerResult> {
130
+ const { plan, origin, htmlContent, permissionMode, sharingEnabled = true, shareBaseUrl, pasteApiUrl, onReady, mode, customPlanPath } = options;
131
+
132
+ const isRemote = isRemoteSession();
133
+ const configuredPort = getServerPort();
134
+ const wslFlag = await isWSL();
135
+ const gitUser = detectGitUser();
136
+
137
+ // Side-channel pre-warm: kick off the code-file walk now so the
138
+ // renderer's POST /api/doc/exists lands on warm cache.
139
+ void warmFileListCache(process.cwd(), "code");
140
+
141
+ // --- Archive mode setup ---
142
+ let archivePlans: ArchivedPlan[] = [];
143
+ let initialArchivePlan = "";
144
+ let resolveDone: (() => void) | undefined;
145
+ let donePromise: Promise<void> | undefined;
146
+
147
+ if (mode === "archive") {
148
+ archivePlans = listArchivedPlans(customPlanPath ?? undefined);
149
+ initialArchivePlan = archivePlans.length > 0
150
+ ? readArchivedPlan(archivePlans[0].filename, customPlanPath ?? undefined) ?? ""
151
+ : "";
152
+ donePromise = new Promise<void>((resolve) => { resolveDone = resolve; });
153
+ }
154
+
155
+ // --- Plan review mode setup (skip in archive mode) ---
156
+ const draftKey = mode !== "archive" ? contentHash(plan) : "";
157
+ const editorAnnotations = mode !== "archive" ? createEditorAnnotationHandler() : null;
158
+ const externalAnnotations = mode !== "archive" ? createExternalAnnotationHandler("plan") : null;
159
+ const aiRuntime = mode !== "archive" ? await createAIRuntime() : null;
160
+ const slug = mode !== "archive" ? generateSlug(plan) : "";
161
+
162
+ // Lazy cache for in-session archive browsing (plan review sidebar tab)
163
+ let cachedArchivePlans: ReturnType<typeof listArchivedPlans> | null = null;
164
+
165
+ // Plan-specific: repo info, version history, decision promise
166
+ let repoInfo: Awaited<ReturnType<typeof getRepoInfo>> | null = null;
167
+ let project = "";
168
+ let currentPlanPath = "";
169
+ let previousPlan: string | null = null;
170
+ let versionInfo = { version: 0, totalVersions: 0, project: "" };
171
+
172
+ let resolveDecision: (result: {
173
+ approved: boolean;
174
+ feedback?: string;
175
+ savedPath?: string;
176
+ agentSwitch?: string;
177
+ permissionMode?: string;
178
+ }) => void;
179
+ let decisionPromise: Promise<{
180
+ approved: boolean;
181
+ feedback?: string;
182
+ savedPath?: string;
183
+ agentSwitch?: string;
184
+ permissionMode?: string;
185
+ }>;
186
+
187
+ if (mode !== "archive") {
188
+ repoInfo = await getRepoInfo();
189
+ project = (await detectProjectName()) ?? "_unknown";
190
+ const historyResult = saveToHistory(project, slug, plan);
191
+ currentPlanPath = historyResult.path;
192
+ previousPlan =
193
+ historyResult.version > 1
194
+ ? getPlanVersion(project, slug, historyResult.version - 1)
195
+ : null;
196
+ versionInfo = {
197
+ version: historyResult.version,
198
+ totalVersions: getVersionCount(project, slug),
199
+ project,
200
+ };
201
+
202
+ decisionPromise = new Promise((resolve) => {
203
+ resolveDecision = resolve;
204
+ });
205
+ } else {
206
+ // Never-resolving promise — archive mode uses waitForDone instead
207
+ decisionPromise = new Promise(() => {});
208
+ }
209
+
210
+ // Start server with retry logic
211
+ let server: ReturnType<typeof Bun.serve> | null = null;
212
+
213
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
214
+ try {
215
+ server = Bun.serve({
216
+ hostname: getServerHostname(),
217
+ port: configuredPort,
218
+
219
+ async fetch(req, server) {
220
+ const url = new URL(req.url);
221
+
222
+ // API: Get a specific plan version from history
223
+ if (url.pathname === "/api/plan/version") {
224
+ const vParam = url.searchParams.get("v");
225
+ if (!vParam) {
226
+ return new Response("Missing v parameter", { status: 400 });
227
+ }
228
+ const v = parseInt(vParam, 10);
229
+ if (isNaN(v) || v < 1) {
230
+ return new Response("Invalid version number", { status: 400 });
231
+ }
232
+ const content = getPlanVersion(project, slug, v);
233
+ if (content === null) {
234
+ return Response.json({ error: "Version not found" }, { status: 404 });
235
+ }
236
+ return Response.json({ plan: content, version: v });
237
+ }
238
+
239
+ // API: List all versions for the current plan
240
+ if (url.pathname === "/api/plan/versions") {
241
+ return Response.json({
242
+ project,
243
+ slug,
244
+ versions: listVersions(project, slug),
245
+ });
246
+ }
247
+
248
+ // API: List archived plans (from ~/.plannotator/plans/)
249
+ // Cached for session lifetime — new plans won't appear during a single review
250
+ if (url.pathname === "/api/archive/plans" && req.method === "GET") {
251
+ const customPath = url.searchParams.get("customPath") || undefined;
252
+ if (!cachedArchivePlans) cachedArchivePlans = listArchivedPlans(customPath);
253
+ return Response.json({ plans: cachedArchivePlans });
254
+ }
255
+
256
+ // API: Get a specific archived plan
257
+ if (url.pathname === "/api/archive/plan" && req.method === "GET") {
258
+ const filename = url.searchParams.get("filename");
259
+ if (!filename) {
260
+ return Response.json({ error: "Missing filename parameter" }, { status: 400 });
261
+ }
262
+ const customPath = url.searchParams.get("customPath") || undefined;
263
+ const content = readArchivedPlan(filename, customPath);
264
+ if (content === null) {
265
+ return Response.json({ error: "Plan not found" }, { status: 404 });
266
+ }
267
+ return Response.json({ markdown: content, filepath: filename });
268
+ }
269
+
270
+ // API: Close archive browser (archive mode only)
271
+ if (url.pathname === "/api/done" && req.method === "POST") {
272
+ resolveDone?.();
273
+ return Response.json({ ok: true });
274
+ }
275
+
276
+ // API: Get plan content
277
+ if (url.pathname === "/api/plan") {
278
+ if (mode === "archive") {
279
+ return Response.json({
280
+ plan: initialArchivePlan,
281
+ origin,
282
+ mode: "archive",
283
+ archivePlans,
284
+ sharingEnabled,
285
+ shareBaseUrl,
286
+ isWSL: wslFlag,
287
+ serverConfig: getServerConfig(gitUser),
288
+ });
289
+ }
290
+ return Response.json({ plan, origin, permissionMode, sharingEnabled, shareBaseUrl, pasteApiUrl, repoInfo, previousPlan, versionInfo, projectRoot: process.cwd(), isWSL: wslFlag, serverConfig: getServerConfig(gitUser) });
291
+ }
292
+
293
+ // API: Serve a linked markdown document
294
+ if (url.pathname === "/api/doc" && req.method === "GET") {
295
+ return handleDoc(req);
296
+ }
297
+
298
+ // API: Batch existence check for code-file paths the renderer detected
299
+ if (url.pathname === "/api/doc/exists" && req.method === "POST") {
300
+ return handleDocExists(req);
301
+ }
302
+
303
+ // API: Hook status for the Settings Hooks tab
304
+ if (url.pathname === "/api/hooks/status" && req.method === "GET") {
305
+ const config = loadConfig();
306
+ const hook = readImprovementHook("enterplanmode-improve");
307
+ const pfmEnabled = config.pfmReminder === true;
308
+ const composed = composeImproveContext({
309
+ pfmEnabled,
310
+ improvementHookContent: hook?.content ?? null,
311
+ });
312
+ return Response.json({
313
+ pfmReminder: { enabled: pfmEnabled },
314
+ improvementHook: {
315
+ present: !!hook,
316
+ filePath: hook?.filePath ?? getImprovementHookExpectedPath("enterplanmode-improve"),
317
+ fileSize: hook?.content?.length ?? null,
318
+ content: hook?.content ?? null,
319
+ },
320
+ composedLength: composed?.length ?? null,
321
+ });
322
+ }
323
+
324
+ // API: Update user config (write-back to ~/.plannotator/config.json)
325
+ if (url.pathname === "/api/config" && req.method === "POST") {
326
+ try {
327
+ const body = (await req.json()) as { displayName?: string; diffOptions?: Record<string, unknown>; conventionalComments?: boolean; conventionalLabels?: unknown[] | null; pfmReminder?: boolean };
328
+ const toSave: Record<string, unknown> = {};
329
+ if (body.displayName !== undefined) toSave.displayName = body.displayName;
330
+ if (body.diffOptions !== undefined) toSave.diffOptions = body.diffOptions;
331
+ if (body.conventionalComments !== undefined) toSave.conventionalComments = body.conventionalComments;
332
+ if (body.conventionalLabels !== undefined) toSave.conventionalLabels = body.conventionalLabels;
333
+ if (body.pfmReminder !== undefined) toSave.pfmReminder = body.pfmReminder;
334
+ if (Object.keys(toSave).length > 0) saveConfig(toSave as Parameters<typeof saveConfig>[0]);
335
+ return Response.json({ ok: true });
336
+ } catch {
337
+ return Response.json({ error: "Invalid request" }, { status: 400 });
338
+ }
339
+ }
340
+
341
+ // API: Serve images (local paths or temp uploads)
342
+ if (url.pathname === "/api/image") {
343
+ return handleImage(req);
344
+ }
345
+
346
+ // API: Upload image -> save to temp -> return path
347
+ if (url.pathname === "/api/upload" && req.method === "POST") {
348
+ return handleUpload(req);
349
+ }
350
+
351
+ // API: Open plan diff in VS Code
352
+ if (url.pathname === "/api/plan/vscode-diff" && req.method === "POST") {
353
+ try {
354
+ const body = (await req.json()) as { baseVersion: number };
355
+
356
+ if (!body.baseVersion) {
357
+ return Response.json({ error: "Missing baseVersion" }, { status: 400 });
358
+ }
359
+
360
+ const basePath = getPlanVersionPath(project, slug, body.baseVersion);
361
+ if (!basePath) {
362
+ return Response.json({ error: `Version ${body.baseVersion} not found` }, { status: 404 });
363
+ }
364
+
365
+ const result = await openEditorDiff(basePath, currentPlanPath);
366
+ if ("error" in result) {
367
+ return Response.json({ error: result.error }, { status: 500 });
368
+ }
369
+ return Response.json({ ok: true });
370
+ } catch (err) {
371
+ const message = err instanceof Error ? err.message : "Failed to open VS Code diff";
372
+ return Response.json({ error: message }, { status: 500 });
373
+ }
374
+ }
375
+
376
+ // API: Detect Obsidian vaults
377
+ if (url.pathname === "/api/obsidian/vaults") {
378
+ return handleObsidianVaults();
379
+ }
380
+
381
+ // API: List Obsidian vault files as a tree
382
+ if (url.pathname === "/api/reference/obsidian/files" && req.method === "GET") {
383
+ return handleObsidianFiles(req);
384
+ }
385
+
386
+ // API: Read an Obsidian vault document
387
+ if (url.pathname === "/api/reference/obsidian/doc" && req.method === "GET") {
388
+ return handleObsidianDoc(req);
389
+ }
390
+
391
+ // API: List markdown files in a directory as a tree
392
+ if (url.pathname === "/api/reference/files" && req.method === "GET") {
393
+ return handleFileBrowserFiles(req);
394
+ }
395
+
396
+ // API: Get available agents (OpenCode only)
397
+ if (url.pathname === "/api/agents") {
398
+ return handleAgents(options.opencodeClient);
399
+ }
400
+
401
+ // API: Annotation draft persistence
402
+ if (url.pathname === "/api/draft") {
403
+ if (req.method === "POST") return handleDraftSave(req, draftKey);
404
+ if (req.method === "DELETE") return handleDraftDelete(draftKey);
405
+ return handleDraftLoad(draftKey);
406
+ }
407
+
408
+ // API: Editor annotations (VS Code extension)
409
+ const editorResponse = await editorAnnotations?.handle(req, url);
410
+ if (editorResponse) return editorResponse;
411
+
412
+ // API: External annotations (SSE-based, for any external tool)
413
+ const externalResponse = await externalAnnotations?.handle(req, url, {
414
+ disableIdleTimeout: () => server.timeout(req, 0),
415
+ });
416
+ if (externalResponse) return externalResponse;
417
+
418
+ if (url.pathname.startsWith("/api/ai/")) {
419
+ if (!aiRuntime) {
420
+ if (url.pathname.slice("/api/ai/".length) === "capabilities" && req.method === "GET") {
421
+ return Response.json({ available: false, providers: [] });
422
+ }
423
+ return Response.json({ error: "AI backend not available" }, { status: 503 });
424
+ }
425
+ const handler = aiRuntime.endpoints[url.pathname as keyof AIEndpoints];
426
+ if (handler) {
427
+ if (url.pathname === AI_QUERY_ENDPOINT) {
428
+ server.timeout(req, 0);
429
+ }
430
+ return handler(req);
431
+ }
432
+ return Response.json({ error: "Not found" }, { status: 404 });
433
+ }
434
+
435
+ // API: Save to notes (decoupled from approve/deny)
436
+ if (url.pathname === "/api/save-notes" && req.method === "POST") {
437
+ const results: { obsidian?: IntegrationResult; bear?: IntegrationResult; octarine?: IntegrationResult } = {};
438
+
439
+ try {
440
+ const body = (await req.json()) as {
441
+ obsidian?: ObsidianConfig;
442
+ bear?: BearConfig;
443
+ octarine?: OctarineConfig;
444
+ };
445
+
446
+ // Run integrations in parallel — they're independent
447
+ const promises: Promise<void>[] = [];
448
+ if (body.obsidian?.vaultPath && body.obsidian?.plan) {
449
+ promises.push(saveToObsidian(body.obsidian).then(r => { results.obsidian = r; }));
450
+ }
451
+ if (body.bear?.plan) {
452
+ promises.push(saveToBear(body.bear).then(r => { results.bear = r; }));
453
+ }
454
+ if (body.octarine?.plan && body.octarine?.workspace) {
455
+ promises.push(saveToOctarine(body.octarine).then(r => { results.octarine = r; }));
456
+ }
457
+ await Promise.allSettled(promises);
458
+
459
+ for (const [name, result] of Object.entries(results)) {
460
+ if (!result?.success && result) {
461
+ console.error(`[${name}] Save failed: ${result.error}`);
462
+ }
463
+ }
464
+ } catch (err) {
465
+ console.error(`[Save Notes] Error:`, err);
466
+ return Response.json({ error: "Save failed" }, { status: 500 });
467
+ }
468
+
469
+ return Response.json({ ok: true, results });
470
+ }
471
+
472
+ // API: Approve plan
473
+ if (url.pathname === "/api/approve" && req.method === "POST") {
474
+ // Check for note integrations and optional feedback
475
+ let feedback: string | undefined;
476
+ let agentSwitch: string | undefined;
477
+ let requestedPermissionMode: string | undefined;
478
+ let planSaveEnabled = true; // default to enabled for backwards compat
479
+ let planSaveCustomPath: string | undefined;
480
+ try {
481
+ const body = (await req.json().catch(() => ({}))) as {
482
+ obsidian?: ObsidianConfig;
483
+ bear?: BearConfig;
484
+ octarine?: OctarineConfig;
485
+ feedback?: string;
486
+ agentSwitch?: string;
487
+ planSave?: { enabled: boolean; customPath?: string };
488
+ permissionMode?: string;
489
+ };
490
+
491
+ // Capture feedback if provided (for "approve with notes")
492
+ if (body.feedback) {
493
+ feedback = body.feedback;
494
+ }
495
+
496
+ // Capture agent switch setting for OpenCode
497
+ if (body.agentSwitch) {
498
+ agentSwitch = body.agentSwitch;
499
+ }
500
+
501
+ // Capture permission mode from client request (Claude Code)
502
+ if (body.permissionMode) {
503
+ requestedPermissionMode = body.permissionMode;
504
+ }
505
+
506
+ // Capture plan save settings
507
+ if (body.planSave !== undefined) {
508
+ planSaveEnabled = body.planSave.enabled;
509
+ planSaveCustomPath = body.planSave.customPath;
510
+ }
511
+
512
+ // Run integrations in parallel — they're independent
513
+ const integrationResults: Record<string, IntegrationResult> = {};
514
+ const integrationPromises: Promise<void>[] = [];
515
+ if (body.obsidian?.vaultPath && body.obsidian?.plan) {
516
+ integrationPromises.push(saveToObsidian(body.obsidian).then(r => { integrationResults.obsidian = r; }));
517
+ }
518
+ if (body.bear?.plan) {
519
+ integrationPromises.push(saveToBear(body.bear).then(r => { integrationResults.bear = r; }));
520
+ }
521
+ if (body.octarine?.plan && body.octarine?.workspace) {
522
+ integrationPromises.push(saveToOctarine(body.octarine).then(r => { integrationResults.octarine = r; }));
523
+ }
524
+ await Promise.allSettled(integrationPromises);
525
+
526
+ for (const [name, result] of Object.entries(integrationResults)) {
527
+ if (!result?.success && result) {
528
+ console.error(`[${name}] Save failed: ${result.error}`);
529
+ }
530
+ }
531
+ } catch (err) {
532
+ // Don't block approval on integration errors
533
+ console.error(`[Integration] Error:`, err);
534
+ }
535
+
536
+ // Save annotations and final snapshot (if enabled)
537
+ let savedPath: string | undefined;
538
+ if (planSaveEnabled) {
539
+ const annotations = feedback || "";
540
+ if (annotations) {
541
+ saveAnnotations(slug, annotations, planSaveCustomPath);
542
+ }
543
+ savedPath = saveFinalSnapshot(slug, "approved", plan, annotations, planSaveCustomPath);
544
+ }
545
+
546
+ // Clean up draft on successful submit
547
+ deleteDraft(draftKey);
548
+
549
+ // Use permission mode from client request if provided, otherwise fall back to hook input
550
+ const effectivePermissionMode = requestedPermissionMode || permissionMode;
551
+ resolveDecision({ approved: true, feedback, savedPath, agentSwitch, permissionMode: effectivePermissionMode });
552
+ return Response.json({ ok: true, savedPath });
553
+ }
554
+
555
+ // API: Deny with feedback
556
+ if (url.pathname === "/api/deny" && req.method === "POST") {
557
+ let feedback = "Plan rejected by user";
558
+ let planSaveEnabled = true; // default to enabled for backwards compat
559
+ let planSaveCustomPath: string | undefined;
560
+ try {
561
+ const body = (await req.json()) as {
562
+ feedback?: string;
563
+ planSave?: { enabled: boolean; customPath?: string };
564
+ };
565
+ feedback = body.feedback || feedback;
566
+
567
+ // Capture plan save settings
568
+ if (body.planSave !== undefined) {
569
+ planSaveEnabled = body.planSave.enabled;
570
+ planSaveCustomPath = body.planSave.customPath;
571
+ }
572
+ } catch {
573
+ // Use default feedback
574
+ }
575
+
576
+ // Save annotations and final snapshot (if enabled)
577
+ let savedPath: string | undefined;
578
+ if (planSaveEnabled) {
579
+ saveAnnotations(slug, feedback, planSaveCustomPath);
580
+ savedPath = saveFinalSnapshot(slug, "denied", plan, feedback, planSaveCustomPath);
581
+ }
582
+
583
+ deleteDraft(draftKey);
584
+ resolveDecision({ approved: false, feedback, savedPath });
585
+ return Response.json({ ok: true, savedPath });
586
+ }
587
+
588
+ // Favicon
589
+ if (url.pathname === "/favicon.svg") return handleFavicon();
590
+
591
+ // Serve embedded HTML for all other routes (SPA)
592
+ return new Response(htmlContent, {
593
+ headers: { "Content-Type": "text/html" },
594
+ });
595
+ },
596
+
597
+ error(err) {
598
+ console.error("[plannotator] Server error:", err);
599
+ return new Response(
600
+ `Internal Server Error: ${err instanceof Error ? err.message : String(err)}`,
601
+ { status: 500, headers: { "Content-Type": "text/plain" } },
602
+ );
603
+ },
604
+ });
605
+
606
+ break; // Success, exit retry loop
607
+ } catch (err: unknown) {
608
+ const isAddressInUse =
609
+ err instanceof Error && err.message.includes("EADDRINUSE");
610
+
611
+ if (isAddressInUse && attempt < MAX_RETRIES) {
612
+ await Bun.sleep(RETRY_DELAY_MS);
613
+ continue;
614
+ }
615
+
616
+ if (isAddressInUse) {
617
+ const hint = isRemote ? " (set PLANNOTATOR_PORT to use different port)" : "";
618
+ throw new Error(`Port ${configuredPort} in use after ${MAX_RETRIES} retries${hint}`);
619
+ }
620
+
621
+ throw err;
622
+ }
623
+ }
624
+
625
+ if (!server) {
626
+ throw new Error("Failed to start server");
627
+ }
628
+
629
+ const port = server.port!;
630
+ const serverUrl = `http://localhost:${port}`;
631
+
632
+ // Notify caller that server is ready
633
+ if (onReady) {
634
+ onReady(serverUrl, isRemote, port);
635
+ }
636
+
637
+ return {
638
+ port,
639
+ url: serverUrl,
640
+ isRemote,
641
+ waitForDecision: () => decisionPromise,
642
+ ...(donePromise && { waitForDone: () => donePromise }),
643
+ stop: () => {
644
+ aiRuntime?.dispose();
645
+ server.stop();
646
+ },
647
+ };
648
+ }