@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,104 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+
4
+ const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
5
+
6
+ async function callExa(toolName: string, args: Record<string, unknown>): Promise<string> {
7
+ const body = JSON.stringify({
8
+ jsonrpc: "2.0",
9
+ id: 1,
10
+ method: "tools/call",
11
+ params: { name: toolName, arguments: args },
12
+ });
13
+
14
+ const res = await fetch(EXA_MCP_URL, {
15
+ method: "POST",
16
+ headers: {
17
+ "Content-Type": "application/json",
18
+ Accept: "application/json, text/event-stream",
19
+ },
20
+ body,
21
+ signal: AbortSignal.timeout(30000),
22
+ });
23
+
24
+ const raw = await res.text();
25
+
26
+ for (const line of raw.split("\n")) {
27
+ if (!line.startsWith("data:")) continue;
28
+ try {
29
+ const json = JSON.parse(line.slice(5).trim());
30
+ if (json.error) throw new Error(json.error.message);
31
+ const text = json.result?.content?.[0]?.text;
32
+ if (text) return text;
33
+ } catch (e: any) {
34
+ if (e.message?.includes("error")) throw e;
35
+ }
36
+ }
37
+
38
+ try {
39
+ const json = JSON.parse(raw);
40
+ if (json.error) throw new Error(json.error.message);
41
+ return json.result?.content?.[0]?.text ?? raw;
42
+ } catch {
43
+ return raw;
44
+ }
45
+ }
46
+
47
+ function ok(text: string) {
48
+ return { content: [{ type: "text" as const, text }], details: {} };
49
+ }
50
+
51
+ function fail(text: string) {
52
+ return { content: [{ type: "text" as const, text }], isError: true as const, details: {} };
53
+ }
54
+
55
+ export function registerExaTools(pi: ExtensionAPI): void {
56
+ pi.registerTool({
57
+ name: "exa_search",
58
+ label: "Exa",
59
+ description:
60
+ "Search the web for any topic. Returns clean text from top results. " +
61
+ "Query tip: describe the ideal page, not keywords. " +
62
+ "'blog post comparing React and Vue performance' not 'React vs Vue'. " +
63
+ "Use category:people for LinkedIn, category:company for company pages.",
64
+ parameters: Type.Object({
65
+ query: Type.String({ description: "Natural language search query" }),
66
+ numResults: Type.Optional(Type.Number({ description: "Number of results (default: 5, max: 100)" })),
67
+ }),
68
+ async execute(_toolCallId, params: any) {
69
+ try {
70
+ const result = await callExa("web_search_exa", {
71
+ query: params.query,
72
+ numResults: params.numResults ?? 5,
73
+ });
74
+ return ok(result);
75
+ } catch (e: any) {
76
+ return fail(`exa_search error: ${e.message}`);
77
+ }
78
+ },
79
+ });
80
+
81
+ pi.registerTool({
82
+ name: "exa_fetch",
83
+ label: "Exa",
84
+ description:
85
+ "Read a webpage's full content as clean markdown. " +
86
+ "Use after exa_search when highlights are insufficient, or to read any URL. " +
87
+ "Batch multiple URLs in one call.",
88
+ parameters: Type.Object({
89
+ urls: Type.Array(Type.String(), { description: "URLs to read" }),
90
+ maxCharacters: Type.Optional(Type.Number({ description: "Max characters per page (default: 3000)" })),
91
+ }),
92
+ async execute(_toolCallId, params: any) {
93
+ try {
94
+ const result = await callExa("web_fetch_exa", {
95
+ urls: params.urls,
96
+ maxCharacters: params.maxCharacters ?? 3000,
97
+ });
98
+ return ok(result);
99
+ } catch (e: any) {
100
+ return fail(`exa_fetch error: ${e.message}`);
101
+ }
102
+ },
103
+ });
104
+ }
@@ -0,0 +1,486 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
5
+ import type { PiPiConfig } from "./config.js";
6
+
7
+ export interface OpenRouterModelData {
8
+ name: string;
9
+ context_length: number;
10
+ max_completion_tokens: number;
11
+ pricing: {
12
+ prompt: number;
13
+ completion: number;
14
+ };
15
+ modality: string;
16
+ }
17
+
18
+ export interface FlantSettings {
19
+ enabled: boolean;
20
+ autoUpdate: boolean;
21
+ cacheTTLDays: number;
22
+ lastUpdated: string | null;
23
+ cachedFlantModels: string[] | null;
24
+ cachedOpenRouterData: Record<string, OpenRouterModelData> | null;
25
+ }
26
+
27
+ const GEMINI_MAP: Record<string, string> = {
28
+ "gemini-3-flash": "google/gemini-3.0-flash",
29
+ "gemini-3.1-flash-lite": "google/gemini-3.1-flash-lite-preview",
30
+ "gemini-3.1-pro": "google/gemini-3.1-pro-preview",
31
+ };
32
+
33
+ function resolveAgentDir(): string {
34
+ const envKey = "PI_CODING_AGENT_DIR";
35
+ const envDir = process.env[envKey];
36
+ if (envDir) {
37
+ if (envDir === "~") return homedir();
38
+ if (envDir.startsWith("~/")) return homedir() + envDir.slice(1);
39
+ return envDir;
40
+ }
41
+ return join(homedir(), ".pi", "agent");
42
+ }
43
+
44
+ const SETTINGS_DIR = join(resolveAgentDir(), "extensions", "pp", "cache");
45
+ const SETTINGS_PATH = join(SETTINGS_DIR, "flant-models.json");
46
+ const DEFAULT_SETTINGS: FlantSettings = {
47
+ enabled: false,
48
+ autoUpdate: true,
49
+ cacheTTLDays: 7,
50
+ lastUpdated: null,
51
+ cachedFlantModels: null,
52
+ cachedOpenRouterData: null,
53
+ };
54
+
55
+ let piRef: ExtensionAPI | null = null;
56
+ let generatedFlantConfig: Partial<PiPiConfig> | null = null;
57
+
58
+ export function setPI(pi: ExtensionAPI): void {
59
+ piRef = pi;
60
+ }
61
+
62
+ export function clearFlantGeneratedConfig(): void {
63
+ generatedFlantConfig = null;
64
+ }
65
+
66
+ export function unregisterFlantProviders(pi?: ExtensionAPI): void {
67
+ const api = pi ?? piRef;
68
+ if (!api) return;
69
+ api.unregisterProvider("pp-flant-anthropic");
70
+ api.unregisterProvider("pp-flant-openai");
71
+ }
72
+
73
+ function ensureSettingsDir(): void {
74
+ if (!existsSync(SETTINGS_DIR)) {
75
+ mkdirSync(SETTINGS_DIR, { recursive: true });
76
+ }
77
+ }
78
+
79
+ function toNumber(value: unknown, fallback = 0): number {
80
+ if (typeof value === "number" && Number.isFinite(value)) return value;
81
+ if (typeof value === "string") {
82
+ const parsed = Number(value);
83
+ if (Number.isFinite(parsed)) return parsed;
84
+ }
85
+ return fallback;
86
+ }
87
+
88
+ function normalizeSettings(raw: unknown): FlantSettings {
89
+ if (!raw || typeof raw !== "object") return { ...DEFAULT_SETTINGS };
90
+ const value = raw as Record<string, unknown>;
91
+ const cacheTTLDays = Math.max(1, Math.round(toNumber(value.cacheTTLDays, DEFAULT_SETTINGS.cacheTTLDays)));
92
+ return {
93
+ enabled: !!value.enabled,
94
+ autoUpdate: value.autoUpdate === undefined ? true : !!value.autoUpdate,
95
+ cacheTTLDays,
96
+ lastUpdated: typeof value.lastUpdated === "string" ? value.lastUpdated : null,
97
+ cachedFlantModels: Array.isArray(value.cachedFlantModels)
98
+ ? value.cachedFlantModels.filter((m): m is string => typeof m === "string")
99
+ : null,
100
+ cachedOpenRouterData: value.cachedOpenRouterData && typeof value.cachedOpenRouterData === "object"
101
+ ? value.cachedOpenRouterData as Record<string, OpenRouterModelData>
102
+ : null,
103
+ };
104
+ }
105
+
106
+ export function loadFlantSettings(): FlantSettings {
107
+ if (!existsSync(SETTINGS_PATH)) return { ...DEFAULT_SETTINGS };
108
+ try {
109
+ const raw = readFileSync(SETTINGS_PATH, "utf-8");
110
+ return normalizeSettings(JSON.parse(raw));
111
+ } catch {
112
+ return { ...DEFAULT_SETTINGS };
113
+ }
114
+ }
115
+
116
+ export function saveFlantSettings(settings: FlantSettings): void {
117
+ ensureSettingsDir();
118
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
119
+ }
120
+
121
+ function toTitleCase(token: string): string {
122
+ const lower = token.toLowerCase();
123
+ if (lower === "gpt") return "GPT";
124
+ if (lower === "o3") return "O3";
125
+ if (lower === "qwen") return "Qwen";
126
+ if (lower === "claude") return "Claude";
127
+ if (lower === "api") return "API";
128
+ if (lower === "ai") return "AI";
129
+ if (!token.length) return token;
130
+ return token.charAt(0).toUpperCase() + token.slice(1);
131
+ }
132
+
133
+ export function generateDisplayName(flantId: string): string {
134
+ const parts = flantId.split("-").filter(Boolean);
135
+ const out: string[] = [];
136
+ let i = 0;
137
+ while (i < parts.length) {
138
+ const part = parts[i];
139
+ if (/^\d+$/.test(part)) {
140
+ const versionParts = [part];
141
+ i += 1;
142
+ while (i < parts.length && /^\d+$/.test(parts[i])) {
143
+ versionParts.push(parts[i]);
144
+ i += 1;
145
+ }
146
+ out.push(versionParts.join("."));
147
+ continue;
148
+ }
149
+ out.push(toTitleCase(part));
150
+ i += 1;
151
+ }
152
+ return out.join(" ");
153
+ }
154
+
155
+ function mapClaudeModelId(modelId: string): string {
156
+ const rest = modelId.slice("claude-".length);
157
+ const parts = rest.split("-").filter(Boolean);
158
+ const firstNumber = parts.findIndex((p) => /^\d+$/.test(p));
159
+ if (firstNumber === -1) return `anthropic/claude-${rest}`;
160
+ const family = parts.slice(0, firstNumber).join("-");
161
+ const version = parts.slice(firstNumber).join(".");
162
+ return family.length > 0
163
+ ? `anthropic/claude-${family}-${version}`
164
+ : `anthropic/claude-${version}`;
165
+ }
166
+
167
+ function normalizeQwenRest(rest: string): string {
168
+ let value = rest;
169
+ if (value.startsWith("-")) value = value.slice(1);
170
+ if (!value) return "default";
171
+ return value;
172
+ }
173
+
174
+ function mapFlantToOpenRouterId(modelId: string): string | null {
175
+ if (GEMINI_MAP[modelId]) return GEMINI_MAP[modelId];
176
+ if (modelId.startsWith("claude-")) return mapClaudeModelId(modelId);
177
+ if (modelId.startsWith("gpt-")) return `openai/${modelId}`;
178
+ if (modelId.startsWith("deepseek-")) return `deepseek/deepseek-${modelId.slice("deepseek-".length)}`;
179
+ if (modelId.startsWith("grok-")) return `x-ai/grok-${modelId.slice("grok-".length)}`;
180
+ if (modelId.startsWith("minimax-")) return `minimax/minimax-${modelId.slice("minimax-".length)}`;
181
+ if (modelId.startsWith("qwen")) return `qwen/qwen-${normalizeQwenRest(modelId.slice("qwen".length))}`;
182
+ if (modelId.startsWith("o3-")) return `openai/o3-${modelId.slice("o3-".length)}`;
183
+ if (modelId.startsWith("sonar-")) return `perplexity/sonar-${modelId.slice("sonar-".length)}`;
184
+ return null;
185
+ }
186
+
187
+ export async function discoverFlantModels(apiKey: string): Promise<string[]> {
188
+ const res = await fetch("https://llm-api.flant.ru/v1/models", {
189
+ headers: { Authorization: `Bearer ${apiKey}` },
190
+ signal: AbortSignal.timeout(30000),
191
+ });
192
+ if (!res.ok) {
193
+ throw new Error(`Flant API returned ${res.status}`);
194
+ }
195
+ const payload = await res.json() as { data?: Array<{ id?: unknown }> };
196
+ const models = (payload.data ?? [])
197
+ .map((m) => (typeof m.id === "string" ? m.id : ""))
198
+ .filter((id) => id.length > 0 && !id.startsWith("or/"));
199
+ return [...new Set(models)];
200
+ }
201
+
202
+ export async function fetchOpenRouterMetadata(modelIds: string[]): Promise<Record<string, OpenRouterModelData>> {
203
+ const mapping = new Map<string, string>();
204
+ for (const modelId of modelIds) {
205
+ const mapped = mapFlantToOpenRouterId(modelId);
206
+ if (mapped) mapping.set(modelId, mapped);
207
+ }
208
+ if (mapping.size === 0) return {};
209
+
210
+ const res = await fetch("https://openrouter.ai/api/v1/models", {
211
+ signal: AbortSignal.timeout(30000),
212
+ });
213
+ if (!res.ok) {
214
+ throw new Error(`OpenRouter API returned ${res.status}`);
215
+ }
216
+
217
+ const payload = await res.json() as { data?: Array<Record<string, unknown>> };
218
+ const modelMap = new Map<string, Record<string, unknown>>();
219
+ for (const item of payload.data ?? []) {
220
+ const id = typeof item.id === "string" ? item.id : "";
221
+ if (id) modelMap.set(id, item);
222
+ }
223
+
224
+ const out: Record<string, OpenRouterModelData> = {};
225
+ for (const [flantModelId, openRouterId] of mapping.entries()) {
226
+ const model = modelMap.get(openRouterId);
227
+ if (!model) continue;
228
+ const pricing = model.pricing && typeof model.pricing === "object"
229
+ ? model.pricing as Record<string, unknown>
230
+ : {};
231
+ const architecture = model.architecture && typeof model.architecture === "object"
232
+ ? model.architecture as Record<string, unknown>
233
+ : {};
234
+ const topProvider = model.top_provider && typeof model.top_provider === "object"
235
+ ? model.top_provider as Record<string, unknown>
236
+ : {};
237
+ out[flantModelId] = {
238
+ name: typeof model.name === "string" ? model.name : generateDisplayName(flantModelId),
239
+ context_length: toNumber(model.context_length, 200000),
240
+ max_completion_tokens: toNumber(topProvider.max_completion_tokens, 32000),
241
+ pricing: {
242
+ prompt: toNumber(pricing.prompt, 0),
243
+ completion: toNumber(pricing.completion, 0),
244
+ },
245
+ modality: typeof architecture.modality === "string" ? architecture.modality : "text",
246
+ };
247
+ }
248
+ return out;
249
+ }
250
+
251
+ function modelSpec(modelId: string): string {
252
+ const provider = modelId.startsWith("claude-") ? "pp-flant-anthropic" : "pp-flant-openai";
253
+ return `${provider}/${modelId}`;
254
+ }
255
+
256
+ function buildProviderModelConfig(
257
+ flantModelId: string,
258
+ metadata: Record<string, OpenRouterModelData>,
259
+ ): ProviderModelConfig {
260
+ const modelMeta = metadata[flantModelId];
261
+ const modality = (modelMeta?.modality ?? "text").toLowerCase();
262
+ return {
263
+ id: flantModelId,
264
+ name: modelMeta?.name ?? generateDisplayName(flantModelId),
265
+ reasoning: true,
266
+ input: modality.includes("image") ? ["text", "image"] : ["text"],
267
+ cost: {
268
+ input: toNumber(modelMeta?.pricing.prompt, 0) * 1_000_000,
269
+ output: toNumber(modelMeta?.pricing.completion, 0) * 1_000_000,
270
+ cacheRead: 0,
271
+ cacheWrite: 0,
272
+ },
273
+ contextWindow: toNumber(modelMeta?.context_length, 200000),
274
+ maxTokens: toNumber(modelMeta?.max_completion_tokens, 32000),
275
+ };
276
+ }
277
+
278
+ export function registerFlantProviders(
279
+ pi: ExtensionAPI,
280
+ models: string[],
281
+ metadata: Record<string, OpenRouterModelData>,
282
+ ): void {
283
+ const uniqueModels = [...new Set(models)];
284
+ const anthropicModels = uniqueModels.filter((m) => m.startsWith("claude-"));
285
+ const openaiModels = uniqueModels.filter((m) => !m.startsWith("claude-"));
286
+
287
+ pi.registerProvider("pp-flant-anthropic", {
288
+ api: "anthropic-messages",
289
+ baseUrl: "https://llm-api.flant.ru",
290
+ apiKey: "$FLANT_API_KEY",
291
+ models: anthropicModels.map((m) => buildProviderModelConfig(m, metadata)),
292
+ });
293
+
294
+ pi.registerProvider("pp-flant-openai", {
295
+ api: "openai-completions",
296
+ baseUrl: "https://llm-api.flant.ru/v1",
297
+ apiKey: "$FLANT_API_KEY",
298
+ models: openaiModels.map((m) => buildProviderModelConfig(m, metadata)),
299
+ });
300
+ }
301
+
302
+ function compareModelVersion(a: string, b: string): number {
303
+ const aParts = (a.match(/\d+/g) ?? []).map(Number);
304
+ const bParts = (b.match(/\d+/g) ?? []).map(Number);
305
+ const len = Math.max(aParts.length, bParts.length);
306
+ for (let i = 0; i < len; i++) {
307
+ const ai = aParts[i] ?? 0;
308
+ const bi = bParts[i] ?? 0;
309
+ if (ai !== bi) return ai - bi;
310
+ }
311
+ return a.localeCompare(b);
312
+ }
313
+
314
+ function pickLatest(models: string[]): string | null {
315
+ if (models.length === 0) return null;
316
+ return models
317
+ .slice()
318
+ .sort((a, b) => compareModelVersion(b, a))[0] ?? null;
319
+ }
320
+
321
+ function pickCheapestFastModel(models: string[]): string | null {
322
+ const geminiFlashLite = pickLatest(models.filter((m) => /^gemini-.*flash-lite/.test(m)));
323
+ if (geminiFlashLite) return geminiFlashLite;
324
+ const geminiFlash = pickLatest(models.filter((m) => /^gemini-.*flash/.test(m) && !m.includes("flash-lite")));
325
+ if (geminiFlash) return geminiFlash;
326
+ const gptMini = pickLatest(models.filter((m) => /^gpt-5.*-mini$/.test(m)));
327
+ if (gptMini) return gptMini;
328
+ return pickLatest(models.filter((m) => /^claude-haiku-/.test(m)));
329
+ }
330
+
331
+ function makeVariant(modelId: string | null, fallbackModelId: string): { enabled: boolean; model: string; thinking: string } {
332
+ if (!modelId) return { enabled: false, model: modelSpec(fallbackModelId), thinking: "high" };
333
+ return { enabled: true, model: modelSpec(modelId), thinking: "high" };
334
+ }
335
+
336
+ export function generateFlantConfig(models: string[]): Partial<PiPiConfig> {
337
+ const uniqueModels = [...new Set(models)];
338
+ if (uniqueModels.length === 0) return {};
339
+
340
+ const latestOpus = pickLatest(uniqueModels.filter((m) => /^claude-opus-/.test(m)));
341
+ const latestClaude = pickLatest(uniqueModels.filter((m) => /^claude-/.test(m)));
342
+ const latestGpt5 = pickLatest(uniqueModels.filter((m) => /^gpt-5/.test(m) && !m.endsWith("-mini") && !m.endsWith("-codex")));
343
+ const latestGpt = latestGpt5 ?? pickLatest(uniqueModels.filter((m) => /^gpt-/.test(m) && !m.endsWith("-mini") && !m.endsWith("-codex")));
344
+ const latestGeminiPro = pickLatest(uniqueModels.filter((m) => /^gemini-.*-pro$/.test(m)));
345
+ const latestDeepseek = pickLatest(uniqueModels.filter((m) => /^deepseek-/.test(m)));
346
+ const latestGrok = pickLatest(uniqueModels.filter((m) => /^grok-/.test(m)));
347
+ const fastest = pickCheapestFastModel(uniqueModels);
348
+
349
+ const fallback = latestOpus ?? latestClaude ?? latestGpt ?? latestGeminiPro ?? latestDeepseek ?? latestGrok ?? uniqueModels[0];
350
+ const implementModel = latestOpus ?? latestClaude ?? fallback;
351
+ const debugModel = latestGpt ?? latestGeminiPro ?? latestDeepseek ?? fallback;
352
+ const brainstormModel = latestOpus ?? latestClaude ?? fallback;
353
+ const taskModel = latestOpus ?? latestClaude ?? fallback;
354
+ const fastModel = fastest ?? debugModel;
355
+
356
+ return {
357
+ mainModel: {
358
+ implement: { model: modelSpec(implementModel), thinking: "high" },
359
+ debug: { model: modelSpec(debugModel), thinking: "high" },
360
+ brainstorm: { model: modelSpec(brainstormModel), thinking: "high" },
361
+ },
362
+ planners: {
363
+ opus: makeVariant(latestOpus, fallback),
364
+ gpt: makeVariant(latestGpt, fallback),
365
+ gemini: makeVariant(latestGeminiPro, fallback),
366
+ },
367
+ planReviewers: {
368
+ opus: makeVariant(latestOpus, fallback),
369
+ gpt: makeVariant(latestGpt, fallback),
370
+ gemini: makeVariant(latestGeminiPro, fallback),
371
+ },
372
+ codeReviewers: {
373
+ opus: makeVariant(latestOpus, fallback),
374
+ gpt: makeVariant(latestGpt, fallback),
375
+ gemini: makeVariant(latestGeminiPro, fallback),
376
+ },
377
+ brainstormReviewers: {
378
+ opus: makeVariant(latestOpus, fallback),
379
+ gpt: makeVariant(latestGpt, fallback),
380
+ gemini: makeVariant(latestGeminiPro, fallback),
381
+ },
382
+ agents: {
383
+ explore: { model: modelSpec(fastModel), thinking: "low" },
384
+ librarian: { model: modelSpec(fastModel), thinking: "medium" },
385
+ task: { model: modelSpec(taskModel), thinking: "medium" },
386
+ },
387
+ };
388
+ }
389
+
390
+ function isCacheValid(settings: FlantSettings): boolean {
391
+ if (!settings.lastUpdated || !settings.cachedFlantModels || !settings.cachedOpenRouterData) return false;
392
+ const updatedAt = new Date(settings.lastUpdated).getTime();
393
+ if (!Number.isFinite(updatedAt)) return false;
394
+ const ttlMs = Math.max(1, settings.cacheTTLDays) * 24 * 60 * 60 * 1000;
395
+ return Date.now() - updatedAt < ttlMs;
396
+ }
397
+
398
+ export function getFlantGeneratedConfig(): Partial<PiPiConfig> | null {
399
+ return generatedFlantConfig;
400
+ }
401
+
402
+ (globalThis as any)[Symbol.for("pi-pi:flant-config")] = getFlantGeneratedConfig;
403
+
404
+ export async function updateFlantInfra(
405
+ pi: ExtensionAPI,
406
+ ): Promise<{ ok: boolean; error?: string; models?: string[] }> {
407
+ setPI(pi);
408
+ const settings = loadFlantSettings();
409
+
410
+ let models = isCacheValid(settings) ? settings.cachedFlantModels : null;
411
+ let metadata = isCacheValid(settings) ? settings.cachedOpenRouterData : null;
412
+ let refreshed = false;
413
+
414
+ if (!models || !metadata) {
415
+ const apiKey = process.env.FLANT_API_KEY;
416
+ if (!apiKey) {
417
+ if (settings.cachedFlantModels && settings.cachedOpenRouterData) {
418
+ models = settings.cachedFlantModels;
419
+ metadata = settings.cachedOpenRouterData;
420
+ } else {
421
+ return { ok: false, error: "FLANT_API_KEY is not set" };
422
+ }
423
+ } else {
424
+ try {
425
+ models = await discoverFlantModels(apiKey);
426
+ try {
427
+ metadata = await fetchOpenRouterMetadata(models);
428
+ } catch {
429
+ metadata = settings.cachedOpenRouterData ?? {};
430
+ }
431
+ settings.cachedFlantModels = models;
432
+ settings.cachedOpenRouterData = metadata;
433
+ settings.lastUpdated = new Date().toISOString();
434
+ saveFlantSettings(settings);
435
+ refreshed = true;
436
+ } catch (err: any) {
437
+ if (settings.cachedFlantModels && settings.cachedOpenRouterData) {
438
+ models = settings.cachedFlantModels;
439
+ metadata = settings.cachedOpenRouterData;
440
+ } else {
441
+ return { ok: false, error: err?.message ?? "Failed to update Flant infrastructure" };
442
+ }
443
+ }
444
+ }
445
+ }
446
+
447
+ if (!models || !metadata) {
448
+ return { ok: false, error: "No Flant model data available" };
449
+ }
450
+
451
+ try {
452
+ registerFlantProviders(pi, models, metadata);
453
+ generatedFlantConfig = generateFlantConfig(models);
454
+ if (!refreshed && settings.cachedFlantModels && settings.cachedOpenRouterData && !settings.lastUpdated) {
455
+ settings.lastUpdated = new Date().toISOString();
456
+ saveFlantSettings(settings);
457
+ }
458
+ return { ok: true, models };
459
+ } catch (err: any) {
460
+ return { ok: false, error: err?.message ?? "Failed to register Flant providers" };
461
+ }
462
+ }
463
+
464
+ export function initFlantSync(pi: ExtensionAPI): void {
465
+ setPI(pi);
466
+ const settings = loadFlantSettings();
467
+ if (!settings.enabled) {
468
+ generatedFlantConfig = null;
469
+ return;
470
+ }
471
+ if (settings.cachedFlantModels && settings.cachedOpenRouterData) {
472
+ registerFlantProviders(pi, settings.cachedFlantModels, settings.cachedOpenRouterData);
473
+ generatedFlantConfig = generateFlantConfig(settings.cachedFlantModels);
474
+ }
475
+ }
476
+
477
+ export async function initFlantOnStartup(pi: ExtensionAPI): Promise<void> {
478
+ setPI(pi);
479
+ const settings = loadFlantSettings();
480
+ if (!settings.enabled) {
481
+ generatedFlantConfig = null;
482
+ return;
483
+ }
484
+ if (!settings.autoUpdate) return;
485
+ await updateFlantInfra(pi);
486
+ }
@@ -0,0 +1,80 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { resolve } from "path";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import { Orchestrator } from "./orchestrator.js";
5
+ import { registerCommandHandlers } from "./command-handlers.js";
6
+ import { registerEventHandlers } from "./event-handlers.js";
7
+ import { registerCbmTools } from "./cbm.js";
8
+ import { registerExaTools } from "./exa.js";
9
+ import { registerAstSearchTool } from "./ast-search.js";
10
+ import { validatePlan, validateArtifact } from "./validate-artifacts.js";
11
+ import { initFlantSync } from "./flant-infra.js";
12
+
13
+ const ORCHESTRATOR_KEY = Symbol.for("pi-pi:orchestrator-initialized");
14
+ export const SUBAGENT_SESSION_KEY = Symbol.for("pi-pi:subagent-session");
15
+
16
+ export default function (pi: ExtensionAPI) {
17
+ if ((globalThis as any)[ORCHESTRATOR_KEY]) {
18
+ (globalThis as any)[SUBAGENT_SESSION_KEY] = true;
19
+ registerSubagentTools(pi);
20
+ return;
21
+ }
22
+ (globalThis as any)[ORCHESTRATOR_KEY] = true;
23
+
24
+ initFlantSync(pi);
25
+
26
+ const orchestrator = new Orchestrator(pi);
27
+ registerEventHandlers(orchestrator);
28
+ registerCommandHandlers(orchestrator);
29
+ }
30
+
31
+ function registerSubagentTools(pi: ExtensionAPI): void {
32
+ const cwd = process.cwd();
33
+ registerCbmTools(pi, cwd);
34
+ registerExaTools(pi);
35
+ registerAstSearchTool(pi, cwd);
36
+
37
+ pi.on("tool_result", async (event) => {
38
+ if ((event.toolName !== "write" && event.toolName !== "edit") || event.isError) return;
39
+
40
+ const input = event.input as { file_path?: string; filePath?: string; path?: string };
41
+ const filePath = input.file_path || input.filePath || input.path;
42
+ if (!filePath) return;
43
+
44
+ const resolved = resolve(cwd, filePath);
45
+ if (!resolved.endsWith(".md")) return;
46
+ if (!existsSync(resolved)) return;
47
+
48
+ if (resolved.includes("/plans/") && !resolved.includes("synthesized") && !resolved.includes("review_")) {
49
+ const content = readFileSync(resolved, "utf-8");
50
+ const result = validatePlan(content);
51
+ if (!result.ok) {
52
+ return {
53
+ content: [
54
+ ...event.content,
55
+ {
56
+ type: "text" as const,
57
+ text: `\n\n<validation-error>\nPlan structure is invalid:\n${result.errors.map((e) => `- ${e}`).join("\n")}\n\nFix immediately. Required structure:\n# Plan\n## Scope\n<2-4 lines>\n## Checklist\n- [ ] <outcome> — Done when: <observable condition>\n## Blockers (optional)\n<issues>\n\nRewrite the file now.\n</validation-error>`,
58
+ },
59
+ ],
60
+ };
61
+ }
62
+ }
63
+
64
+ if (resolved.includes("/artifacts/")) {
65
+ const content = readFileSync(resolved, "utf-8");
66
+ const result = validateArtifact(content);
67
+ if (!result.ok) {
68
+ return {
69
+ content: [
70
+ ...event.content,
71
+ {
72
+ type: "text" as const,
73
+ text: `\n\n<validation-error>\nArtifact structure is invalid:\n${result.errors.map((e) => `- ${e}`).join("\n")}\n\nFix immediately. Artifact files must start with # <Title>.\n</validation-error>`,
74
+ },
75
+ ],
76
+ };
77
+ }
78
+ }
79
+ });
80
+ }