@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,86 @@
1
+ import { type CodeAgentEvent, codeAgentEventLabel } from "akanjs/common";
2
+ import chalk from "chalk";
3
+
4
+ export interface CodeAgentStreamPrinterOptions {
5
+ /** One JSON object per line instead of prose, for a pipe that parses. */
6
+ json?: boolean;
7
+ thinking?: boolean;
8
+ write?: (text: string) => void;
9
+ }
10
+
11
+ export class CodeAgentStreamPrinter {
12
+ readonly #options: CodeAgentStreamPrinterOptions;
13
+ readonly #write: (text: string) => void;
14
+ #streaming = false;
15
+
16
+ constructor(options: CodeAgentStreamPrinterOptions = {}) {
17
+ this.#options = options;
18
+ this.#write = options.write ?? ((text) => process.stdout.write(text));
19
+ }
20
+
21
+ print(event: CodeAgentEvent) {
22
+ if (this.#options.json) return this.#write(`${JSON.stringify(event)}\n`);
23
+ switch (event.type) {
24
+ case "text_delta":
25
+ this.#streaming = true;
26
+ return this.#write(event.text);
27
+ case "thinking_delta":
28
+ if (!this.#options.thinking) return;
29
+ this.#streaming = true;
30
+ return this.#write(chalk.dim(event.text));
31
+ case "tool_start":
32
+ return this.#line(chalk.cyan(`→ ${event.tool.title}`));
33
+ case "tool_end":
34
+ return this.#line(CodeAgentStreamPrinter.#toolEnd(event));
35
+ case "session":
36
+ return this.#line(chalk.dim(codeAgentEventLabel(event)));
37
+ case "turn_end":
38
+ // Only `truncated` is said: a cut answer looks exactly like a finished one.
39
+ if (event.stopReason === "truncated") return this.#line(chalk.yellow(codeAgentEventLabel(event)));
40
+ return;
41
+ case "message":
42
+ case "tool_progress":
43
+ case "turn_start":
44
+ case "idle":
45
+ case "queue":
46
+ case "context":
47
+ // A stream has no rail to redraw; the pool already reports a child's start and cost as notices.
48
+ case "subagent":
49
+ return;
50
+ case "question":
51
+ return this.#line(chalk.yellow(`? ${event.question.prompt}`));
52
+ case "approval":
53
+ return this.#line(chalk.yellow(`approve? ${event.request.summary}`));
54
+ case "notice":
55
+ return this.#line(CodeAgentStreamPrinter.#notice(event.level, event.message));
56
+ case "error":
57
+ return this.#line(chalk.red(`error: ${event.message}`));
58
+ default:
59
+ return this.#line(chalk.dim(codeAgentEventLabel(event)));
60
+ }
61
+ }
62
+
63
+ /** Closes a half-written streamed line so the exit code and the shell prompt do not land mid-sentence. */
64
+ finish() {
65
+ if (this.#streaming) this.#write("\n");
66
+ this.#streaming = false;
67
+ }
68
+
69
+ #line(text: string) {
70
+ if (this.#streaming) this.#write("\n");
71
+ this.#streaming = false;
72
+ this.#write(`${text}\n`);
73
+ }
74
+
75
+ static #toolEnd(event: Extract<CodeAgentEvent, { type: "tool_end" }>) {
76
+ if (event.outcome === "ok") return chalk.green(`✓ ${event.tool.title}`);
77
+ if (event.outcome === "blocked") return chalk.yellow(`⦸ ${event.tool.title} — ${event.output}`);
78
+ return chalk.red(`✗ ${event.tool.title} — ${event.output.split("\n")[0] ?? ""}`);
79
+ }
80
+
81
+ static #notice(level: "info" | "warning" | "error", message: string) {
82
+ if (level === "error") return chalk.red(message);
83
+ if (level === "warning") return chalk.yellow(message);
84
+ return chalk.dim(message);
85
+ }
86
+ }
@@ -0,0 +1,85 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { CodeAgentApprovalRequest, CodeAgentQuestion } from "akanjs/common";
4
+
5
+ interface SuspendedState {
6
+ question?: CodeAgentQuestion;
7
+ approvals: CodeAgentApprovalRequest[];
8
+ }
9
+
10
+ // Taking an ask removes it, which makes an answer idempotent: a second tab or a retried frame finds nothing.
11
+ export class CodeAgentSuspended {
12
+ readonly #file: string | null;
13
+ #state: SuspendedState;
14
+
15
+ constructor(file: string | null) {
16
+ this.#file = file;
17
+ this.#state = CodeAgentSuspended.#read(file);
18
+ }
19
+
20
+ static fileOf(sessionDir: string | undefined, sessionId: string) {
21
+ return sessionDir ? path.join(sessionDir, `${sessionId}.suspended.json`) : null;
22
+ }
23
+
24
+ get question() {
25
+ return this.#state.question;
26
+ }
27
+
28
+ get approvals(): readonly CodeAgentApprovalRequest[] {
29
+ return this.#state.approvals;
30
+ }
31
+
32
+ //* A new question replaces the old one: asking ends the turn, so the earlier question has nobody left to answer it.
33
+ setQuestion(question: CodeAgentQuestion) {
34
+ this.#state = { ...this.#state, question };
35
+ this.#save();
36
+ }
37
+
38
+ addApproval(request: CodeAgentApprovalRequest) {
39
+ this.#state = { ...this.#state, approvals: [...this.#state.approvals, request] };
40
+ this.#save();
41
+ }
42
+
43
+ takeQuestion(questionId: string) {
44
+ const question = this.#state.question;
45
+ if (question?.questionId !== questionId) return undefined;
46
+ this.#state = { approvals: this.#state.approvals };
47
+ this.#save();
48
+ return question;
49
+ }
50
+
51
+ takeApproval(approvalId: string) {
52
+ const request = this.#state.approvals.find((approval) => approval.approvalId === approvalId);
53
+ if (!request) return undefined;
54
+ this.#state = { ...this.#state, approvals: this.#state.approvals.filter((approval) => approval !== request) };
55
+ this.#save();
56
+ return request;
57
+ }
58
+
59
+ //* A prompt the user typed instead of answering moves the conversation on; the parked asks belong to a turn it replaced.
60
+ clear() {
61
+ if (!this.#state.question && !this.#state.approvals.length) return;
62
+ this.#state = { approvals: [] };
63
+ this.#save();
64
+ }
65
+
66
+ #save() {
67
+ if (!this.#file) return;
68
+ if (!this.#state.question && !this.#state.approvals.length) {
69
+ rmSync(this.#file, { force: true });
70
+ return;
71
+ }
72
+ mkdirSync(path.dirname(this.#file), { recursive: true });
73
+ writeFileSync(this.#file, `${JSON.stringify(this.#state)}\n`);
74
+ }
75
+
76
+ static #read(file: string | null): SuspendedState {
77
+ if (!file || !existsSync(file)) return { approvals: [] };
78
+ try {
79
+ const parsed = JSON.parse(readFileSync(file, "utf8")) as Partial<SuspendedState>;
80
+ return { ...(parsed.question ? { question: parsed.question } : {}), approvals: parsed.approvals ?? [] };
81
+ } catch {
82
+ return { approvals: [] };
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,47 @@
1
+ import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ export interface CodeAgentUiHandlers {
4
+ ask(prompt: string, kind: "text" | "select" | "confirm", choices?: string[]): Promise<string | undefined>;
5
+ notify(level: "info" | "warning" | "error", message: string): void;
6
+ }
7
+
8
+ // Only the dialogs and `notify` mean anything off the engine's TUI; the rest are no-ops, not throws, so an
9
+ // extension that sets a status line does not take the turn down with it.
10
+ export class CodeAgentUi {
11
+ readonly #handlers: CodeAgentUiHandlers;
12
+
13
+ constructor(handlers: CodeAgentUiHandlers) {
14
+ this.#handlers = handlers;
15
+ }
16
+
17
+ context(): ExtensionUIContext {
18
+ const noop = () => {};
19
+ const ui = {
20
+ select: async (title: string, options: string[]) => await this.#handlers.ask(title, "select", options),
21
+ confirm: async (title: string, message: string) => {
22
+ const answer = await this.#handlers.ask(message ? `${title}: ${message}` : title, "confirm");
23
+ return answer === "yes" || answer === "true";
24
+ },
25
+ input: async (title: string) => await this.#handlers.ask(title, "text"),
26
+ notify: (message: string, type: "info" | "warning" | "error" = "info") => this.#handlers.notify(type, message),
27
+ editor: async (title: string, prefill?: string) => (await this.#handlers.ask(title, "text")) ?? prefill,
28
+ onTerminalInput: () => noop,
29
+ setStatus: noop,
30
+ setWorkingMessage: noop,
31
+ setWorkingVisible: noop,
32
+ setWorkingIndicator: noop,
33
+ setHiddenThinkingLabel: noop,
34
+ setWidget: noop,
35
+ setFooter: noop,
36
+ setHeader: noop,
37
+ setTitle: noop,
38
+ custom: async () => undefined,
39
+ pasteToEditor: noop,
40
+ setEditorText: noop,
41
+ getEditorText: () => "",
42
+ addAutocompleteProvider: noop,
43
+ setCustomEditorComponent: noop,
44
+ };
45
+ return ui as unknown as ExtensionUIContext;
46
+ }
47
+ }
@@ -0,0 +1,148 @@
1
+ import {
2
+ appendFileSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ statSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import path from "node:path";
12
+ import { parseJsonLine, readJsonLines } from "./jsonLines";
13
+
14
+ export interface CodePeer {
15
+ id: string;
16
+ name: string;
17
+ cwd: string;
18
+ pid: number;
19
+ /** When the session last said it was alive. */
20
+ at: number;
21
+ }
22
+
23
+ export interface CodeMail {
24
+ at: number;
25
+ from: string;
26
+ fromName: string;
27
+ text: string;
28
+ }
29
+
30
+ // A directory, not a socket or daemon: it survives either end restarting and needs nothing listening.
31
+ // Presence is a heartbeat file, not a registration, so a session that died silently goes stale on its own.
32
+ export class CodeMailbox {
33
+ static readonly staleMs = 20_000;
34
+ static readonly beatMs = 5_000;
35
+ static readonly pollMs = 1_500;
36
+ /** How many messages one session may send in a minute, so two agents answering each other cannot run away. */
37
+ static readonly sendsPerMinute = 20;
38
+
39
+ readonly #dir: string;
40
+ readonly #id: string;
41
+ #name: string;
42
+ #read = 0;
43
+ #sent: number[] = [];
44
+ #beat: ReturnType<typeof setInterval> | null = null;
45
+ #poll: ReturnType<typeof setInterval> | null = null;
46
+
47
+ constructor(dir: string, id: string, name: string) {
48
+ this.#dir = dir;
49
+ this.#id = id;
50
+ this.#name = name;
51
+ }
52
+
53
+ get inbox() {
54
+ return path.join(this.#dir, "inbox", `${this.#id}.jsonl`);
55
+ }
56
+
57
+ /** Announces this session and starts delivering what arrives for it. */
58
+ open(cwd: string, onMail: (mail: CodeMail) => void) {
59
+ mkdirSync(path.join(this.#dir, "live"), { recursive: true });
60
+ mkdirSync(path.join(this.#dir, "inbox"), { recursive: true });
61
+ // What is already in the inbox belongs to an earlier run; delivering it would replay a past conversation.
62
+ this.#read = readJsonLines(this.inbox).length;
63
+ this.beat(cwd);
64
+ this.#beat = setInterval(() => this.beat(cwd), CodeMailbox.beatMs);
65
+ this.#poll = setInterval(() => {
66
+ for (const mail of this.#drain()) onMail(mail);
67
+ }, CodeMailbox.pollMs);
68
+ }
69
+
70
+ close() {
71
+ if (this.#beat) clearInterval(this.#beat);
72
+ if (this.#poll) clearInterval(this.#poll);
73
+ this.#beat = null;
74
+ this.#poll = null;
75
+ try {
76
+ rmSync(path.join(this.#dir, "live", `${this.#id}.json`), { force: true });
77
+ } catch {
78
+ // A leftover presence file goes stale on its own; failing to remove it is not worth an error.
79
+ }
80
+ }
81
+
82
+ rename(name: string, cwd: string) {
83
+ this.#name = name;
84
+ this.beat(cwd);
85
+ }
86
+
87
+ beat(cwd: string) {
88
+ const peer: CodePeer = { id: this.#id, name: this.#name, cwd, pid: process.pid, at: Date.now() };
89
+ try {
90
+ writeFileSync(path.join(this.#dir, "live", `${this.#id}.json`), JSON.stringify(peer));
91
+ } catch {
92
+ // A read-only or missing directory means no presence; messaging then simply finds nobody.
93
+ }
94
+ }
95
+
96
+ /** The live sessions of this workspace, this one excluded — it is not a peer of itself. */
97
+ peers(): CodePeer[] {
98
+ const dir = path.join(this.#dir, "live");
99
+ if (!existsSync(dir)) return [];
100
+ const now = Date.now();
101
+ return readdirSync(dir)
102
+ .filter((entry) => entry.endsWith(".json"))
103
+ .map((entry) => CodeMailbox.#peer(path.join(dir, entry)))
104
+ .filter((peer): peer is CodePeer => !!peer && peer.id !== this.#id && now - peer.at < CodeMailbox.staleMs)
105
+ .sort((left, right) => right.at - left.at);
106
+ }
107
+
108
+ /** Resolves what a person typed — a name, or the front of an id — to exactly one peer. */
109
+ find(target: string) {
110
+ const needle = target.trim().toLowerCase();
111
+ const peers = this.peers();
112
+ const byName = peers.filter((peer) => peer.name.toLowerCase() === needle);
113
+ if (byName.length === 1) return byName[0];
114
+ const byPrefix = peers.filter((peer) => peer.id.startsWith(needle) || peer.name.toLowerCase().startsWith(needle));
115
+ return byPrefix.length === 1 ? byPrefix[0] : undefined;
116
+ }
117
+
118
+ send(to: string, text: string) {
119
+ const now = Date.now();
120
+ this.#sent = this.#sent.filter((at) => now - at < 60_000);
121
+ if (this.#sent.length >= CodeMailbox.sendsPerMinute)
122
+ throw new Error(`Too many messages: ${CodeMailbox.sendsPerMinute} a minute is the ceiling.`);
123
+ this.#sent.push(now);
124
+ const mail: CodeMail = { at: now, from: this.#id, fromName: this.#name, text };
125
+ mkdirSync(path.join(this.#dir, "inbox"), { recursive: true });
126
+ // Append, never rewrite: two senders may be writing at the same moment, and one line is one write.
127
+ appendFileSync(path.join(this.#dir, "inbox", `${to}.jsonl`), `${JSON.stringify(mail)}\n`);
128
+ }
129
+
130
+ #drain(): CodeMail[] {
131
+ const lines = readJsonLines(this.inbox);
132
+ if (lines.length <= this.#read) return [];
133
+ const fresh = lines.slice(this.#read);
134
+ this.#read = lines.length;
135
+ return fresh
136
+ .map((line) => parseJsonLine<CodeMail>(line))
137
+ .filter((mail): mail is CodeMail => !!mail?.text && typeof mail.from === "string");
138
+ }
139
+
140
+ static #peer(file: string) {
141
+ try {
142
+ const peer = JSON.parse(readFileSync(file, "utf8")) as CodePeer;
143
+ return typeof peer.id === "string" ? { ...peer, at: peer.at || statSync(file).mtimeMs } : undefined;
144
+ } catch {
145
+ return undefined;
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,56 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { codeAgentSessionName } from "akanjs/common";
5
+ import { CodeSessionIndex } from "./CodeSessionIndex";
6
+ import { parseJsonLine } from "./jsonLines";
7
+
8
+ interface CodeSessionRecord {
9
+ [key: string]: unknown;
10
+ type?: string;
11
+ id?: string;
12
+ }
13
+
14
+ // Only the `session` record's id is rewritten: a shared id would give two agents one inbox and one resume target.
15
+ export class CodeSessionFork {
16
+ static fork(dir: string, id: string, name?: string) {
17
+ const source = CodeSessionIndex.fileOf(dir, id);
18
+ if (!source) throw new Error(`No stored session ${id} in ${dir}`);
19
+ const lines = readFileSync(source, "utf8")
20
+ .split("\n")
21
+ .filter((line) => !!line.trim());
22
+ if (!lines.length) throw new Error(`Session ${id} has nothing in it to fork`);
23
+ const at = new Date();
24
+ const forked = Bun.randomUUIDv7();
25
+ const records = lines
26
+ .map((line) => parseJsonLine<CodeSessionRecord>(line))
27
+ .filter((record): record is CodeSessionRecord => !!record);
28
+ const first = records.find((record) => record.type === "session");
29
+ if (!first) throw new Error(`Session ${id} carries no session record`);
30
+ first.id = forked;
31
+ first.timestamp = at.toISOString();
32
+ records.push(CodeSessionFork.#named(records, name ?? CodeSessionFork.#nameOf(records), at));
33
+ // The engine's own file-name shape, which `CodeSessionIndex.fileOf` matches a resume against.
34
+ const file = path.join(dir, `${at.toISOString().replace(/[:.]/g, "-")}_${forked}.jsonl`);
35
+ writeFileSync(file, `${records.map((record) => JSON.stringify(record)).join("\n")}\n`);
36
+ return { id: forked, file };
37
+ }
38
+
39
+ /** A rename is an appended record, so the fork is named by adding one rather than by editing the original. */
40
+ static #named(records: CodeSessionRecord[], name: string, at: Date) {
41
+ const parent = records.at(-1)?.id;
42
+ return {
43
+ type: "session_info",
44
+ id: randomBytes(4).toString("hex"),
45
+ parentId: typeof parent === "string" ? parent : null,
46
+ timestamp: at.toISOString(),
47
+ name,
48
+ };
49
+ }
50
+
51
+ static #nameOf(records: CodeSessionRecord[]) {
52
+ const named = records.filter((record) => record.type === "session_info").at(-1);
53
+ const current = typeof named?.name === "string" ? named.name : "";
54
+ return codeAgentSessionName(current ? `${current} fork` : "fork");
55
+ }
56
+ }
@@ -0,0 +1,78 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { parseJsonLine, readJsonLines } from "./jsonLines";
4
+
5
+ export interface CodeSessionEntry {
6
+ id: string;
7
+ file: string;
8
+ /** What the session calls itself, when it was named. */
9
+ name: string | undefined;
10
+ /** The first thing the person asked, which is what a list of sessions is actually scanned for. */
11
+ opening: string;
12
+ updatedAt: number;
13
+ turns: number;
14
+ }
15
+
16
+ // Read as files: the engine's session manager has nothing that enumerates the directory.
17
+ export class CodeSessionIndex {
18
+ static readonly readLimit = 512 * 1024;
19
+
20
+ static list(dir: string, limit = 30): CodeSessionEntry[] {
21
+ if (!existsSync(dir)) return [];
22
+ const files = readdirSync(dir)
23
+ .filter((entry) => entry.endsWith(".jsonl"))
24
+ .map((entry) => path.join(dir, entry))
25
+ .map((file) => ({ file, updatedAt: CodeSessionIndex.#mtime(file) }))
26
+ .sort((left, right) => right.updatedAt - left.updatedAt)
27
+ .slice(0, limit);
28
+ return files
29
+ .map((entry) => CodeSessionIndex.#read(entry.file, entry.updatedAt))
30
+ .filter((entry): entry is CodeSessionEntry => !!entry);
31
+ }
32
+
33
+ static fileOf(dir: string, id: string) {
34
+ if (!existsSync(dir)) return undefined;
35
+ // The file name carries the id after its timestamp, so the directory is searched without reading anything.
36
+ const match = readdirSync(dir).find((entry) => entry.endsWith(`${id}.jsonl`) || entry === `${id}.jsonl`);
37
+ return match ? path.join(dir, match) : undefined;
38
+ }
39
+
40
+ static #mtime(file: string) {
41
+ try {
42
+ return statSync(file).mtimeMs;
43
+ } catch {
44
+ return 0;
45
+ }
46
+ }
47
+
48
+ static #read(file: string, updatedAt: number): CodeSessionEntry | undefined {
49
+ const lines = readJsonLines(file, CodeSessionIndex.readLimit);
50
+ if (!lines.length) return undefined;
51
+ const entry: CodeSessionEntry = { id: "", file, name: undefined, opening: "", updatedAt, turns: 0 };
52
+ for (const line of lines) {
53
+ const record = parseJsonLine<{ [key: string]: unknown; type?: string }>(line);
54
+ if (!record) continue;
55
+ if (record.type === "session" && typeof record.id === "string") entry.id = record.id;
56
+ // A rename appends a second entry rather than editing the first, so the last one on the file wins.
57
+ if (record.type === "session_info" && typeof record.name === "string") entry.name = record.name;
58
+ if (record.type !== "message") continue;
59
+ const message = record.message as { role?: string; content?: unknown } | undefined;
60
+ if (message?.role !== "user") continue;
61
+ entry.turns += 1;
62
+ if (!entry.opening) entry.opening = CodeSessionIndex.#text(message.content);
63
+ }
64
+ return entry.id ? entry : undefined;
65
+ }
66
+
67
+ static #text(content: unknown) {
68
+ const text =
69
+ typeof content === "string"
70
+ ? content
71
+ : Array.isArray(content)
72
+ ? content
73
+ .map((part) => (typeof part === "object" && part && "text" in part ? String(part.text) : ""))
74
+ .join(" ")
75
+ : "";
76
+ return text.replace(/\s+/g, " ").trim();
77
+ }
78
+ }