@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.20

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 (265) hide show
  1. package/README.md +1 -0
  2. package/abstractDoc.test.ts +6 -15
  3. package/abstractDoc.ts +6 -8
  4. package/agentsIndex.test.ts +4 -7
  5. package/agentsIndex.ts +6 -22
  6. package/akanApp/BackendImportGraph.test.ts +8 -22
  7. package/akanApp/BackendImportGraph.ts +1 -20
  8. package/akanApp/akanApp.host.test.ts +1 -3
  9. package/akanApp/akanApp.host.ts +82 -254
  10. package/akanApp/devHostPolicy.test.ts +10 -34
  11. package/akanApp/devHostPolicy.ts +12 -98
  12. package/akanConfig/akanConfig.test.ts +85 -69
  13. package/akanConfig/akanConfig.ts +69 -153
  14. package/akanConfig/types.ts +1 -0
  15. package/akanContext.ts +106 -159
  16. package/akanMcpContract.ts +28 -73
  17. package/appSelectionMemory.ts +1 -4
  18. package/applicationBuildReporter.ts +37 -5
  19. package/applicationBuildRunner.test.ts +5 -3
  20. package/applicationBuildRunner.ts +54 -61
  21. package/applicationReleasePackager.ts +24 -44
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +9 -30
  24. package/artifact/implicitRootLayout.ts +34 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +6 -1
  28. package/biomeBase.ts +2 -5
  29. package/capacitorApp.test.ts +32 -16
  30. package/capacitorApp.ts +110 -238
  31. package/cloud/cloudApi.test.ts +202 -0
  32. package/cloud/cloudApi.ts +77 -39
  33. package/cloud/configLock.test.ts +64 -0
  34. package/cloud/configLock.ts +96 -0
  35. package/cloud/constants.ts +26 -5
  36. package/cloud/globalConfig.ts +70 -32
  37. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  38. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  39. package/codeAgent/agent/CodeAgent.ts +551 -0
  40. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  41. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  42. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  43. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  44. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  45. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  46. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  47. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  48. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  49. package/codeAgent/agent/CodeMailbox.ts +148 -0
  50. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  51. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  52. package/codeAgent/agent/SubagentPool.ts +214 -0
  53. package/codeAgent/agent/akanCodeModel.ts +47 -0
  54. package/codeAgent/agent/akanCodePaths.ts +31 -0
  55. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  56. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  57. package/codeAgent/agent/codeAgent.test.ts +178 -0
  58. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  59. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  60. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  61. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  62. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  63. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  64. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  65. package/codeAgent/agent/consoleToStderr.ts +8 -0
  66. package/codeAgent/agent/jsonLines.ts +29 -0
  67. package/codeAgent/agent/stringArg.ts +5 -0
  68. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  69. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  70. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  71. package/codeAgent/feedback/PreviewView.ts +95 -0
  72. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  73. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  74. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  75. package/codeAgent/index.ts +34 -0
  76. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  77. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  78. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  79. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  80. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  81. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  82. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  83. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  84. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  85. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  86. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  87. package/codeAgent/tools/AkanCli.ts +50 -0
  88. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  89. package/codeAgent/tools/AkanEditScope.ts +48 -0
  90. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  91. package/codeAgent/tools/AkanToolPack.ts +92 -0
  92. package/codeAgent/tools/AkanVerifier.ts +48 -0
  93. package/codeAgent/tools/AskToolPack.ts +99 -0
  94. package/codeAgent/tools/MailToolPack.ts +76 -0
  95. package/codeAgent/tools/McpClient.ts +187 -0
  96. package/codeAgent/tools/McpOAuth.ts +233 -0
  97. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  98. package/codeAgent/tools/McpServerConfig.ts +129 -0
  99. package/codeAgent/tools/McpSignIn.ts +158 -0
  100. package/codeAgent/tools/McpTokenStore.ts +81 -0
  101. package/codeAgent/tools/McpToolPack.ts +199 -0
  102. package/codeAgent/tools/SessionToolPack.ts +91 -0
  103. package/codeAgent/tools/WebToolPack.ts +147 -0
  104. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  105. package/codeAgent/tools/askToolPack.test.ts +116 -0
  106. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  107. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  108. package/codegenLock.test.ts +4 -13
  109. package/codegenLock.ts +3 -17
  110. package/commandDecorators/argMeta.ts +1 -5
  111. package/commandDecorators/camelToKebabCase.ts +1 -0
  112. package/commandDecorators/command.ts +46 -102
  113. package/commandDecorators/commandBuilder.ts +30 -61
  114. package/commandDecorators/commandDecorators.test.ts +2 -10
  115. package/commandDecorators/dependencyBuilder.ts +10 -1
  116. package/commandDecorators/helpFormatter.ts +68 -145
  117. package/commandDecorators/index.ts +0 -2
  118. package/commandDecorators/targetMeta.ts +9 -7
  119. package/createTunnel.ts +7 -14
  120. package/dependencyScanner.test.ts +4 -16
  121. package/dependencyScanner.ts +31 -80
  122. package/devkitUtils.test.ts +48 -20
  123. package/executors.linkFallback.test.ts +103 -0
  124. package/executors.test.ts +132 -106
  125. package/executors.ts +194 -484
  126. package/fileEditor.ts +9 -37
  127. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  128. package/fileSys.ts +6 -8
  129. package/formSetterScanner.ts +3 -16
  130. package/frontendBuild/allRoutesBuilder.ts +7 -21
  131. package/frontendBuild/autoImportSync.test.ts +27 -38
  132. package/frontendBuild/autoImportSync.ts +31 -65
  133. package/frontendBuild/buildRouteClient.test.ts +62 -37
  134. package/frontendBuild/bundleDefine.ts +12 -0
  135. package/frontendBuild/clientBuildTypes.ts +5 -36
  136. package/frontendBuild/clientEntriesBundler.ts +9 -40
  137. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  138. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  139. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  140. package/frontendBuild/cssCandidateCache.ts +6 -41
  141. package/frontendBuild/cssCompiler.ts +29 -78
  142. package/frontendBuild/cssImportResolver.ts +6 -19
  143. package/frontendBuild/devChangePlanner.ts +7 -10
  144. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  145. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  146. package/frontendBuild/fontOptimizer.test.ts +6 -14
  147. package/frontendBuild/fontOptimizer.ts +15 -72
  148. package/frontendBuild/fontPruner.test.ts +5 -10
  149. package/frontendBuild/fontPruner.ts +5 -20
  150. package/frontendBuild/frontendBuild.test.ts +46 -69
  151. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  152. package/frontendBuild/hmrWatcher.test.ts +8 -32
  153. package/frontendBuild/hmrWatcher.ts +14 -71
  154. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  155. package/frontendBuild/precompressArtifacts.ts +5 -18
  156. package/frontendBuild/routeClientBuilder.ts +23 -46
  157. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  158. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  159. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  160. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  161. package/frontendBuild/styleContract.ts +1 -8
  162. package/frontendBuild/themeValidator.ts +4 -26
  163. package/frontendBuild/watchRootResolver.test.ts +23 -42
  164. package/frontendBuild/watchRootResolver.ts +5 -27
  165. package/getRelatedCnsts.ts +12 -92
  166. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  167. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  168. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  169. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  170. package/incrementalBuilder/builderChannel.test.ts +24 -49
  171. package/incrementalBuilder/builderChannel.ts +6 -37
  172. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  173. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  174. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  175. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  176. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  177. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  178. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  179. package/index.ts +5 -17
  180. package/integration/devResourceProbe.ts +50 -56
  181. package/integration/devStability.integration.test.ts +47 -194
  182. package/integration/devStabilityHarness.test.ts +13 -28
  183. package/integration/devStabilityHarness.ts +65 -330
  184. package/integration/ssrMemoryProbe.ts +41 -167
  185. package/libSource.test.ts +4 -15
  186. package/libSource.ts +19 -31
  187. package/lint/gritRules.test.ts +8 -24
  188. package/lint/no-deep-internal-import.grit +2 -2
  189. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  190. package/linter.test.ts +1 -2
  191. package/linter.ts +26 -116
  192. package/mobile/mobileTarget.ts +14 -13
  193. package/package.json +10 -5
  194. package/packageExports.test.ts +3 -18
  195. package/packageExportsMap.ts +3 -19
  196. package/piDependencyPins.test.ts +46 -0
  197. package/platformTest/BunTestReport.test.ts +60 -0
  198. package/platformTest/BunTestReport.ts +65 -0
  199. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  200. package/platformTest/IdleWatchedProcess.ts +73 -0
  201. package/platformTest/LinuxDockerTarget.ts +105 -0
  202. package/platformTest/PlatformTestRun.test.ts +64 -0
  203. package/platformTest/PlatformTestRun.ts +141 -0
  204. package/platformTest/PlatformTestTarget.ts +142 -0
  205. package/platformTest/TestSnapshot.test.ts +72 -0
  206. package/platformTest/TestSnapshot.ts +103 -0
  207. package/platformTest/WindowsSshTarget.ts +151 -0
  208. package/prompter.ts +3 -13
  209. package/qualityScanner.test.ts +9 -26
  210. package/qualityScanner.ts +197 -285
  211. package/recipeScanner.test.ts +7 -12
  212. package/recipeScanner.ts +9 -38
  213. package/repoIdentity.ts +2 -9
  214. package/routeSourceValidator.test.ts +0 -1
  215. package/routeSourceValidator.ts +7 -42
  216. package/scanInfo.ts +87 -216
  217. package/selectModel.ts +1 -2
  218. package/semver.ts +3 -5
  219. package/slicePlanner.test.ts +6 -24
  220. package/slicePlanner.ts +11 -32
  221. package/spinner.test.ts +3 -8
  222. package/spinner.ts +4 -16
  223. package/sshTunnel.ts +1 -6
  224. package/ssrScanner.test.ts +1 -6
  225. package/ssrScanner.ts +103 -113
  226. package/stripAnsi.ts +3 -0
  227. package/subspace.test.ts +30 -25
  228. package/subspace.ts +81 -115
  229. package/subspaceConfig.ts +11 -9
  230. package/testHelpers.ts +194 -0
  231. package/transforms/asyncDefaultExportDetector.ts +3 -11
  232. package/transforms/barrelAnalyzer.ts +43 -135
  233. package/transforms/barrelImportsPlugin.ts +38 -175
  234. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  235. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  236. package/transforms/moduleSyntax.ts +32 -0
  237. package/transforms/rscUseClientTransform.ts +35 -20
  238. package/transforms/transforms.test.ts +153 -44
  239. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  240. package/transforms/useClientBundlePlugin.ts +2 -22
  241. package/typeChecker.ts +7 -63
  242. package/types.ts +1 -0
  243. package/uploadRelease.ts +10 -25
  244. package/workflow/artifacts.ts +18 -0
  245. package/workflow/executor.test.ts +1 -12
  246. package/workflow/executor.ts +101 -110
  247. package/workflow/index.ts +0 -1
  248. package/workflow/moduleIndex.test.ts +4 -15
  249. package/workflow/moduleIndex.ts +9 -49
  250. package/workflow/plan.ts +18 -25
  251. package/workflow/render.ts +80 -96
  252. package/workflow/rolloutGate.test.ts +1 -1
  253. package/workflow/source.ts +52 -69
  254. package/workflow/sourceAst.ts +50 -0
  255. package/workspaceLayout.ts +4 -19
  256. package/abstractCompactor.ts +0 -112
  257. package/aiEditor.test.ts +0 -60
  258. package/aiEditor.ts +0 -387
  259. package/commandDecorators/commandMeta.ts +0 -7
  260. package/commandDecorators/types.ts +0 -10
  261. package/getDirname.ts +0 -4
  262. package/guideline.ts +0 -19
  263. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  264. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  265. package/workflow/primitive.ts +0 -59
@@ -1,6 +1,7 @@
1
- import { chmod, mkdir } from "node:fs/promises";
1
+ import { mkdir, rename, rm, writeFile } from "node:fs/promises";
2
2
  import dayjs from "dayjs";
3
3
  import { FileSys } from "../fileSys";
4
+ import { ConfigLock } from "./configLock";
4
5
  import {
5
6
  type AccessToken,
6
7
  type AccessTokenDto,
@@ -12,6 +13,7 @@ import {
12
13
  type HostConfig,
13
14
  type HostConfigDto,
14
15
  type RemoteEnvServerConfig,
16
+ type TestTargetsConfig,
15
17
  } from "./constants";
16
18
 
17
19
  export class GlobalConfig {
@@ -27,58 +29,94 @@ export class GlobalConfig {
27
29
  ...akanConfig,
28
30
  cloudHost: akanConfig.cloudHost ?? defaultAkanGlobalConfig.cloudHost,
29
31
  remoteEnvServers: akanConfig.remoteEnvServers ?? defaultAkanGlobalConfig.remoteEnvServers,
32
+ testTargets: akanConfig.testTargets ?? defaultAkanGlobalConfig.testTargets,
30
33
  };
31
34
  }
32
- /**
33
- * This file holds the cloud jwt, a refresh token that does not expire, and the LLM api key, so it is
34
- * written owner-only — the same `0600` the runtime gives its control socket. `Bun.write` takes no mode
35
- * and lands on `0666 & ~umask` (0644 on a default shell), so the mode is applied after the write; an
36
- * existing world-readable file is tightened by the next write rather than left as it was found.
37
- */
35
+ // Holds the cloud jwt and refresh token, so owner-only 0600: written to a 0600 sibling and renamed over the file,
36
+ // which also replaces a world-readable one and never shows another CLI process a half-written file.
38
37
  static async #setAkanGlobalConfig(akanConfig: AkanGlobalConfig) {
39
38
  await mkdir(basePath, { recursive: true, mode: 0o700 });
40
- await Bun.write(configPath, JSON.stringify(akanConfig, null, 2));
41
- await chmod(configPath, 0o600);
39
+ const temp = `${configPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
40
+ try {
41
+ await writeFile(temp, JSON.stringify(akanConfig, null, 2), { mode: 0o600 });
42
+ await rename(temp, configPath);
43
+ } catch (error) {
44
+ await rm(temp, { force: true });
45
+ throw error;
46
+ }
47
+ }
48
+ static readonly #lockPath = `${configPath}.lock`;
49
+ // Every write re-reads under the lock: one built on an earlier read could restore a rotated-away refresh token.
50
+ static async #update(mutate: (akanConfig: AkanGlobalConfig) => AkanGlobalConfig) {
51
+ await ConfigLock.run(GlobalConfig.#lockPath, async () => {
52
+ await GlobalConfig.#setAkanGlobalConfig(mutate(await GlobalConfig.#getAkanGlobalConfig()));
53
+ });
42
54
  }
43
55
  static async getHostConfig(host = GlobalConfig.akanCloudHost): Promise<HostConfig> {
44
56
  const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
45
57
  return GlobalConfig.toHostConfig(akanConfig.cloudHost[host] ?? getDefaultHostConfig(host));
46
58
  }
47
59
  static async setHostConfig(config: HostConfig = getDefaultHostConfig()) {
48
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
49
- akanConfig.cloudHost[config.host] = GlobalConfig.toHostConfigDto(config);
50
- await GlobalConfig.#setAkanGlobalConfig(akanConfig);
60
+ await GlobalConfig.#update((akanConfig) => GlobalConfig.#withHost(akanConfig, config));
51
61
  }
52
- static async getLlmConfig(): Promise<AkanGlobalConfig["llm"]> {
53
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
54
- return akanConfig.llm ?? null;
62
+ static #withHost(akanConfig: AkanGlobalConfig, config: HostConfig): AkanGlobalConfig {
63
+ return {
64
+ ...akanConfig,
65
+ cloudHost: { ...akanConfig.cloudHost, [config.host]: GlobalConfig.toHostConfigDto(config) },
66
+ };
55
67
  }
56
- static async setLlmConfig(llmConfig: AkanGlobalConfig["llm"]) {
57
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
58
- await GlobalConfig.#setAkanGlobalConfig({ ...akanConfig, llm: llmConfig });
68
+ // The cloud rotates the refresh token and revokes every session of the account when one is presented twice, so it
69
+ // leaves the disk before it is sent: a refresh killed in flight leaves nothing to replay, only a sign-in to redo.
70
+ static async refreshHostAuth(host: string, refresh: (refreshToken: string) => Promise<AccessToken>) {
71
+ try {
72
+ return await ConfigLock.run(GlobalConfig.#lockPath, async () => {
73
+ const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
74
+ const current = GlobalConfig.toHostConfig(akanConfig.cloudHost[host] ?? getDefaultHostConfig(host));
75
+ const accessToken = current.auth?.accessToken;
76
+ const refreshToken = accessToken?.refreshToken;
77
+ if (!accessToken || !refreshToken || !GlobalConfig.needRefreshToken(accessToken)) return current;
78
+ const claimed: HostConfig = {
79
+ ...current,
80
+ auth: { ...current.auth, accessToken: { ...accessToken, refreshToken: null } },
81
+ };
82
+ await GlobalConfig.#setAkanGlobalConfig(GlobalConfig.#withHost(akanConfig, claimed));
83
+ const next = await refresh(refreshToken).catch(() => null);
84
+ if (!next?.jwt) return claimed;
85
+ const refreshed: HostConfig = { ...current, auth: { ...current.auth, accessToken: next } };
86
+ await GlobalConfig.#setAkanGlobalConfig(
87
+ GlobalConfig.#withHost(await GlobalConfig.#getAkanGlobalConfig(), refreshed),
88
+ );
89
+ return refreshed;
90
+ });
91
+ } catch {
92
+ // No lock, or a write that failed: what is stored is used as it is, and nothing is refreshed outside the lock.
93
+ return await GlobalConfig.getHostConfig(host);
94
+ }
59
95
  }
60
96
  static async getRemoteEnvServers(): Promise<AkanGlobalConfig["remoteEnvServers"]> {
61
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
62
- return akanConfig.remoteEnvServers;
97
+ return (await GlobalConfig.#getAkanGlobalConfig()).remoteEnvServers;
63
98
  }
64
99
  static async setRemoteEnvServer(name: string, config: RemoteEnvServerConfig) {
65
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
66
- await GlobalConfig.#setAkanGlobalConfig({
100
+ await GlobalConfig.#update((akanConfig) => ({
67
101
  ...akanConfig,
68
- remoteEnvServers: {
69
- ...akanConfig.remoteEnvServers,
70
- [name]: config,
71
- },
72
- });
102
+ remoteEnvServers: { ...akanConfig.remoteEnvServers, [name]: config },
103
+ }));
73
104
  }
74
105
  static async removeRemoteEnvServer(name: string) {
75
- const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
76
- const { [name]: _, ...remoteEnvServers } = akanConfig.remoteEnvServers;
77
- await GlobalConfig.#setAkanGlobalConfig({
78
- ...akanConfig,
79
- remoteEnvServers,
106
+ await GlobalConfig.#update((akanConfig) => {
107
+ const { [name]: _, ...remoteEnvServers } = akanConfig.remoteEnvServers;
108
+ return { ...akanConfig, remoteEnvServers };
80
109
  });
81
110
  }
111
+ static async getTestTargets(): Promise<TestTargetsConfig> {
112
+ return (await GlobalConfig.#getAkanGlobalConfig()).testTargets;
113
+ }
114
+ static async setTestTargets(testTargets: TestTargetsConfig) {
115
+ await GlobalConfig.#update((akanConfig) => ({
116
+ ...akanConfig,
117
+ testTargets: { ...akanConfig.testTargets, ...testTargets },
118
+ }));
119
+ }
82
120
  static needRefreshToken(accessToken: AccessToken): boolean {
83
121
  return !!accessToken?.expiresAt?.isBefore(dayjs().add(1, "hour"));
84
122
  }
@@ -0,0 +1,97 @@
1
+ import { existsSync, mkdirSync, mkdtempSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { type InlineExtension, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
5
+ import type { CodeAgentProfile } from "akanjs/common";
6
+ import { AkanEnvKeys } from "../tools/AkanEnvKeys";
7
+ import { type AkanContextFile, AkanContextFiles } from "./AkanContextFiles";
8
+ import { akanCodePaths } from "./akanCodePaths";
9
+ import { akanSystemPrompt } from "./akanSystemPrompt";
10
+ import type { CodeAgentProxy } from "./CodeAgentProxy";
11
+ import { CodeSessionIndex } from "./CodeSessionIndex";
12
+
13
+ export interface AkanCodeServicesOptions {
14
+ workspaceRoot: string;
15
+ cwd: string;
16
+ profile: CodeAgentProfile;
17
+ extensions: InlineExtension[];
18
+ sessionId?: string;
19
+ }
20
+
21
+ // All four engine services are injected explicitly: any one left out makes the engine fall back to `~/.pi/`.
22
+ export class AkanCodeServices {
23
+ static settings() {
24
+ // In memory on purpose: file settings would be a second configuration surface beside the profile.
25
+ // `enableInstallTelemetry` defaults to true and adds pi-branded headers to OpenRouter/NVIDIA/Cloudflare calls.
26
+ return SettingsManager.inMemory({
27
+ enableInstallTelemetry: false,
28
+ enableAnalytics: false,
29
+ quietStartup: true,
30
+ // Twice the engine's 16,384: at 16k a reasoning model stops mid-thought at the compaction threshold,
31
+ // and the recovery re-sends a near-full window to retry the turn.
32
+ compaction: { reserveTokens: 32_768 },
33
+ });
34
+ }
35
+
36
+ // The engine locks and rewrites `authPath` itself, so two `akan code` refreshing a token at once cannot clobber it.
37
+ static async runtime(workspaceRoot: string, proxy: CodeAgentProxy | null = null) {
38
+ mkdirSync(akanCodePaths.globalDir(), { recursive: true, mode: 0o700 });
39
+ const runtime = await ModelRuntime.create({
40
+ //* Behind a proxy no real key may reach a request, so credentials live in a throwaway file.
41
+ authPath: proxy
42
+ ? path.join(mkdtempSync(path.join(tmpdir(), "akan-code-proxy-")), "auth.json")
43
+ : akanCodePaths.authFile(),
44
+ modelsPath: akanCodePaths.modelsFile(),
45
+ });
46
+ if (proxy) await proxy.apply(runtime);
47
+ else await AkanEnvKeys.apply(runtime, workspaceRoot);
48
+ return runtime;
49
+ }
50
+
51
+ static sessions(workspaceRoot: string, cwd: string, profile: CodeAgentProfile, resume?: string) {
52
+ //* `remote` has no store of its own yet, so it keeps a file session: a pod worker's suspended asks need it.
53
+ if (profile.session.store === "memory") return SessionManager.inMemory(cwd);
54
+ const dir = akanCodePaths.sessionsDir(workspaceRoot);
55
+ mkdirSync(dir, { recursive: true });
56
+ const manager = SessionManager.create(cwd, dir);
57
+ if (!resume) return manager;
58
+ const file = CodeSessionIndex.fileOf(dir, resume);
59
+ // A mistyped id that quietly opened a new session would look like a session that lost its history.
60
+ if (!file) throw new Error(`No stored session ${resume} in ${dir}`);
61
+ manager.setSessionFile(file);
62
+ return manager;
63
+ }
64
+
65
+ // Skills are text, never code that runs, so unlike extensions they may come from the home directory too.
66
+ static #skillPaths(profile: CodeAgentProfile, workspaceRoot: string) {
67
+ if (!profile.context.skills) return [];
68
+ const dirs = [
69
+ akanCodePaths.builtinSkillsDir(),
70
+ akanCodePaths.globalSkillsDir(),
71
+ akanCodePaths.skillsDir(workspaceRoot),
72
+ ];
73
+ return dirs.filter((dir): dir is string => !!dir && existsSync(dir));
74
+ }
75
+
76
+ static resourceOptions(options: AkanCodeServicesOptions) {
77
+ // A path that does not exist is reported as a load error rather than skipped, and these two are optional.
78
+ const present = (dir: string) => (existsSync(dir) ? [dir] : []);
79
+ return {
80
+ noExtensions: true,
81
+ // Disables only the engine's default skill locations (`~/.pi/skills`, `.pi/`); `additionalSkillPaths` merge.
82
+ noSkills: true,
83
+ noPromptTemplates: true,
84
+ noThemes: true,
85
+ noContextFiles: !options.profile.context.projectFiles,
86
+ agentsFilesOverride: ({ agentsFiles }: { agentsFiles: AkanContextFile[] }) => ({
87
+ agentsFiles: options.profile.context.projectFiles
88
+ ? AkanContextFiles.extend(agentsFiles, { workspaceRoot: options.workspaceRoot, cwd: options.cwd })
89
+ : agentsFiles,
90
+ }),
91
+ appendSystemPrompt: [akanSystemPrompt(options.profile)],
92
+ additionalExtensionPaths: present(akanCodePaths.extensionsDir(options.workspaceRoot)),
93
+ additionalSkillPaths: AkanCodeServices.#skillPaths(options.profile, options.workspaceRoot),
94
+ extensionFactories: options.extensions,
95
+ };
96
+ }
97
+ }
@@ -0,0 +1,94 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export interface AkanContextFile {
5
+ path: string;
6
+ content: string;
7
+ }
8
+
9
+ export interface AkanContextFilesOptions {
10
+ workspaceRoot: string;
11
+ cwd: string;
12
+ }
13
+
14
+ // The engine takes only the first of `AGENTS.override.md · AGENTS.md · CLAUDE.md` per directory and never reads
15
+ // `.cursor/rules`; this appends what it drops, after the engine's own list.
16
+ export class AkanContextFiles {
17
+ /** Cursor's frontmatter block, which is metadata for its own rule picker and not instruction for a model. */
18
+ static readonly #frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
19
+
20
+ static extend(base: AkanContextFile[], options: AkanContextFilesOptions): AkanContextFile[] {
21
+ const seen = new Set(base.map((file) => file.path));
22
+ // Keyed by content too: the usual way to carry both names is a symlink or a one-line pointer.
23
+ const contents = new Set(base.map((file) => file.content.trim()));
24
+ const added: AkanContextFile[] = [];
25
+ for (const file of [...AkanContextFiles.#claude(base), ...AkanContextFiles.#cursor(options)]) {
26
+ if (seen.has(file.path) || contents.has(file.content.trim()) || !file.content.trim()) continue;
27
+ seen.add(file.path);
28
+ contents.add(file.content.trim());
29
+ added.push(file);
30
+ }
31
+ return [...base, ...added];
32
+ }
33
+
34
+ // Not beside an `AGENTS.override.md`: that file exists to displace the others in its directory.
35
+ static #claude(base: AkanContextFile[]) {
36
+ const files: AkanContextFile[] = [];
37
+ for (const file of base) {
38
+ if (!/^AGENTS\.(md|MD)$/.test(path.basename(file.path))) continue;
39
+ for (const name of ["CLAUDE.md", "CLAUDE.MD"]) {
40
+ const candidate = path.join(path.dirname(file.path), name);
41
+ const content = AkanContextFiles.#read(candidate);
42
+ if (content !== undefined) files.push({ path: candidate, content });
43
+ }
44
+ }
45
+ return files;
46
+ }
47
+
48
+ // Only `alwaysApply: true` or frontmatter-less rules: Cursor's `globs`/`description`/manual kinds are scoped.
49
+ static #cursor(options: AkanContextFilesOptions) {
50
+ const files: AkanContextFile[] = [];
51
+ const roots = [...new Set([options.workspaceRoot, options.cwd])];
52
+ for (const root of roots) {
53
+ const legacy = AkanContextFiles.#read(path.join(root, ".cursorrules"));
54
+ if (legacy !== undefined) files.push({ path: path.join(root, ".cursorrules"), content: legacy });
55
+ for (const file of AkanContextFiles.#mdcFiles(path.join(root, ".cursor", "rules"))) {
56
+ const raw = AkanContextFiles.#read(file);
57
+ if (raw === undefined) continue;
58
+ const body = AkanContextFiles.#applied(raw);
59
+ if (body !== undefined) files.push({ path: file, content: body });
60
+ }
61
+ }
62
+ return files;
63
+ }
64
+
65
+ static #applied(raw: string) {
66
+ const match = AkanContextFiles.#frontmatter.exec(raw);
67
+ if (!match) return raw;
68
+ if (!/^\s*alwaysApply\s*:\s*true\s*$/m.test(match[1] ?? "")) return undefined;
69
+ return raw.slice(match[0].length);
70
+ }
71
+
72
+ /** Sorted, because the order rules reach the model in is otherwise the order a directory happens to list. */
73
+ static #mdcFiles(dir: string) {
74
+ if (!existsSync(dir)) return [];
75
+ try {
76
+ return readdirSync(dir, { recursive: true, withFileTypes: true })
77
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".mdc"))
78
+ .map((entry) => path.join(entry.parentPath ?? dir, entry.name))
79
+ .sort();
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ static #read(file: string) {
86
+ try {
87
+ if (!existsSync(file) || !statSync(file).isFile()) return undefined;
88
+ return readFileSync(file, "utf8").replace(/^/, "");
89
+ } catch {
90
+ // A rule file that cannot be read costs its own rules. It must not cost the session its start.
91
+ return undefined;
92
+ }
93
+ }
94
+ }