@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,469 @@
1
+ /**
2
+ * Pi SDK provider — bridges Plannotator's AI layer with Pi's coding agent.
3
+ *
4
+ * Spawns `pi --mode rpc` as a subprocess and communicates via JSONL over
5
+ * stdio. No Pi SDK is imported — this is a thin protocol adapter.
6
+ *
7
+ * One subprocess per session. The user must have the `pi` CLI installed.
8
+ */
9
+
10
+ import { BaseSession } from "../base-session.ts";
11
+ import { buildEffectivePrompt, buildSystemPrompt } from "../context.ts";
12
+ import type {
13
+ AIMessage,
14
+ AIProvider,
15
+ AIProviderCapabilities,
16
+ CreateSessionOptions,
17
+ PiSDKConfig,
18
+ } from "../types.ts";
19
+ import {
20
+ buildWindowsCommandScriptSpawnCommand,
21
+ killWindowsProcessTree,
22
+ resolveWindowsCommandShim,
23
+ } from "./command-path.ts";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Constants
27
+ // ---------------------------------------------------------------------------
28
+
29
+ const PROVIDER_NAME = "pi-sdk";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // JSONL subprocess wrapper
33
+ // ---------------------------------------------------------------------------
34
+
35
+ type EventListener = (event: Record<string, unknown>) => void;
36
+
37
+ class PiProcess {
38
+ private proc: ReturnType<typeof Bun.spawn> | null = null;
39
+ private listeners: EventListener[] = [];
40
+ private pendingRequests = new Map<
41
+ string,
42
+ {
43
+ resolve: (data: Record<string, unknown>) => void;
44
+ reject: (err: Error) => void;
45
+ }
46
+ >();
47
+ private nextId = 0;
48
+ private buffer = "";
49
+ private _alive = false;
50
+
51
+ async spawn(piPath: string, cwd: string): Promise<void> {
52
+ const commandPath = resolveWindowsCommandShim(piPath);
53
+ const command =
54
+ buildWindowsCommandScriptSpawnCommand(commandPath, ["--mode", "rpc"]) ?? [
55
+ commandPath,
56
+ "--mode",
57
+ "rpc",
58
+ ];
59
+ try {
60
+ this.proc = Bun.spawn(command, {
61
+ cwd,
62
+ stdin: "pipe",
63
+ stdout: "pipe",
64
+ stderr: "pipe",
65
+ });
66
+ } catch (err) {
67
+ const error = err instanceof Error ? err : new Error(String(err));
68
+ this.handleProcessEnd(error);
69
+ throw error;
70
+ }
71
+ this._alive = true;
72
+
73
+ this.readStream();
74
+
75
+ this.proc.exited.then(() => {
76
+ this.handleProcessEnd(new Error("Pi process exited unexpectedly"));
77
+ });
78
+ }
79
+
80
+ private handleProcessEnd(error: Error): void {
81
+ if (!this.proc && this.pendingRequests.size === 0) return;
82
+
83
+ this._alive = false;
84
+ this.proc = null;
85
+ for (const [, pending] of this.pendingRequests) {
86
+ pending.reject(error);
87
+ }
88
+ this.pendingRequests.clear();
89
+ // Signal active query listeners so the drain loop exits with an error
90
+ for (const listener of this.listeners) {
91
+ listener({ type: "process_exited" });
92
+ }
93
+ }
94
+
95
+ private async readStream(): Promise<void> {
96
+ if (!this.proc?.stdout || typeof this.proc.stdout === "number") return;
97
+ const reader = (this.proc.stdout as ReadableStream<Uint8Array>).getReader();
98
+ const decoder = new TextDecoder();
99
+
100
+ try {
101
+ while (true) {
102
+ const { done, value } = await reader.read();
103
+ if (done) break;
104
+
105
+ this.buffer += decoder.decode(value, { stream: true });
106
+ const lines = this.buffer.split("\n");
107
+ this.buffer = lines.pop() ?? "";
108
+
109
+ for (const line of lines) {
110
+ const trimmed = line.replace(/\r$/, "");
111
+ if (!trimmed) continue;
112
+ try {
113
+ const parsed = JSON.parse(trimmed);
114
+ this.routeMessage(parsed);
115
+ } catch {
116
+ // Ignore malformed lines
117
+ }
118
+ }
119
+ }
120
+ } catch {
121
+ // Stream closed
122
+ }
123
+ }
124
+
125
+ private routeMessage(msg: Record<string, unknown>): void {
126
+ // Response to a command we sent
127
+ if (msg.type === "response" && typeof msg.id === "string") {
128
+ const pending = this.pendingRequests.get(msg.id);
129
+ if (pending) {
130
+ this.pendingRequests.delete(msg.id);
131
+ if (msg.success === false) {
132
+ pending.reject(new Error((msg.error as string) ?? "RPC error"));
133
+ } else {
134
+ pending.resolve((msg.data as Record<string, unknown>) ?? {});
135
+ }
136
+ return;
137
+ }
138
+ }
139
+
140
+ // Agent event — forward to listeners
141
+ for (const listener of this.listeners) {
142
+ listener(msg);
143
+ }
144
+ }
145
+
146
+ /** Send a command without waiting for a response. */
147
+ send(command: Record<string, unknown>): void {
148
+ if (!this.proc?.stdin || typeof this.proc.stdin === "number") return;
149
+ // Bun.spawn stdin is a FileSink with .write(), not a WritableStream
150
+ const sink = this.proc.stdin as { write(data: string): void; flush(): void };
151
+ sink.write(`${JSON.stringify(command)}\n`);
152
+ sink.flush();
153
+ }
154
+
155
+ /** Send a command and wait for the correlated response. */
156
+ sendAndWait(
157
+ command: Record<string, unknown>,
158
+ ): Promise<Record<string, unknown>> {
159
+ const id = `req_${++this.nextId}`;
160
+ return new Promise((resolve, reject) => {
161
+ this.pendingRequests.set(id, { resolve, reject });
162
+ this.send({ ...command, id });
163
+ });
164
+ }
165
+
166
+ /** Register a listener for agent events (non-response messages). */
167
+ onEvent(listener: EventListener): () => void {
168
+ this.listeners.push(listener);
169
+ return () => {
170
+ const idx = this.listeners.indexOf(listener);
171
+ if (idx >= 0) this.listeners.splice(idx, 1);
172
+ };
173
+ }
174
+
175
+ get alive(): boolean {
176
+ return this._alive;
177
+ }
178
+
179
+ kill(): void {
180
+ this._alive = false;
181
+ const proc = this.proc;
182
+ this.proc = null;
183
+ if (proc) {
184
+ if (!killWindowsProcessTree(proc.pid)) {
185
+ proc.kill();
186
+ }
187
+ }
188
+ this.listeners.length = 0;
189
+ for (const [, pending] of this.pendingRequests) {
190
+ pending.reject(new Error("Process killed"));
191
+ }
192
+ this.pendingRequests.clear();
193
+ }
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Provider
198
+ // ---------------------------------------------------------------------------
199
+
200
+ export class PiSDKProvider implements AIProvider {
201
+ readonly name = PROVIDER_NAME;
202
+ readonly capabilities: AIProviderCapabilities = {
203
+ fork: false,
204
+ resume: false,
205
+ streaming: true,
206
+ tools: true,
207
+ };
208
+ models?: Array<{ id: string; label: string; default?: boolean }>;
209
+
210
+ private config: PiSDKConfig;
211
+ private sessions = new Map<string, PiSDKSession>();
212
+
213
+ constructor(config: PiSDKConfig) {
214
+ this.config = config;
215
+ }
216
+
217
+ async createSession(options: CreateSessionOptions): Promise<PiSDKSession> {
218
+ const session = new PiSDKSession({
219
+ systemPrompt: buildSystemPrompt(options.context),
220
+ cwd: options.cwd ?? this.config.cwd ?? process.cwd(),
221
+ parentSessionId: null,
222
+ piExecutablePath: this.config.piExecutablePath ?? "pi",
223
+ model: options.model ?? this.config.model,
224
+ });
225
+ this.sessions.set(session.id, session);
226
+ return session;
227
+ }
228
+
229
+ async forkSession(): Promise<never> {
230
+ throw new Error(
231
+ "Pi does not support session forking. " +
232
+ "The endpoint layer should fall back to createSession().",
233
+ );
234
+ }
235
+
236
+ async resumeSession(): Promise<never> {
237
+ throw new Error("Pi does not support session resuming.");
238
+ }
239
+
240
+ dispose(): void {
241
+ for (const session of this.sessions.values()) {
242
+ session.killProcess();
243
+ }
244
+ this.sessions.clear();
245
+ }
246
+
247
+ /** Fetch available models from Pi. Call before registering the provider. */
248
+ async fetchModels(): Promise<void> {
249
+ const piPath = this.config.piExecutablePath ?? "pi";
250
+
251
+ let proc: PiProcess | undefined;
252
+
253
+ try {
254
+ proc = new PiProcess();
255
+ await proc.spawn(piPath, this.config.cwd ?? process.cwd());
256
+
257
+ const data = await Promise.race([
258
+ proc.sendAndWait({ type: "get_available_models" }),
259
+ new Promise<never>((_, reject) =>
260
+ setTimeout(() => reject(new Error("Timeout")), 10_000),
261
+ ),
262
+ ]);
263
+
264
+ const rawModels = (
265
+ data as {
266
+ models?: Array<{ provider: string; id: string; name?: string }>;
267
+ }
268
+ ).models;
269
+ if (rawModels && rawModels.length > 0) {
270
+ this.models = rawModels.map((m, i) => ({
271
+ id: `${m.provider}/${m.id}`,
272
+ label: m.name ?? m.id,
273
+ ...(i === 0 && { default: true }),
274
+ }));
275
+ }
276
+ } catch {
277
+ // Pi not configured or no models available
278
+ } finally {
279
+ proc?.kill();
280
+ }
281
+ }
282
+ }
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // Session
286
+ // ---------------------------------------------------------------------------
287
+
288
+ interface SessionConfig {
289
+ systemPrompt: string;
290
+ cwd: string;
291
+ parentSessionId: string | null;
292
+ piExecutablePath: string;
293
+ /** Model in "provider/modelId" format, e.g. "anthropic/claude-haiku-4-5". */
294
+ model?: string;
295
+ }
296
+
297
+ class PiSDKSession extends BaseSession {
298
+ private config: SessionConfig;
299
+ private process: PiProcess | null = null;
300
+
301
+ constructor(config: SessionConfig) {
302
+ super({ parentSessionId: config.parentSessionId });
303
+ this.config = config;
304
+ }
305
+
306
+ async *query(prompt: string): AsyncIterable<AIMessage> {
307
+ const started = this.startQuery();
308
+ if (!started) {
309
+ yield BaseSession.BUSY_ERROR;
310
+ return;
311
+ }
312
+ const { gen } = started;
313
+
314
+ try {
315
+ // Lazy-spawn subprocess
316
+ if (!this.process || !this.process.alive) {
317
+ this.process = new PiProcess();
318
+ await this.process.spawn(this.config.piExecutablePath, this.config.cwd);
319
+
320
+ // Set model if specified (format: "provider/modelId")
321
+ if (this.config.model) {
322
+ const [provider, ...rest] = this.config.model.split("/");
323
+ const modelId = rest.join("/");
324
+ if (provider && modelId) {
325
+ try {
326
+ await this.process.sendAndWait({
327
+ type: "set_model",
328
+ provider,
329
+ modelId,
330
+ });
331
+ } catch {
332
+ // Continue with Pi's default model
333
+ }
334
+ }
335
+ }
336
+
337
+ // Get session ID
338
+ try {
339
+ const state = await this.process.sendAndWait({ type: "get_state" });
340
+ if (typeof state.sessionId === "string") {
341
+ this.resolveId(state.sessionId);
342
+ }
343
+ } catch {
344
+ // Continue with placeholder ID
345
+ }
346
+
347
+ // If subprocess died during startup, surface the error immediately
348
+ if (!this.process.alive) {
349
+ yield {
350
+ type: "error",
351
+ error:
352
+ "Pi process exited during startup. Check that Pi is configured correctly (API keys, models).",
353
+ code: "pi_startup_error",
354
+ };
355
+ return;
356
+ }
357
+ }
358
+
359
+ // Build effective prompt (prepend system prompt on first query)
360
+ const effectivePrompt = buildEffectivePrompt(
361
+ prompt,
362
+ this.config.systemPrompt,
363
+ this._firstQuerySent,
364
+ );
365
+
366
+ // Set up async queue to bridge callback events → async iterable
367
+ const queue: AIMessage[] = [];
368
+ let resolve: (() => void) | null = null;
369
+ let done = false;
370
+
371
+ const push = (msg: AIMessage) => {
372
+ queue.push(msg);
373
+ resolve?.();
374
+ };
375
+
376
+ const finish = () => {
377
+ done = true;
378
+ resolve?.();
379
+ };
380
+
381
+ const unsubscribe = this.process.onEvent((event) => {
382
+ const mapped = mapPiEvent(event, this.id);
383
+ for (const msg of mapped) {
384
+ push(msg);
385
+ if (
386
+ msg.type === "result" ||
387
+ (msg.type === "error" &&
388
+ (event.type === "agent_end" || event.type === "process_exited"))
389
+ ) {
390
+ finish();
391
+ }
392
+ }
393
+ });
394
+
395
+ // Send prompt — use sendAndWait to catch RPC-level rejections
396
+ // (e.g. expired credentials, invalid session)
397
+ try {
398
+ await this.process.sendAndWait({
399
+ type: "prompt",
400
+ message: effectivePrompt,
401
+ });
402
+ } catch (err) {
403
+ unsubscribe();
404
+ yield {
405
+ type: "error",
406
+ error: `Pi rejected prompt: ${err instanceof Error ? err.message : String(err)}`,
407
+ code: "pi_prompt_rejected",
408
+ };
409
+ return;
410
+ }
411
+ this._firstQuerySent = true;
412
+
413
+ // Drain queue
414
+ try {
415
+ while (!done || queue.length > 0) {
416
+ if (queue.length > 0) {
417
+ yield queue.shift()!;
418
+ } else {
419
+ await new Promise<void>((r) => {
420
+ resolve = r;
421
+ });
422
+ resolve = null;
423
+ }
424
+ }
425
+ } finally {
426
+ unsubscribe();
427
+ }
428
+ } catch (err) {
429
+ yield {
430
+ type: "error",
431
+ error: err instanceof Error ? err.message : String(err),
432
+ code: "provider_error",
433
+ };
434
+ } finally {
435
+ this.endQuery(gen);
436
+ }
437
+ }
438
+
439
+ abort(): void {
440
+ if (this.process?.alive) {
441
+ this.process.send({ type: "abort" });
442
+ }
443
+ super.abort();
444
+ }
445
+
446
+ /** Kill the subprocess. Called by the provider on dispose. */
447
+ killProcess(): void {
448
+ this.process?.kill();
449
+ this.process = null;
450
+ }
451
+ }
452
+
453
+ // ---------------------------------------------------------------------------
454
+ // Event mapping — shared with pi-sdk-node.ts
455
+ // ---------------------------------------------------------------------------
456
+
457
+ import { mapPiEvent } from "./pi-events.ts";
458
+ export { mapPiEvent } from "./pi-events.ts";
459
+
460
+ // ---------------------------------------------------------------------------
461
+ // Factory registration
462
+ // ---------------------------------------------------------------------------
463
+
464
+ import { registerProviderFactory } from "../provider.ts";
465
+
466
+ registerProviderFactory(
467
+ PROVIDER_NAME,
468
+ async (config) => new PiSDKProvider(config as PiSDKConfig),
469
+ );
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Session manager — tracks active and historical AI sessions.
3
+ *
4
+ * Each Plannotator server instance (plan review, code review, annotate)
5
+ * gets its own SessionManager. It tracks:
6
+ *
7
+ * - Active sessions (currently streaming or idle but resumable)
8
+ * - The lineage from forked sessions back to their parent
9
+ * - Metadata for UI display (timestamps, mode, status)
10
+ *
11
+ * This is an in-memory store scoped to the server's lifetime. Sessions
12
+ * are not persisted to disk by the manager (the underlying provider
13
+ * handles its own persistence via the agent SDK).
14
+ */
15
+
16
+ import type { AISession, AIContextMode } from "./types.ts";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export interface SessionEntry {
23
+ /** The live session handle (if still active). */
24
+ session: AISession;
25
+ /** What mode this session was created for. */
26
+ mode: AIContextMode;
27
+ /** The parent session ID this was forked from (null if standalone). */
28
+ parentSessionId: string | null;
29
+ /** When this session was created. */
30
+ createdAt: number;
31
+ /** When the last query was sent. */
32
+ lastActiveAt: number;
33
+ /** Short description for UI display (e.g., the user's first question). */
34
+ label?: string;
35
+ }
36
+
37
+ export interface SessionManagerOptions {
38
+ /**
39
+ * Maximum number of sessions to keep in the manager.
40
+ * Oldest idle sessions are evicted when the limit is reached.
41
+ * Default: 20.
42
+ */
43
+ maxSessions?: number;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Implementation
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export class SessionManager {
51
+ private sessions = new Map<string, SessionEntry>();
52
+ private aliases = new Map<string, string>();
53
+ private maxSessions: number;
54
+
55
+ constructor(options: SessionManagerOptions = {}) {
56
+ this.maxSessions = options.maxSessions ?? 20;
57
+ }
58
+
59
+ /**
60
+ * Track a newly created session.
61
+ *
62
+ * If the session supports ID resolution (e.g., the real SDK session ID
63
+ * arrives after the first query), call `remapId()` to update the key.
64
+ */
65
+ track(session: AISession, mode: AIContextMode, label?: string): SessionEntry {
66
+ this.evictIfNeeded();
67
+
68
+ const entry: SessionEntry = {
69
+ session,
70
+ mode,
71
+ parentSessionId: session.parentSessionId,
72
+ createdAt: Date.now(),
73
+ lastActiveAt: Date.now(),
74
+ label,
75
+ };
76
+ this.sessions.set(session.id, entry);
77
+
78
+ // Wire up ID remapping so providers can resolve the real session ID later
79
+ session.onIdResolved = (oldId, newId) => this.remapId(oldId, newId);
80
+
81
+ return entry;
82
+ }
83
+
84
+ /**
85
+ * Remap a session from one ID to another.
86
+ * Used when the real session ID is resolved after initial tracking.
87
+ */
88
+ remapId(oldId: string, newId: string): void {
89
+ const entry = this.sessions.get(oldId);
90
+ if (entry) {
91
+ this.sessions.delete(oldId);
92
+ this.sessions.set(newId, entry);
93
+ // Keep the old ID as an alias so clients using the original ID still work
94
+ this.aliases.set(oldId, newId);
95
+ }
96
+ }
97
+
98
+ /** Resolve an alias to the canonical ID, or return the ID as-is. */
99
+ private resolve(sessionId: string): string {
100
+ return this.aliases.get(sessionId) ?? sessionId;
101
+ }
102
+
103
+ /**
104
+ * Get a tracked session by ID (or alias).
105
+ */
106
+ get(sessionId: string): SessionEntry | undefined {
107
+ return this.sessions.get(this.resolve(sessionId));
108
+ }
109
+
110
+ /**
111
+ * Mark a session as recently active (updates lastActiveAt).
112
+ */
113
+ touch(sessionId: string): void {
114
+ const entry = this.sessions.get(this.resolve(sessionId));
115
+ if (entry) {
116
+ entry.lastActiveAt = Date.now();
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Remove a session from tracking.
122
+ * Does NOT abort the session — call session.abort() first if needed.
123
+ */
124
+ remove(sessionId: string): void {
125
+ const canonical = this.resolve(sessionId);
126
+ this.sessions.delete(canonical);
127
+ // Clean up any aliases pointing to this session
128
+ for (const [alias, target] of this.aliases) {
129
+ if (target === canonical) this.aliases.delete(alias);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * List all tracked sessions, newest first.
135
+ */
136
+ list(): SessionEntry[] {
137
+ return [...this.sessions.values()].sort(
138
+ (a, b) => b.lastActiveAt - a.lastActiveAt
139
+ );
140
+ }
141
+
142
+ /**
143
+ * List sessions forked from a specific parent.
144
+ */
145
+ forksOf(parentSessionId: string): SessionEntry[] {
146
+ return this.list().filter(
147
+ (e) => e.parentSessionId === parentSessionId
148
+ );
149
+ }
150
+
151
+ /**
152
+ * Get the number of tracked sessions.
153
+ */
154
+ get size(): number {
155
+ return this.sessions.size;
156
+ }
157
+
158
+ /**
159
+ * Abort all active sessions and clear tracking.
160
+ */
161
+ disposeAll(): void {
162
+ for (const entry of this.sessions.values()) {
163
+ if (entry.session.isActive) {
164
+ entry.session.abort();
165
+ }
166
+ }
167
+ this.sessions.clear();
168
+ this.aliases.clear();
169
+ }
170
+
171
+ // -------------------------------------------------------------------------
172
+ // Internal
173
+ // -------------------------------------------------------------------------
174
+
175
+ private evictIfNeeded(): void {
176
+ if (this.sessions.size < this.maxSessions) return;
177
+
178
+ // Find the oldest idle session to evict
179
+ let oldest: { id: string; at: number } | null = null;
180
+ for (const [id, entry] of this.sessions) {
181
+ if (entry.session.isActive) continue; // don't evict active sessions
182
+ if (!oldest || entry.lastActiveAt < oldest.at) {
183
+ oldest = { id, at: entry.lastActiveAt };
184
+ }
185
+ }
186
+
187
+ if (oldest) {
188
+ this.sessions.delete(oldest.id);
189
+ // Clean up aliases pointing to the evicted session
190
+ for (const [alias, target] of this.aliases) {
191
+ if (target === oldest.id) this.aliases.delete(alias);
192
+ }
193
+ }
194
+ }
195
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true,
9
+ "allowImportingTsExtensions": true,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "types": ["bun-types"]
13
+ },
14
+ "exclude": ["**/*.test.ts"]
15
+ }