@akanjs/devkit 3.0.0-beta.9 → 3.0.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 (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +21 -7
  3. package/README.md +17 -8
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +439 -141
  19. package/akanConfig/akanConfig.ts +359 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +22 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +182 -0
  241. package/mobile/desktopBin.ts +144 -0
  242. package/mobile/desktopServerStage.test.ts +252 -0
  243. package/mobile/desktopServerStage.ts +178 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +184 -0
  250. package/mobile/nativeApp.ts +454 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +438 -0
  254. package/mobile/nativeConfig.ts +294 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +9 -25
  298. package/subspace.ts +48 -115
  299. package/subspaceConfig.ts +3 -11
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
@@ -0,0 +1,199 @@
1
+ import type { ExtensionAPI, InlineExtension } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ type CodeAgentMcpServerRef,
4
+ type CodeAgentMcpStatus,
5
+ type CodeAgentProfile,
6
+ codeAgentClip,
7
+ codeAgentOutputChars,
8
+ } from "akanjs/common";
9
+ import { Type } from "typebox";
10
+ import { McpClient, type McpToolInfo, McpUnauthorized } from "./McpClient";
11
+ import { McpServerConfig } from "./McpServerConfig";
12
+ import { McpSignIn } from "./McpSignIn";
13
+
14
+ export interface McpToolPackOptions {
15
+ workspaceRoot: string;
16
+ profile: CodeAgentProfile;
17
+ onNotice?: (message: string) => void;
18
+ }
19
+
20
+ interface McpToolPackServer {
21
+ client: McpClient;
22
+ tools: McpToolInfo[];
23
+ token: string | undefined;
24
+ status: CodeAgentMcpStatus;
25
+ renewing: Promise<McpClient | undefined> | undefined;
26
+ refused: McpClient | undefined;
27
+ }
28
+
29
+ // Tool names carry the server prefix: two servers offering `search` is the normal case, and a shadow is invisible.
30
+ export class McpToolPack {
31
+ readonly #clients: McpToolPackServer[] = [];
32
+ readonly #status: CodeAgentMcpStatus[] = [];
33
+ readonly #onNotice: ((message: string) => void) | undefined;
34
+
35
+ private constructor(onNotice: ((message: string) => void) | undefined) {
36
+ this.#onNotice = onNotice;
37
+ }
38
+
39
+ /** Undefined only when nothing is declared; a pack whose every server failed still comes back to say why. */
40
+ static async connect(options: McpToolPackOptions) {
41
+ const refs = McpToolPack.#refs(options);
42
+ if (!refs.length) return undefined;
43
+ const pack = new McpToolPack(options.onNotice);
44
+ for (const ref of refs) {
45
+ const base = { name: ref.name, transport: ref.transport, target: McpServerConfig.targetOf(ref) };
46
+ // Never an interactive sign-in: connect runs inside `CodeAgent.create`, before any screen is drawn.
47
+ const token = ref.transport === "http" ? await McpSignIn.token(ref, options.onNotice) : undefined;
48
+ try {
49
+ const opened = await McpToolPack.#openRefreshing(ref, token, options.onNotice);
50
+ const status: CodeAgentMcpStatus = {
51
+ ...base,
52
+ tools: opened.tools.map((tool) => McpToolPack.#nameOf(ref.name, tool.name)),
53
+ auth: opened.token ? "authorized" : "none",
54
+ };
55
+ pack.#clients.push({ ...opened, status, renewing: undefined, refused: undefined });
56
+ pack.#status.push(status);
57
+ } catch (error) {
58
+ if (error instanceof McpUnauthorized) {
59
+ options.onNotice?.(`MCP server "${ref.name}" needs signing in — /mcp login ${ref.name}`);
60
+ pack.#status.push({ ...base, tools: [], auth: "required" });
61
+ continue;
62
+ }
63
+ // One unreachable integration must not cost the agent its turn, so it costs only its own tools.
64
+ options.onNotice?.(`MCP server "${ref.name}" is unavailable: ${String(error)}`);
65
+ pack.#status.push({ ...base, tools: [], error: String(error) });
66
+ }
67
+ }
68
+ return pack;
69
+ }
70
+
71
+ // A token refused before its stated expiry, or issued with none, gets one refresh before a sign-in is asked for.
72
+ static async #openRefreshing(
73
+ ref: CodeAgentMcpServerRef,
74
+ token: string | undefined,
75
+ onNotice: ((message: string) => void) | undefined,
76
+ ) {
77
+ try {
78
+ return await McpToolPack.#open(ref, token);
79
+ } catch (error) {
80
+ if (!(error instanceof McpUnauthorized) || !token) throw error;
81
+ const retry = await McpSignIn.retryToken(ref, token, onNotice);
82
+ if (!retry) throw error;
83
+ return await McpToolPack.#open(ref, retry);
84
+ }
85
+ }
86
+
87
+ static async #open(ref: CodeAgentMcpServerRef, token: string | undefined) {
88
+ const client = await new McpClient(ref, token).connect();
89
+ return { client, tools: await client.listTools(), token };
90
+ }
91
+
92
+ get toolNames() {
93
+ return this.#clients.flatMap(({ client, tools }) =>
94
+ tools.map((tool) => McpToolPack.#nameOf(client.ref.name, tool.name)),
95
+ );
96
+ }
97
+
98
+ get status(): CodeAgentMcpStatus[] {
99
+ return this.#status;
100
+ }
101
+
102
+ extension(): InlineExtension {
103
+ return { name: "akan-mcp", factory: (pi: ExtensionAPI) => this.#register(pi) };
104
+ }
105
+
106
+ close() {
107
+ for (const { client } of this.#clients) client.close();
108
+ }
109
+
110
+ #register(pi: ExtensionAPI) {
111
+ for (const server of this.#clients)
112
+ for (const tool of server.tools) {
113
+ const name = McpToolPack.#nameOf(server.client.ref.name, tool.name);
114
+ const description = tool.description ?? tool.name;
115
+ pi.registerTool({
116
+ name,
117
+ label: `${server.client.ref.name}: ${tool.name}`,
118
+ description,
119
+ promptSnippet: `${name}: ${description.split(".")[0] ?? description}`,
120
+ // The server's own schema goes through untouched; it is the party that validates the call.
121
+ parameters: Type.Unsafe<Record<string, unknown>>(tool.inputSchema),
122
+ execute: async (_id, params) => {
123
+ const result = await this.#call(server, tool.name, params ?? {});
124
+ return {
125
+ content: [{ type: "text", text: codeAgentClip(result.text, codeAgentOutputChars) }],
126
+ details: undefined,
127
+ isError: result.isError,
128
+ };
129
+ },
130
+ });
131
+ }
132
+ }
133
+
134
+ async #call(server: McpToolPackServer, tool: string, args: Record<string, unknown>) {
135
+ const client = server.client;
136
+ try {
137
+ return await client.callTool(tool, args);
138
+ } catch (error) {
139
+ if (!(error instanceof McpUnauthorized)) throw error;
140
+ }
141
+ const renewed = await this.#renewed(server, client);
142
+ try {
143
+ if (renewed) return await renewed.callTool(tool, args);
144
+ } catch (error) {
145
+ if (!(error instanceof McpUnauthorized)) throw error;
146
+ McpToolPack.#refuse(server, renewed);
147
+ }
148
+ throw new Error(`MCP server "${client.ref.name}" needs signing in — /mcp login ${client.ref.name}`);
149
+ }
150
+
151
+ // Reported as a failed connect is; only a new pack, after `/mcp login` or `/mcp reload`, reads `authorized` again.
152
+ static #refuse(server: McpToolPackServer, client: McpClient | undefined) {
153
+ server.refused = client;
154
+ server.status.auth = "required";
155
+ }
156
+
157
+ // One refresh per refused token, shared by every call it refused; a server that refuses every token costs one.
158
+ async #renewed(server: McpToolPackServer, refused: McpClient) {
159
+ if (server.client === refused && server.refused !== refused)
160
+ server.renewing ??= this.#renew(server, refused).finally(() => {
161
+ server.renewing = undefined;
162
+ });
163
+ if (server.renewing) return await server.renewing;
164
+ return server.client === server.refused ? undefined : server.client;
165
+ }
166
+
167
+ async #renew(server: McpToolPackServer, refused: McpClient) {
168
+ const token = server.token ? await McpSignIn.retryToken(refused.ref, server.token, this.#onNotice) : undefined;
169
+ const client = token ? await McpToolPack.#reopen(refused.ref, token) : undefined;
170
+ if (!client) {
171
+ McpToolPack.#refuse(server, refused);
172
+ return undefined;
173
+ }
174
+ refused.close();
175
+ server.client = client;
176
+ server.token = token;
177
+ return client;
178
+ }
179
+
180
+ // A new client, so a new streamable HTTP session: a server may bind its session to the token that opened it.
181
+ static async #reopen(ref: CodeAgentMcpServerRef, token: string) {
182
+ try {
183
+ return await new McpClient(ref, token).connect();
184
+ } catch (error) {
185
+ if (error instanceof McpUnauthorized) return undefined;
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ static #nameOf(server: string, tool: string) {
191
+ return `mcp__${server.replace(/[^a-zA-Z0-9_]/g, "_")}__${tool}`;
192
+ }
193
+
194
+ static #refs(options: McpToolPackOptions): CodeAgentMcpServerRef[] {
195
+ if (options.profile.tools.mcp === "off") return [];
196
+ const declared = Array.isArray(options.profile.tools.mcp) ? options.profile.tools.mcp : [];
197
+ return [...McpServerConfig.read(options.workspaceRoot), ...declared];
198
+ }
199
+ }
@@ -0,0 +1,91 @@
1
+ import type { ExtensionAPI, InlineExtension, SessionInfo } from "@earendil-works/pi-coding-agent";
2
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
3
+ import { type CodeAgentProfile, codeAgentClip, codeAgentOutputChars } from "akanjs/common";
4
+ import { Type } from "typebox";
5
+ import { akanCodePaths } from "../agent/akanCodePaths";
6
+
7
+ export interface SessionToolPackOptions {
8
+ workspaceRoot: string;
9
+ cwd: string;
10
+ profile: CodeAgentProfile;
11
+ /** Excluded from results: reading your own transcript back is a way to spend a context window twice. */
12
+ currentSessionId: () => string;
13
+ }
14
+
15
+ const snippetChars = 240;
16
+
17
+ export class SessionToolPack {
18
+ readonly #options: SessionToolPackOptions;
19
+
20
+ constructor(options: SessionToolPackOptions) {
21
+ this.#options = options;
22
+ }
23
+
24
+ static readonly toolNames = ["session_search", "session_read"];
25
+
26
+ names() {
27
+ return this.#options.profile.session.crossSession ? SessionToolPack.toolNames : [];
28
+ }
29
+
30
+ extension(): InlineExtension | undefined {
31
+ if (!this.names().length) return undefined;
32
+ return { name: "akan-sessions", factory: (pi: ExtensionAPI) => this.#register(pi) };
33
+ }
34
+
35
+ #register(pi: ExtensionAPI) {
36
+ pi.registerTool({
37
+ name: "session_search",
38
+ label: "Search sessions",
39
+ description:
40
+ "Search earlier coding-agent conversations in this workspace for a phrase, and get back the matching sessions with a snippet. Use it when the user refers to something decided before, or when you suspect this problem has been solved here already.",
41
+ promptSnippet: "session_search: find earlier conversations in this workspace by phrase",
42
+ parameters: Type.Object({
43
+ query: Type.String({ description: "A phrase to look for, matched case-insensitively." }),
44
+ limit: Type.Optional(Type.Number({ description: "How many sessions to return, default 5." })),
45
+ }),
46
+ execute: async (_id, params) => {
47
+ const matches = await this.#search(params.query, params.limit ?? 5);
48
+ return { content: [{ type: "text", text: matches }], details: undefined };
49
+ },
50
+ });
51
+ pi.registerTool({
52
+ name: "session_read",
53
+ label: "Read session",
54
+ description: "Read one earlier conversation, found with session_search, as plain text.",
55
+ promptSnippet: "session_read: read one earlier conversation by id",
56
+ parameters: Type.Object({ sessionId: Type.String() }),
57
+ execute: async (_id, params) => {
58
+ const text = await this.#read(params.sessionId);
59
+ return { content: [{ type: "text", text }], details: undefined, isError: text.startsWith("No session") };
60
+ },
61
+ });
62
+ }
63
+
64
+ async #search(query: string, limit: number) {
65
+ const needle = query.toLowerCase();
66
+ const sessions = (await this.#list()).filter(
67
+ (session) =>
68
+ session.id !== this.#options.currentSessionId() && session.allMessagesText.toLowerCase().includes(needle),
69
+ );
70
+ if (!sessions.length) return `No earlier session mentions "${query}".`;
71
+ return sessions
72
+ .slice(0, limit)
73
+ .map((session) => {
74
+ const at = session.allMessagesText.toLowerCase().indexOf(needle);
75
+ const snippet = session.allMessagesText.slice(Math.max(0, at - 80), at + snippetChars).replace(/\s+/g, " ");
76
+ return `- ${session.id} (${session.modified.toISOString().slice(0, 10)}${session.name ? `, "${session.name}"` : ""})\n …${snippet}…`;
77
+ })
78
+ .join("\n");
79
+ }
80
+
81
+ async #read(sessionId: string) {
82
+ const session = (await this.#list()).find((entry) => entry.id === sessionId);
83
+ if (!session) return `No session with id ${sessionId} in this workspace.`;
84
+ return codeAgentClip(session.allMessagesText, codeAgentOutputChars * 2);
85
+ }
86
+
87
+ // This workspace's directory only: a global list would reach another checkout's conversations.
88
+ async #list(): Promise<SessionInfo[]> {
89
+ return await SessionManager.list(this.#options.cwd, akanCodePaths.sessionsDir(this.#options.workspaceRoot));
90
+ }
91
+ }
@@ -0,0 +1,147 @@
1
+ import type { ExtensionAPI, InlineExtension } from "@earendil-works/pi-coding-agent";
2
+ import { type CodeAgentProfile, codeAgentClip, codeAgentOutputChars } from "akanjs/common";
3
+ import { Type } from "typebox";
4
+
5
+ export interface WebToolPackOptions {
6
+ profile: CodeAgentProfile;
7
+ }
8
+
9
+ interface SearchProvider {
10
+ name: string;
11
+ envKey: string;
12
+ search(key: string, query: string, count: number): Promise<string>;
13
+ }
14
+
15
+ // web_search only with a provider key set: a tool that can never work costs prompt tokens and invites retries.
16
+ export class WebToolPack {
17
+ readonly #options: WebToolPackOptions;
18
+
19
+ constructor(options: WebToolPackOptions) {
20
+ this.#options = options;
21
+ }
22
+
23
+ static providers: SearchProvider[] = [
24
+ {
25
+ name: "brave",
26
+ envKey: "BRAVE_API_KEY",
27
+ search: async (key, query, count) => {
28
+ const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`;
29
+ const response = await fetch(url, { headers: { "x-subscription-token": key, accept: "application/json" } });
30
+ const body = (await response.json()) as {
31
+ web?: { results?: { title: string; url: string; description: string }[] };
32
+ };
33
+ return (body.web?.results ?? []).map((r) => `- ${r.title}\n ${r.url}\n ${r.description}`).join("\n");
34
+ },
35
+ },
36
+ {
37
+ name: "tavily",
38
+ envKey: "TAVILY_API_KEY",
39
+ search: async (key, query, count) => {
40
+ const response = await fetch("https://api.tavily.com/search", {
41
+ method: "POST",
42
+ headers: { "content-type": "application/json" },
43
+ body: JSON.stringify({ api_key: key, query, max_results: count }),
44
+ });
45
+ const body = (await response.json()) as { results?: { title: string; url: string; content: string }[] };
46
+ return (body.results ?? []).map((r) => `- ${r.title}\n ${r.url}\n ${r.content}`).join("\n");
47
+ },
48
+ },
49
+ ];
50
+
51
+ /** Names the session allowlist must carry. Empty when the profile, or the environment, offers nothing. */
52
+ names() {
53
+ const names: string[] = [];
54
+ if (this.#options.profile.tools.web.fetch) names.push("web_fetch");
55
+ if (this.#options.profile.tools.web.search && WebToolPack.#provider()) names.push("web_search");
56
+ return names;
57
+ }
58
+
59
+ extension(): InlineExtension | undefined {
60
+ if (!this.names().length) return undefined;
61
+ return { name: "akan-web", factory: (pi: ExtensionAPI) => this.#register(pi) };
62
+ }
63
+
64
+ #register(pi: ExtensionAPI) {
65
+ if (this.#options.profile.tools.web.fetch) this.#registerFetch(pi);
66
+ const provider = WebToolPack.#provider();
67
+ if (this.#options.profile.tools.web.search && provider) this.#registerSearch(pi, provider);
68
+ }
69
+
70
+ #registerFetch(pi: ExtensionAPI) {
71
+ const allowHosts = this.#options.profile.network.allowHosts;
72
+ pi.registerTool({
73
+ name: "web_fetch",
74
+ label: "Fetch",
75
+ description: "Fetch a URL and return its readable text. HTML is stripped to text; the result is truncated.",
76
+ promptSnippet: "web_fetch: fetch a URL and return its readable text",
77
+ parameters: Type.Object({ url: Type.String({ description: "Absolute http(s) URL." }) }),
78
+ execute: async (_id, params) => {
79
+ const refusal = WebToolPack.#refuseHost(params.url, allowHosts);
80
+ if (refusal) return { content: [{ type: "text", text: refusal }], details: undefined, isError: true };
81
+ const response = await fetch(params.url, { signal: AbortSignal.timeout(20_000), redirect: "follow" });
82
+ const text = WebToolPack.#readable(await response.text());
83
+ return {
84
+ content: [
85
+ { type: "text", text: `${response.status} ${params.url}\n\n${codeAgentClip(text, codeAgentOutputChars)}` },
86
+ ],
87
+ details: undefined,
88
+ isError: !response.ok,
89
+ };
90
+ },
91
+ });
92
+ }
93
+
94
+ #registerSearch(pi: ExtensionAPI, provider: SearchProvider) {
95
+ pi.registerTool({
96
+ name: "web_search",
97
+ label: "Search",
98
+ description: `Search the web (${provider.name}) and return the top results as title, url and snippet.`,
99
+ promptSnippet: "web_search: search the web and return titles, urls and snippets",
100
+ parameters: Type.Object({
101
+ query: Type.String(),
102
+ count: Type.Optional(Type.Number({ description: "How many results, default 5." })),
103
+ }),
104
+ execute: async (_id, params) => {
105
+ const key = process.env[provider.envKey] ?? "";
106
+ const results = await provider.search(key, params.query, params.count ?? 5);
107
+ return {
108
+ content: [{ type: "text", text: codeAgentClip(results || "(no results)", codeAgentOutputChars) }],
109
+ details: undefined,
110
+ };
111
+ },
112
+ });
113
+ }
114
+
115
+ static #provider() {
116
+ return WebToolPack.providers.find((provider) => !!process.env[provider.envKey]);
117
+ }
118
+
119
+ static #refuseHost(url: string, allowHosts: string[] | undefined) {
120
+ let parsed: URL;
121
+ try {
122
+ parsed = new URL(url);
123
+ } catch {
124
+ return `"${url}" is not an absolute URL.`;
125
+ }
126
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "Only http and https URLs can be fetched.";
127
+ if (allowHosts?.length && !allowHosts.includes(parsed.hostname))
128
+ return `This profile only allows ${allowHosts.join(", ")}.`;
129
+ return undefined;
130
+ }
131
+
132
+ /** Enough to read a documentation page; a full HTML parser is not worth a dependency for that. */
133
+ static #readable(html: string) {
134
+ return html
135
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
136
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
137
+ .replace(/<[^>]+>/g, " ")
138
+ .replace(/&nbsp;/g, " ")
139
+ .replace(/&amp;/g, "&")
140
+ .replace(/&lt;/g, "<")
141
+ .replace(/&gt;/g, ">")
142
+ .replace(/&quot;/g, '"')
143
+ .replace(/[ \t]+/g, " ")
144
+ .replace(/\n{3,}/g, "\n\n")
145
+ .trim();
146
+ }
147
+ }