@neurocode-ai/core 1.18.8

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 (490) hide show
  1. package/bin/neurocode +199 -0
  2. package/bunfig.toml +2 -0
  3. package/drizzle.config.ts +10 -0
  4. package/package.json +131 -0
  5. package/schema.json +2071 -0
  6. package/script/fix-node-pty.ts +28 -0
  7. package/script/migration.ts +196 -0
  8. package/src/account/sql.ts +39 -0
  9. package/src/account.ts +101 -0
  10. package/src/agent.ts +111 -0
  11. package/src/aisdk.ts +235 -0
  12. package/src/background-job.ts +365 -0
  13. package/src/catalog.ts +301 -0
  14. package/src/command.ts +64 -0
  15. package/src/config/agent.ts +25 -0
  16. package/src/config/attachments.ts +15 -0
  17. package/src/config/command.ts +12 -0
  18. package/src/config/compaction.ts +15 -0
  19. package/src/config/experimental.ts +18 -0
  20. package/src/config/formatter.ts +12 -0
  21. package/src/config/lsp.ts +18 -0
  22. package/src/config/markdown.ts +36 -0
  23. package/src/config/mcp.ts +48 -0
  24. package/src/config/plugin/agent.ts +179 -0
  25. package/src/config/plugin/command.ts +83 -0
  26. package/src/config/plugin/external.ts +91 -0
  27. package/src/config/plugin/provider.ts +113 -0
  28. package/src/config/plugin/reference.ts +70 -0
  29. package/src/config/plugin/skill.ts +50 -0
  30. package/src/config/plugin.ts +13 -0
  31. package/src/config/provider.ts +71 -0
  32. package/src/config/reference.ts +22 -0
  33. package/src/config/tool-output.ts +9 -0
  34. package/src/config/watcher.ts +7 -0
  35. package/src/config.ts +227 -0
  36. package/src/control-plane/move-session.ts +148 -0
  37. package/src/control-plane/workspace.sql.ts +20 -0
  38. package/src/credential/sql.ts +14 -0
  39. package/src/credential.ts +138 -0
  40. package/src/cross-spawn-spawner.ts +507 -0
  41. package/src/data-migration.sql.ts +6 -0
  42. package/src/database/database.ts +57 -0
  43. package/src/database/migration/20260127222353_familiar_lady_ursula.ts +107 -0
  44. package/src/database/migration/20260211171708_add_project_commands.ts +11 -0
  45. package/src/database/migration/20260213144116_wakeful_the_professor.ts +23 -0
  46. package/src/database/migration/20260225215848_workspace.ts +19 -0
  47. package/src/database/migration/20260227213759_add_session_workspace_id.ts +12 -0
  48. package/src/database/migration/20260228203230_blue_harpoon.ts +30 -0
  49. package/src/database/migration/20260303231226_add_workspace_fields.ts +15 -0
  50. package/src/database/migration/20260309230000_move_org_to_state.ts +15 -0
  51. package/src/database/migration/20260312043431_session_message_cursor.ts +16 -0
  52. package/src/database/migration/20260323234822_events.ts +26 -0
  53. package/src/database/migration/20260410174513_workspace-name.ts +29 -0
  54. package/src/database/migration/20260413175956_chief_energizer.ts +24 -0
  55. package/src/database/migration/20260423070820_add_icon_url_override.ts +14 -0
  56. package/src/database/migration/20260427172553_slow_nightmare.ts +30 -0
  57. package/src/database/migration/20260428004200_add_session_path.ts +11 -0
  58. package/src/database/migration/20260501142318_next_venus.ts +12 -0
  59. package/src/database/migration/20260504145000_add_sync_owner.ts +11 -0
  60. package/src/database/migration/20260507164347_add_workspace_time.ts +11 -0
  61. package/src/database/migration/20260510033149_session_usage.ts +56 -0
  62. package/src/database/migration/20260511000411_data_migration_state.ts +16 -0
  63. package/src/database/migration/20260511173437_session-metadata.ts +16 -0
  64. package/src/database/migration/20260601010001_normalize_storage_paths.ts +22 -0
  65. package/src/database/migration/20260601202201_amazing_prowler.ts +11 -0
  66. package/src/database/migration/20260602002951_lowly_union_jack.ts +24 -0
  67. package/src/database/migration/20260602182828_add_project_directories.ts +20 -0
  68. package/src/database/migration/20260603001617_session_message_projection_indexes.ts +19 -0
  69. package/src/database/migration/20260603040000_session_message_projection_order.ts +19 -0
  70. package/src/database/migration/20260603141458_session_input_inbox.ts +25 -0
  71. package/src/database/migration/20260603160727_jittery_ezekiel_stane.ts +20 -0
  72. package/src/database/migration/20260604172448_event_sourced_session_input.ts +47 -0
  73. package/src/database/migration/20260605003541_add_session_context_snapshot.ts +21 -0
  74. package/src/database/migration/20260605042240_add_context_epoch_agent.ts +11 -0
  75. package/src/database/migration/20260611035744_credential.ts +25 -0
  76. package/src/database/migration/20260611192811_lush_chimera.ts +25 -0
  77. package/src/database/migration/20260612174303_project_dir_strategy.ts +29 -0
  78. package/src/database/migration/20260622142730_simplify_session_context_epoch.ts +13 -0
  79. package/src/database/migration/20260622170816_reset_v2_session_state.ts +17 -0
  80. package/src/database/migration/20260622202450_simplify_session_input.ts +17 -0
  81. package/src/database/migration.gen.ts +44 -0
  82. package/src/database/migration.ts +81 -0
  83. package/src/database/path.ts +91 -0
  84. package/src/database/schema.gen.ts +274 -0
  85. package/src/database/schema.sql.ts +10 -0
  86. package/src/database/sqlite.bun.ts +183 -0
  87. package/src/database/sqlite.node.ts +178 -0
  88. package/src/database/sqlite.ts +8 -0
  89. package/src/effect/app-node-builder.ts +23 -0
  90. package/src/effect/app-node-platform.ts +18 -0
  91. package/src/effect/app-node.ts +14 -0
  92. package/src/effect/dfdf +1 -0
  93. package/src/effect/keyed-mutex.ts +45 -0
  94. package/src/effect/layer-node.ts +333 -0
  95. package/src/effect/memo-map.ts +3 -0
  96. package/src/effect/runtime.ts +21 -0
  97. package/src/effect/service-use.ts +43 -0
  98. package/src/event/sql.ts +25 -0
  99. package/src/event.ts +638 -0
  100. package/src/file-mutation.ts +207 -0
  101. package/src/file.ts +6 -0
  102. package/src/filesystem/fff.bun.ts +140 -0
  103. package/src/filesystem/fff.node.ts +138 -0
  104. package/src/filesystem/ignore.ts +67 -0
  105. package/src/filesystem/protected.ts +53 -0
  106. package/src/filesystem/search.ts +239 -0
  107. package/src/filesystem/watcher.ts +140 -0
  108. package/src/filesystem.ts +118 -0
  109. package/src/flag/flag.ts +78 -0
  110. package/src/fs-util.ts +274 -0
  111. package/src/git.ts +987 -0
  112. package/src/github-copilot/README.md +5 -0
  113. package/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  114. package/src/github-copilot/chat/get-response-metadata.ts +15 -0
  115. package/src/github-copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  116. package/src/github-copilot/chat/openai-compatible-api-types.ts +64 -0
  117. package/src/github-copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  118. package/src/github-copilot/chat/openai-compatible-chat-options.ts +28 -0
  119. package/src/github-copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  120. package/src/github-copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  121. package/src/github-copilot/copilot-provider.ts +100 -0
  122. package/src/github-copilot/openai-compatible-error.ts +27 -0
  123. package/src/github-copilot/responses/convert-to-openai-responses-input.ts +335 -0
  124. package/src/github-copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  125. package/src/github-copilot/responses/openai-config.ts +18 -0
  126. package/src/github-copilot/responses/openai-error.ts +22 -0
  127. package/src/github-copilot/responses/openai-responses-api-types.ts +214 -0
  128. package/src/github-copilot/responses/openai-responses-language-model.ts +1770 -0
  129. package/src/github-copilot/responses/openai-responses-prepare-tools.ts +173 -0
  130. package/src/github-copilot/responses/openai-responses-settings.ts +1 -0
  131. package/src/github-copilot/responses/tool/code-interpreter.ts +87 -0
  132. package/src/github-copilot/responses/tool/file-search.ts +127 -0
  133. package/src/github-copilot/responses/tool/image-generation.ts +114 -0
  134. package/src/github-copilot/responses/tool/local-shell.ts +64 -0
  135. package/src/github-copilot/responses/tool/web-search-preview.ts +103 -0
  136. package/src/github-copilot/responses/tool/web-search.ts +102 -0
  137. package/src/global.ts +87 -0
  138. package/src/id/id.ts +47 -0
  139. package/src/image/photon.ts +94 -0
  140. package/src/image.ts +81 -0
  141. package/src/installation/version.ts +8 -0
  142. package/src/instruction-context.ts +101 -0
  143. package/src/integration/connection.ts +12 -0
  144. package/src/integration.ts +520 -0
  145. package/src/location-mutation.ts +162 -0
  146. package/src/location-service-map.ts +18 -0
  147. package/src/location-services.ts +115 -0
  148. package/src/location.ts +39 -0
  149. package/src/markdown.d.ts +4 -0
  150. package/src/model.ts +41 -0
  151. package/src/models-dev.ts +260 -0
  152. package/src/npm-config.ts +40 -0
  153. package/src/npm.ts +269 -0
  154. package/src/oauth/page.ts +276 -0
  155. package/src/observability/logging.ts +71 -0
  156. package/src/observability/otlp.ts +79 -0
  157. package/src/observability/shared.ts +1 -0
  158. package/src/observability.ts +24 -0
  159. package/src/patch.ts +197 -0
  160. package/src/permission/saved.ts +79 -0
  161. package/src/permission/sql.ts +20 -0
  162. package/src/permission.ts +310 -0
  163. package/src/plugin/agent.ts +206 -0
  164. package/src/plugin/command/initialize.txt +65 -0
  165. package/src/plugin/command/review.txt +100 -0
  166. package/src/plugin/command.ts +25 -0
  167. package/src/plugin/host.ts +219 -0
  168. package/src/plugin/internal.ts +153 -0
  169. package/src/plugin/layer-map.example.ts +94 -0
  170. package/src/plugin/models-dev.ts +183 -0
  171. package/src/plugin/promise.ts +93 -0
  172. package/src/plugin/provider/alibaba.ts +15 -0
  173. package/src/plugin/provider/amazon-bedrock.ts +126 -0
  174. package/src/plugin/provider/anthropic.ts +27 -0
  175. package/src/plugin/provider/azure.ts +82 -0
  176. package/src/plugin/provider/cerebras.ts +26 -0
  177. package/src/plugin/provider/cloudflare-ai-gateway.ts +81 -0
  178. package/src/plugin/provider/cloudflare-workers-ai.ts +81 -0
  179. package/src/plugin/provider/cohere.ts +15 -0
  180. package/src/plugin/provider/deepinfra.ts +15 -0
  181. package/src/plugin/provider/dynamic.ts +31 -0
  182. package/src/plugin/provider/gateway.ts +15 -0
  183. package/src/plugin/provider/github-copilot.ts +52 -0
  184. package/src/plugin/provider/gitlab.ts +65 -0
  185. package/src/plugin/provider/google-vertex.ts +173 -0
  186. package/src/plugin/provider/google.ts +15 -0
  187. package/src/plugin/provider/groq.ts +15 -0
  188. package/src/plugin/provider/kilo.ts +21 -0
  189. package/src/plugin/provider/llmgateway.ts +26 -0
  190. package/src/plugin/provider/mistral.ts +15 -0
  191. package/src/plugin/provider/nvidia.ts +22 -0
  192. package/src/plugin/provider/openai-compatible.ts +17 -0
  193. package/src/plugin/provider/openai.ts +292 -0
  194. package/src/plugin/provider/opencode.ts +311 -0
  195. package/src/plugin/provider/openrouter.ts +36 -0
  196. package/src/plugin/provider/perplexity.ts +15 -0
  197. package/src/plugin/provider/sap-ai-core.ts +46 -0
  198. package/src/plugin/provider/snowflake-cortex.ts +89 -0
  199. package/src/plugin/provider/togetherai.ts +15 -0
  200. package/src/plugin/provider/venice.ts +15 -0
  201. package/src/plugin/provider/vercel.ts +27 -0
  202. package/src/plugin/provider/xai.ts +22 -0
  203. package/src/plugin/provider/zenmux.ts +21 -0
  204. package/src/plugin/provider.ts +71 -0
  205. package/src/plugin/skill/customize-neurocode.md +452 -0
  206. package/src/plugin/skill.ts +31 -0
  207. package/src/plugin/variant.ts +39 -0
  208. package/src/plugin.ts +167 -0
  209. package/src/policy.ts +49 -0
  210. package/src/process.ts +261 -0
  211. package/src/project/copy-strategies.ts +35 -0
  212. package/src/project/copy.ts +292 -0
  213. package/src/project/directories.ts +158 -0
  214. package/src/project/schema.ts +16 -0
  215. package/src/project/sql.ts +35 -0
  216. package/src/project.ts +136 -0
  217. package/src/provider.ts +25 -0
  218. package/src/pty/protocol.ts +37 -0
  219. package/src/pty/pty.bun.ts +26 -0
  220. package/src/pty/pty.node.ts +29 -0
  221. package/src/pty/pty.ts +25 -0
  222. package/src/pty/schema.ts +1 -0
  223. package/src/pty/ticket.ts +56 -0
  224. package/src/pty.ts +318 -0
  225. package/src/public-event-manifest.ts +7 -0
  226. package/src/question.ts +153 -0
  227. package/src/reference/guidance.ts +69 -0
  228. package/src/reference.ts +125 -0
  229. package/src/repository-cache.ts +259 -0
  230. package/src/repository.ts +214 -0
  231. package/src/ripgrep/binary.ts +132 -0
  232. package/src/ripgrep.ts +281 -0
  233. package/src/schema.ts +79 -0
  234. package/src/session/compaction.ts +241 -0
  235. package/src/session/context-epoch.ts +174 -0
  236. package/src/session/error.ts +24 -0
  237. package/src/session/event.ts +2 -0
  238. package/src/session/execution/local.ts +46 -0
  239. package/src/session/execution.ts +34 -0
  240. package/src/session/history.ts +101 -0
  241. package/src/session/info.ts +50 -0
  242. package/src/session/input.ts +288 -0
  243. package/src/session/message-updater.ts +397 -0
  244. package/src/session/message.ts +2 -0
  245. package/src/session/projector.ts +458 -0
  246. package/src/session/prompt.ts +1 -0
  247. package/src/session/revert.ts +121 -0
  248. package/src/session/run-coordinator.ts +104 -0
  249. package/src/session/runner/index.ts +28 -0
  250. package/src/session/runner/llm.ts +432 -0
  251. package/src/session/runner/max-steps.ts +16 -0
  252. package/src/session/runner/model.ts +218 -0
  253. package/src/session/runner/publish-llm-event.ts +423 -0
  254. package/src/session/runner/to-llm-message.ts +171 -0
  255. package/src/session/schema.ts +9 -0
  256. package/src/session/sql.ts +176 -0
  257. package/src/session/store.ts +63 -0
  258. package/src/session/todo.ts +78 -0
  259. package/src/session.ts +486 -0
  260. package/src/share/sql.ts +13 -0
  261. package/src/shell.ts +226 -0
  262. package/src/skill/discovery.ts +213 -0
  263. package/src/skill/guidance.ts +76 -0
  264. package/src/skill.ts +132 -0
  265. package/src/snapshot.ts +266 -0
  266. package/src/state.ts +128 -0
  267. package/src/system-context/builtins.ts +50 -0
  268. package/src/system-context/index.ts +320 -0
  269. package/src/system-context/registry.ts +49 -0
  270. package/src/tool/AGENTS.md +59 -0
  271. package/src/tool/application-tools.ts +57 -0
  272. package/src/tool/apply-patch.ts +219 -0
  273. package/src/tool/bash.ts +207 -0
  274. package/src/tool/builtins.ts +48 -0
  275. package/src/tool/edit.ts +223 -0
  276. package/src/tool/glob.ts +105 -0
  277. package/src/tool/grep.ts +137 -0
  278. package/src/tool/http-body.ts +30 -0
  279. package/src/tool/question.ts +94 -0
  280. package/src/tool/read-filesystem.ts +366 -0
  281. package/src/tool/read.ts +117 -0
  282. package/src/tool/registry.ts +147 -0
  283. package/src/tool/skill.ts +109 -0
  284. package/src/tool/todowrite.ts +62 -0
  285. package/src/tool/tool.ts +162 -0
  286. package/src/tool/tools.ts +13 -0
  287. package/src/tool/webfetch.ts +218 -0
  288. package/src/tool/websearch.ts +260 -0
  289. package/src/tool/write.ts +101 -0
  290. package/src/tool-output-store.ts +211 -0
  291. package/src/util/array.ts +10 -0
  292. package/src/util/binary.ts +41 -0
  293. package/src/util/effect-flock.ts +284 -0
  294. package/src/util/encode.ts +51 -0
  295. package/src/util/error.ts +70 -0
  296. package/src/util/flock.ts +358 -0
  297. package/src/util/glob.ts +34 -0
  298. package/src/util/hash.ts +11 -0
  299. package/src/util/identifier.ts +1 -0
  300. package/src/util/iife.ts +3 -0
  301. package/src/util/lazy.ts +11 -0
  302. package/src/util/module.ts +10 -0
  303. package/src/util/path.ts +37 -0
  304. package/src/util/retry.ts +42 -0
  305. package/src/util/slug.ts +74 -0
  306. package/src/util/token.ts +5 -0
  307. package/src/util/which.ts +14 -0
  308. package/src/util/wildcard.ts +14 -0
  309. package/src/v1/config/agent.ts +89 -0
  310. package/src/v1/config/attachment.ts +25 -0
  311. package/src/v1/config/command.ts +13 -0
  312. package/src/v1/config/config.ts +192 -0
  313. package/src/v1/config/console-state.ts +16 -0
  314. package/src/v1/config/error.ts +39 -0
  315. package/src/v1/config/formatter.ts +13 -0
  316. package/src/v1/config/layout.ts +6 -0
  317. package/src/v1/config/lsp.ts +80 -0
  318. package/src/v1/config/mcp.ts +63 -0
  319. package/src/v1/config/migrate.ts +254 -0
  320. package/src/v1/config/permission.ts +50 -0
  321. package/src/v1/config/plugin.ts +9 -0
  322. package/src/v1/config/provider-options.ts +227 -0
  323. package/src/v1/config/provider.ts +121 -0
  324. package/src/v1/config/server.ts +19 -0
  325. package/src/v1/config/skills.ts +13 -0
  326. package/src/v1/permission.ts +33 -0
  327. package/src/v1/session.ts +68 -0
  328. package/src/v2-schema.ts +3 -0
  329. package/src/workspace.ts +6 -0
  330. package/sst-env.d.ts +10 -0
  331. package/test/agent.test.ts +131 -0
  332. package/test/application-tools.test.ts +287 -0
  333. package/test/background-job.test.ts +106 -0
  334. package/test/catalog.test.ts +353 -0
  335. package/test/command.test.ts +56 -0
  336. package/test/config/agent.test.ts +349 -0
  337. package/test/config/command.test.ts +83 -0
  338. package/test/config/config.test.ts +798 -0
  339. package/test/config/fixtures/plugin/directory-plugin.ts +13 -0
  340. package/test/config/plugin.test.ts +248 -0
  341. package/test/config/provider-options.test.ts +224 -0
  342. package/test/config/provider.test.ts +269 -0
  343. package/test/config/skill.test.ts +80 -0
  344. package/test/credential.test.ts +36 -0
  345. package/test/database-migration.test.ts +644 -0
  346. package/test/effect/cross-spawn-spawner.test.ts +426 -0
  347. package/test/effect/keyed-mutex.test.ts +73 -0
  348. package/test/effect/layer-node/layer-node-types.test.ts +143 -0
  349. package/test/effect/layer-node/layer-node.test.ts +261 -0
  350. package/test/effect/layer-node/node-build.test.ts +125 -0
  351. package/test/effect/observability.test.ts +109 -0
  352. package/test/event.test.ts +1124 -0
  353. package/test/file-mutation.test.ts +368 -0
  354. package/test/filesystem/filesystem.test.ts +387 -0
  355. package/test/filesystem/ignore.test.ts +10 -0
  356. package/test/filesystem/search.test.ts +44 -0
  357. package/test/filesystem/watcher.test.ts +266 -0
  358. package/test/fixture/effect-flock-worker.ts +60 -0
  359. package/test/fixture/flock-worker.ts +72 -0
  360. package/test/fixture/git.ts +49 -0
  361. package/test/fixture/location.ts +26 -0
  362. package/test/fixture/tmpdir.ts +25 -0
  363. package/test/fixtures/recordings/session-runner/openai-chat-streams-text.json +27 -0
  364. package/test/git.test.ts +164 -0
  365. package/test/github-copilot/convert-to-copilot-messages.test.ts +523 -0
  366. package/test/github-copilot/copilot-chat-model.test.ts +592 -0
  367. package/test/github-copilot/openai-responses-language-model.test.ts +206 -0
  368. package/test/global.test.ts +16 -0
  369. package/test/instruction-context.test.ts +323 -0
  370. package/test/integration.test.ts +349 -0
  371. package/test/legacy-event-schema.test.ts +16 -0
  372. package/test/lib/effect.ts +53 -0
  373. package/test/lib/tool.ts +20 -0
  374. package/test/location-filesystem.test.ts +69 -0
  375. package/test/location-layer.test.ts +226 -0
  376. package/test/location-mutation.test.ts +178 -0
  377. package/test/location.test.ts +42 -0
  378. package/test/model.test.ts +23 -0
  379. package/test/models.test.ts +290 -0
  380. package/test/move-session.test.ts +235 -0
  381. package/test/npm-config.test.ts +51 -0
  382. package/test/npm.test.ts +84 -0
  383. package/test/oauth-page.test.ts +15 -0
  384. package/test/patch.test.ts +68 -0
  385. package/test/permission.test.ts +315 -0
  386. package/test/plugin/command.test.ts +47 -0
  387. package/test/plugin/fixture.ts +52 -0
  388. package/test/plugin/fixtures/config-effect-plugin.ts +15 -0
  389. package/test/plugin/fixtures/config-promise-plugin.ts +13 -0
  390. package/test/plugin/fixtures/invalid-plugin.ts +1 -0
  391. package/test/plugin/fixtures/models-dev.json +14 -0
  392. package/test/plugin/fixtures/provider-factory.ts +9 -0
  393. package/test/plugin/host.ts +303 -0
  394. package/test/plugin/models-dev.test.ts +170 -0
  395. package/test/plugin/promise.test.ts +67 -0
  396. package/test/plugin/provider-alibaba.test.ts +92 -0
  397. package/test/plugin/provider-amazon-bedrock.test.ts +622 -0
  398. package/test/plugin/provider-anthropic.test.ts +94 -0
  399. package/test/plugin/provider-azure-cognitive-services.test.ts +199 -0
  400. package/test/plugin/provider-azure.test.ts +293 -0
  401. package/test/plugin/provider-cerebras.test.ts +136 -0
  402. package/test/plugin/provider-cloudflare-ai-gateway.test.ts +426 -0
  403. package/test/plugin/provider-cloudflare-workers-ai.test.ts +279 -0
  404. package/test/plugin/provider-cohere.test.ts +123 -0
  405. package/test/plugin/provider-deepinfra.test.ts +159 -0
  406. package/test/plugin/provider-dynamic.test.ts +184 -0
  407. package/test/plugin/provider-gateway.test.ts +115 -0
  408. package/test/plugin/provider-github-copilot.test.ts +278 -0
  409. package/test/plugin/provider-gitlab.test.ts +308 -0
  410. package/test/plugin/provider-google-vertex-anthropic.test.ts +264 -0
  411. package/test/plugin/provider-google-vertex.test.ts +387 -0
  412. package/test/plugin/provider-google.test.ts +80 -0
  413. package/test/plugin/provider-groq.test.ts +121 -0
  414. package/test/plugin/provider-kilo.test.ts +99 -0
  415. package/test/plugin/provider-llmgateway.test.ts +80 -0
  416. package/test/plugin/provider-mistral.test.ts +129 -0
  417. package/test/plugin/provider-nvidia.test.ts +97 -0
  418. package/test/plugin/provider-openai-compatible.test.ts +107 -0
  419. package/test/plugin/provider-openai.test.ts +176 -0
  420. package/test/plugin/provider-opencode.test.ts +423 -0
  421. package/test/plugin/provider-openrouter.test.ts +113 -0
  422. package/test/plugin/provider-perplexity.test.ts +122 -0
  423. package/test/plugin/provider-sap-ai-core.test.ts +159 -0
  424. package/test/plugin/provider-snowflake-cortex.test.ts +261 -0
  425. package/test/plugin/provider-togetherai.test.ts +129 -0
  426. package/test/plugin/provider-venice.test.ts +115 -0
  427. package/test/plugin/provider-vercel.test.ts +83 -0
  428. package/test/plugin/provider-xai.test.ts +125 -0
  429. package/test/plugin/provider-zenmux.test.ts +110 -0
  430. package/test/plugin/skill.test.ts +25 -0
  431. package/test/plugin/variant.test.ts +67 -0
  432. package/test/plugin.test.ts +71 -0
  433. package/test/policy.test.ts +85 -0
  434. package/test/preload.ts +5 -0
  435. package/test/process/process.test.ts +368 -0
  436. package/test/project-copy.test.ts +400 -0
  437. package/test/project-directories.test.ts +61 -0
  438. package/test/project.test.ts +221 -0
  439. package/test/provider-mistral.test.ts +282 -0
  440. package/test/provider-xai-responses.test.ts +32 -0
  441. package/test/pty/info-schema.test.ts +32 -0
  442. package/test/pty/protocol.test.ts +27 -0
  443. package/test/pty/pty-session.test.ts +240 -0
  444. package/test/pty/ticket.test.ts +62 -0
  445. package/test/question.test.ts +114 -0
  446. package/test/reference-guidance.test.ts +76 -0
  447. package/test/reference.test.ts +78 -0
  448. package/test/repository-cache.test.ts +153 -0
  449. package/test/repository.test.ts +71 -0
  450. package/test/ripgrep.test.ts +65 -0
  451. package/test/session-compaction.test.ts +27 -0
  452. package/test/session-create.test.ts +425 -0
  453. package/test/session-history.test.ts +165 -0
  454. package/test/session-projector.test.ts +533 -0
  455. package/test/session-prompt.test.ts +584 -0
  456. package/test/session-run-coordinator.test.ts +418 -0
  457. package/test/session-runner-message.test.ts +501 -0
  458. package/test/session-runner-model.test.ts +347 -0
  459. package/test/session-runner-recorded.test.ts +193 -0
  460. package/test/session-runner-tool-events.test.ts +136 -0
  461. package/test/session-runner-tool-registry.test.ts +452 -0
  462. package/test/session-runner.test.ts +3365 -0
  463. package/test/session-todo.test.ts +94 -0
  464. package/test/session-tool-progress.test.ts +158 -0
  465. package/test/shared-schema.test.ts +206 -0
  466. package/test/shell.test.ts +108 -0
  467. package/test/skill/guidance.test.ts +144 -0
  468. package/test/skill-discovery.test.ts +165 -0
  469. package/test/skill.test.ts +125 -0
  470. package/test/snapshot.test.ts +178 -0
  471. package/test/state.test.ts +115 -0
  472. package/test/system-context/builtins.test.ts +129 -0
  473. package/test/system-context/index.test.ts +307 -0
  474. package/test/system-context/registry.test.ts +114 -0
  475. package/test/tool-apply-patch.test.ts +436 -0
  476. package/test/tool-bash.test.ts +437 -0
  477. package/test/tool-edit.test.ts +434 -0
  478. package/test/tool-output-store.test.ts +247 -0
  479. package/test/tool-question.test.ts +162 -0
  480. package/test/tool-read-filesystem.test.ts +118 -0
  481. package/test/tool-read.test.ts +674 -0
  482. package/test/tool-skill.test.ts +149 -0
  483. package/test/tool-todowrite.test.ts +125 -0
  484. package/test/tool-webfetch.test.ts +281 -0
  485. package/test/tool-websearch.test.ts +316 -0
  486. package/test/tool-write.test.ts +305 -0
  487. package/test/util/effect-flock.test.ts +392 -0
  488. package/test/util/flock.test.ts +428 -0
  489. package/test/util/which.test.ts +100 -0
  490. package/tsconfig.json +7 -0
package/src/policy.ts ADDED
@@ -0,0 +1,49 @@
1
+ export * as Policy from "./policy"
2
+
3
+ import { makeLocationNode } from "./effect/app-node"
4
+ import { Context, Effect as EffectRuntime, Layer, Schema } from "effect"
5
+ import { Wildcard } from "./util/wildcard"
6
+ import { Location } from "./location"
7
+
8
+ export const Effect = Schema.Literals(["allow", "deny"]).annotate({ identifier: "Policy.Effect" })
9
+ export type Effect = typeof Effect.Type
10
+
11
+ export class Info extends Schema.Class<Info>("Policy.Info")({
12
+ action: Schema.String,
13
+ effect: Effect,
14
+ resource: Schema.String,
15
+ }) {}
16
+
17
+ export interface Interface {
18
+ readonly load: (statements: Info[]) => EffectRuntime.Effect<void>
19
+ readonly evaluate: (action: string, resource: string, fallback: Effect) => EffectRuntime.Effect<Effect>
20
+ readonly hasStatements: () => boolean
21
+ }
22
+
23
+ export class Service extends Context.Service<Service, Interface>()("@neurocode/v2/Policy") {}
24
+
25
+ const layer = Layer.effect(
26
+ Service,
27
+ EffectRuntime.gen(function* () {
28
+ let statements: Info[] = []
29
+ yield* Location.Service
30
+
31
+ return Service.of({
32
+ load: EffectRuntime.fn("Policy.load")(function* (input) {
33
+ statements = input
34
+ }),
35
+ hasStatements: () => statements.length > 0,
36
+ evaluate: EffectRuntime.fn("Policy.evaluate")(function* (action, resource, fallback) {
37
+ return (
38
+ statements.findLast(
39
+ (statement) => Wildcard.match(action, statement.action) && Wildcard.match(resource, statement.resource),
40
+ )?.effect ?? fallback
41
+ )
42
+ }),
43
+ })
44
+ }),
45
+ )
46
+
47
+ export const locationLayer = layer
48
+
49
+ export const node = makeLocationNode({ service: Service, layer, deps: [Location.node] })
package/src/process.ts ADDED
@@ -0,0 +1,261 @@
1
+ import { Context, Duration, Effect, Fiber, Layer, Schema, Stream } from "effect"
2
+ import type { PlatformError } from "effect/PlatformError"
3
+ import { ChildProcess } from "effect/unstable/process"
4
+ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
5
+ import { CrossSpawnSpawner } from "./cross-spawn-spawner"
6
+ import { makeGlobalNode } from "./effect/app-node"
7
+
8
+ export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()("AppProcessError", {
9
+ command: Schema.String,
10
+ exitCode: Schema.optional(Schema.Number),
11
+ stderr: Schema.optional(Schema.String),
12
+ cause: Schema.optional(Schema.Defect()),
13
+ }) {
14
+ override get message() {
15
+ const detail =
16
+ this.stderr?.trim() || (this.cause instanceof Error ? this.cause.message : this.cause && String(this.cause))
17
+ const status = this.exitCode === undefined ? "" : ` (exit ${this.exitCode})`
18
+ return `Command failed${status}: ${this.command}${detail ? `: ${detail}` : ""}`
19
+ }
20
+ }
21
+
22
+ export interface RunOptions {
23
+ readonly combineOutput?: boolean
24
+ readonly maxOutputBytes?: number
25
+ readonly maxErrorBytes?: number
26
+ readonly signal?: AbortSignal
27
+ readonly timeout?: Duration.Input
28
+ readonly stdin?: string | Uint8Array | Stream.Stream<Uint8Array, PlatformError>
29
+ }
30
+
31
+ export interface RunStreamOptions {
32
+ readonly signal?: AbortSignal
33
+ readonly includeStderr?: boolean
34
+ readonly okExitCodes?: ReadonlyArray<number>
35
+ readonly maxErrorBytes?: number
36
+ }
37
+
38
+ export interface RunResult {
39
+ readonly command: string
40
+ readonly exitCode: number
41
+ readonly output?: Buffer
42
+ readonly stdout: Buffer
43
+ readonly stderr: Buffer
44
+ readonly outputTruncated?: boolean
45
+ readonly stdoutTruncated: boolean
46
+ readonly stderrTruncated: boolean
47
+ }
48
+
49
+ export type Interface = ChildProcessSpawner["Service"] & {
50
+ readonly run: (command: ChildProcess.Command, options?: RunOptions) => Effect.Effect<RunResult, AppProcessError>
51
+ readonly runStream: (
52
+ command: ChildProcess.Command,
53
+ options?: RunStreamOptions,
54
+ ) => Stream.Stream<string, AppProcessError>
55
+ }
56
+
57
+ export class Service extends Context.Service<Service, Interface>()("@neurocode/AppProcess") {}
58
+
59
+ export const requireSuccess = (result: RunResult): Effect.Effect<RunResult, AppProcessError> =>
60
+ result.exitCode === 0
61
+ ? Effect.succeed(result)
62
+ : Effect.fail(
63
+ new AppProcessError({
64
+ command: result.command,
65
+ exitCode: result.exitCode,
66
+ stderr: result.stderr.toString("utf8"),
67
+ }),
68
+ )
69
+
70
+ export const requireExitIn =
71
+ (codes: ReadonlyArray<number>) =>
72
+ (result: RunResult): Effect.Effect<RunResult, AppProcessError> =>
73
+ codes.includes(result.exitCode)
74
+ ? Effect.succeed(result)
75
+ : Effect.fail(
76
+ new AppProcessError({
77
+ command: result.command,
78
+ exitCode: result.exitCode,
79
+ stderr: result.stderr.toString("utf8"),
80
+ }),
81
+ )
82
+
83
+ const describeCommand = (command: ChildProcess.Command): string => {
84
+ if (command._tag === "StandardCommand") {
85
+ return command.args.length ? `${command.command} ${command.args.join(" ")}` : command.command
86
+ }
87
+ return `${describeCommand(command.left)} | ${describeCommand(command.right)}`
88
+ }
89
+
90
+ const wrapError = (description: string, cause: unknown): AppProcessError =>
91
+ cause instanceof AppProcessError ? cause : new AppProcessError({ command: description, cause })
92
+
93
+ export const abortError = (signal: AbortSignal): Error => {
94
+ const reason = signal.reason
95
+ if (reason instanceof Error) return reason
96
+ const err = new Error("Aborted")
97
+ err.name = "AbortError"
98
+ return err
99
+ }
100
+
101
+ export const waitForAbort = (signal: AbortSignal) =>
102
+ Effect.callback<never, Error>((resume) => {
103
+ if (signal.aborted) {
104
+ resume(Effect.fail(abortError(signal)))
105
+ return
106
+ }
107
+ const onabort = () => resume(Effect.fail(abortError(signal)))
108
+ signal.addEventListener("abort", onabort, { once: true })
109
+ return Effect.sync(() => signal.removeEventListener("abort", onabort))
110
+ })
111
+
112
+ const normalizeStdin = (
113
+ input: string | Uint8Array | Stream.Stream<Uint8Array, PlatformError>,
114
+ ): Stream.Stream<Uint8Array, PlatformError> =>
115
+ typeof input === "string"
116
+ ? Stream.make(new TextEncoder().encode(input))
117
+ : input instanceof Uint8Array
118
+ ? Stream.make(input)
119
+ : input
120
+
121
+ export const collectStream = (stream: Stream.Stream<Uint8Array, PlatformError>, maxOutputBytes: number | undefined) =>
122
+ Stream.runFold(
123
+ stream,
124
+ () => ({ chunks: [] as Uint8Array[], bytes: 0, truncated: false }),
125
+ (acc, chunk) => {
126
+ if (maxOutputBytes === undefined) {
127
+ acc.chunks.push(chunk)
128
+ acc.bytes += chunk.length
129
+ return acc
130
+ }
131
+ const remaining = maxOutputBytes - acc.bytes
132
+ if (remaining > 0) acc.chunks.push(remaining >= chunk.length ? chunk : chunk.slice(0, remaining))
133
+ acc.bytes += chunk.length
134
+ acc.truncated = acc.truncated || acc.bytes > maxOutputBytes
135
+ return acc
136
+ },
137
+ ).pipe(Effect.map((x) => ({ buffer: Buffer.concat(x.chunks), truncated: x.truncated })))
138
+
139
+ const layer = Layer.effect(
140
+ Service,
141
+ Effect.gen(function* () {
142
+ const spawner = yield* ChildProcessSpawner
143
+
144
+ const runCommand = (command: ChildProcess.Command, options?: RunOptions) => {
145
+ const description = describeCommand(command)
146
+ const collect = Effect.scoped(
147
+ Effect.gen(function* () {
148
+ const handle = yield* spawner.spawn(command)
149
+ if (options?.combineOutput) {
150
+ const [output, exitCode] = yield* Effect.all(
151
+ [collectStream(handle.all, options.maxOutputBytes), handle.exitCode],
152
+ { concurrency: "unbounded" },
153
+ )
154
+ return {
155
+ command: description,
156
+ exitCode,
157
+ output: output.buffer,
158
+ stdout: Buffer.alloc(0),
159
+ stderr: Buffer.alloc(0),
160
+ outputTruncated: output.truncated,
161
+ stdoutTruncated: false,
162
+ stderrTruncated: false,
163
+ } satisfies RunResult
164
+ }
165
+ const [stdout, stderr, exitCode] = yield* Effect.all(
166
+ [
167
+ collectStream(handle.stdout, options?.maxOutputBytes),
168
+ collectStream(handle.stderr, options?.maxErrorBytes),
169
+ handle.exitCode,
170
+ ],
171
+ { concurrency: "unbounded" },
172
+ )
173
+ return {
174
+ command: description,
175
+ exitCode,
176
+ stdout: stdout.buffer,
177
+ stderr: stderr.buffer,
178
+ stdoutTruncated: stdout.truncated,
179
+ stderrTruncated: stderr.truncated,
180
+ } satisfies RunResult
181
+ }),
182
+ )
183
+ const timed = options?.timeout
184
+ ? Effect.timeoutOrElse(collect, {
185
+ duration: options.timeout,
186
+ orElse: () => Effect.fail(new AppProcessError({ command: description, cause: new Error("Timed out") })),
187
+ })
188
+ : collect
189
+ const aborted = options?.signal
190
+ ? timed.pipe(
191
+ Effect.raceFirst(
192
+ waitForAbort(options.signal).pipe(Effect.mapError((cause) => wrapError(description, cause))),
193
+ ),
194
+ )
195
+ : timed
196
+ return aborted.pipe(Effect.catch((cause) => Effect.fail(wrapError(description, cause))))
197
+ }
198
+
199
+ const run = Effect.fn("AppProcess.run")(function* (command: ChildProcess.Command, options?: RunOptions) {
200
+ if (options?.stdin === undefined) return yield* runCommand(command, options)
201
+ if (command._tag !== "StandardCommand") {
202
+ return yield* new AppProcessError({
203
+ command: describeCommand(command),
204
+ cause: new Error("stdin option only supports StandardCommand; received PipedCommand"),
205
+ })
206
+ }
207
+ const next = ChildProcess.make(command.command, command.args, {
208
+ ...command.options,
209
+ stdin: normalizeStdin(options.stdin),
210
+ })
211
+ return yield* runCommand(next, options)
212
+ })
213
+
214
+ const runStream = (
215
+ command: ChildProcess.Command,
216
+ options?: RunStreamOptions,
217
+ ): Stream.Stream<string, AppProcessError> => {
218
+ const description = describeCommand(command)
219
+ const okExitCodes = options?.okExitCodes
220
+ const built: Stream.Stream<string, AppProcessError | PlatformError> = Stream.unwrap(
221
+ Effect.gen(function* () {
222
+ const handle = yield* spawner.spawn(command)
223
+ const stderrFiber = yield* Effect.forkScoped(
224
+ collectStream(handle.stderr, options?.maxErrorBytes).pipe(Effect.map((x) => x.buffer.toString("utf8"))),
225
+ )
226
+ const source = options?.includeStderr === true ? handle.all : handle.stdout
227
+ const lines = source.pipe(
228
+ Stream.decodeText,
229
+ Stream.splitLines,
230
+ Stream.filter((line) => line.length > 0),
231
+ )
232
+ const tail = Stream.unwrap(
233
+ Effect.gen(function* () {
234
+ const code = yield* handle.exitCode
235
+ if (okExitCodes && okExitCodes.length > 0 && !okExitCodes.includes(code)) {
236
+ const stderr = yield* Fiber.join(stderrFiber)
237
+ return Stream.fail(new AppProcessError({ command: description, exitCode: code, stderr }))
238
+ }
239
+ return Stream.empty
240
+ }),
241
+ )
242
+ return Stream.concat(lines, tail) as Stream.Stream<string, AppProcessError | PlatformError>
243
+ }),
244
+ )
245
+ const mapped = built.pipe(
246
+ Stream.catch((cause): Stream.Stream<string, AppProcessError> => Stream.fail(wrapError(description, cause))),
247
+ )
248
+ if (!options?.signal) return mapped
249
+ const signal = options.signal
250
+ return mapped.pipe(
251
+ Stream.interruptWhen(waitForAbort(signal).pipe(Effect.mapError((cause) => wrapError(description, cause)))),
252
+ )
253
+ }
254
+
255
+ return Service.of({ ...spawner, run, runStream })
256
+ }),
257
+ )
258
+
259
+ export const node = makeGlobalNode({ service: Service, layer: layer, deps: [CrossSpawnSpawner.node] })
260
+
261
+ export * as AppProcess from "./process"
@@ -0,0 +1,35 @@
1
+ import { Effect } from "effect"
2
+ import { AbsolutePath } from "../schema"
3
+ import { Git } from "../git"
4
+ import { DirectoryUnavailableError, StrategyID, type ListEntry, type Strategy } from "./copy"
5
+
6
+ export function makeGitWorktreeStrategy(input: {
7
+ git: Git.Interface
8
+ canonical: (directory: AbsolutePath) => Effect.Effect<AbsolutePath, DirectoryUnavailableError>
9
+ }) {
10
+ return {
11
+ id: StrategyID.make("git_worktree"),
12
+ create: Effect.fn("ProjectCopy.GitWorktree.create")(function* (options) {
13
+ const repository = yield* input.git.repo.discover(options.sourceDirectory)
14
+ if (!repository) return yield* new DirectoryUnavailableError({ directory: options.sourceDirectory })
15
+ yield* input.git.worktree.create({ repository, directory: options.directory })
16
+ return { directory: yield* input.canonical(options.directory) }
17
+ }),
18
+ remove: Effect.fn("ProjectCopy.GitWorktree.remove")(function* (options) {
19
+ const found = yield* input.git.repo.discover(options.directory)
20
+ if (!found) return yield* new DirectoryUnavailableError({ directory: options.directory })
21
+ yield* input.git.worktree.remove({ repository: found, directory: options.directory, force: options.force })
22
+ }),
23
+ list: Effect.fn("ProjectCopy.GitWorktree.list")(function* (directory) {
24
+ const found = yield* input.git.repo.discover(directory)
25
+ if (!found) return yield* new DirectoryUnavailableError({ directory })
26
+ const entries = yield* input.git.worktree.list(found)
27
+ return yield* Effect.forEach(entries, (entry) =>
28
+ input.canonical(entry.directory).pipe(
29
+ Effect.map((directory) => ({ directory, type: entry.kind === "main" ? "root" : "copy" }) as const),
30
+ Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed(undefined)),
31
+ ),
32
+ ).pipe(Effect.map((items) => items.filter((item): item is ListEntry => item !== undefined)))
33
+ }),
34
+ } satisfies Strategy
35
+ }
@@ -0,0 +1,292 @@
1
+ export * as ProjectCopy from "./copy"
2
+
3
+ import { Context, Effect, Layer, Schema } from "effect"
4
+ import path from "path"
5
+ import { AbsolutePath } from "../schema"
6
+ import { FSUtil } from "../fs-util"
7
+ import { Git } from "../git"
8
+ import { makeLocationNode } from "../effect/app-node"
9
+ import { Project } from "../project"
10
+ import { ProjectDirectories } from "./directories"
11
+ import { makeGitWorktreeStrategy } from "./copy-strategies"
12
+ import { Slug } from "../util/slug"
13
+ import { EventV2 } from "../event"
14
+ import { Database } from "../database/database"
15
+ import { Location } from "../location"
16
+ import { Event } from "@neurocode-ai/schema/project-directories"
17
+ import { ProjectCopy } from "@neurocode-ai/schema/project-copy"
18
+
19
+ export const StrategyID = ProjectCopy.StrategyID
20
+ export type StrategyID = typeof StrategyID.Type
21
+
22
+ export const CreateInput = ProjectCopy.CreateInput
23
+ export type CreateInput = typeof CreateInput.Type
24
+
25
+ export const RemoveInput = ProjectCopy.RemoveInput
26
+ export type RemoveInput = typeof RemoveInput.Type
27
+
28
+ export const RefreshInput = Schema.Struct({
29
+ projectID: Project.ID,
30
+ }).annotate({ identifier: "ProjectCopy.RefreshInput" })
31
+ export type RefreshInput = typeof RefreshInput.Type
32
+
33
+ export const RefreshResult = Schema.Struct({
34
+ updated: Schema.Array(AbsolutePath),
35
+ removed: Schema.Array(AbsolutePath),
36
+ }).annotate({ identifier: "ProjectCopy.RefreshResult" })
37
+ export type RefreshResult = typeof RefreshResult.Type
38
+
39
+ export const Copy = ProjectCopy.Copy
40
+ export type Copy = typeof Copy.Type
41
+
42
+ export const ListEntry = Schema.Struct({
43
+ directory: AbsolutePath,
44
+ type: Schema.Literals(["root", "copy"]),
45
+ }).annotate({ identifier: "ProjectCopy.ListEntry" })
46
+ export type ListEntry = typeof ListEntry.Type
47
+
48
+ export class SourceDirectoryNotFoundError extends Schema.TaggedErrorClass<SourceDirectoryNotFoundError>()(
49
+ "ProjectCopy.SourceDirectoryNotFoundError",
50
+ { directory: AbsolutePath },
51
+ ) {}
52
+
53
+ export class DestinationExistsError extends Schema.TaggedErrorClass<DestinationExistsError>()(
54
+ "ProjectCopy.DestinationExistsError",
55
+ { directory: AbsolutePath },
56
+ ) {}
57
+
58
+ export class DirectoryUnavailableError extends Schema.TaggedErrorClass<DirectoryUnavailableError>()(
59
+ "ProjectCopy.DirectoryUnavailableError",
60
+ { directory: AbsolutePath },
61
+ ) {}
62
+
63
+ export class InvalidDirectoryError extends Schema.TaggedErrorClass<InvalidDirectoryError>()(
64
+ "ProjectCopy.InvalidDirectoryError",
65
+ { directory: AbsolutePath },
66
+ ) {}
67
+
68
+ export class StrategyUnavailableError extends Schema.TaggedErrorClass<StrategyUnavailableError>()(
69
+ "ProjectCopy.StrategyUnavailableError",
70
+ { strategy: StrategyID },
71
+ ) {}
72
+
73
+ export class DuplicateStrategyError extends Schema.TaggedErrorClass<DuplicateStrategyError>()(
74
+ "ProjectCopy.DuplicateStrategyError",
75
+ { strategy: StrategyID },
76
+ ) {}
77
+
78
+ export type Error =
79
+ | SourceDirectoryNotFoundError
80
+ | DestinationExistsError
81
+ | DirectoryUnavailableError
82
+ | InvalidDirectoryError
83
+ | StrategyUnavailableError
84
+ | Git.WorktreeError
85
+
86
+ export interface Strategy {
87
+ readonly id: StrategyID
88
+ readonly create: (input: {
89
+ sourceDirectory: AbsolutePath
90
+ directory: AbsolutePath
91
+ }) => Effect.Effect<Copy, Git.WorktreeError | DirectoryUnavailableError>
92
+ readonly remove: (input: {
93
+ directory: AbsolutePath
94
+ force: boolean
95
+ }) => Effect.Effect<void, Git.WorktreeError | DirectoryUnavailableError>
96
+ readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError>
97
+ }
98
+
99
+ export { Event }
100
+
101
+ export interface Interface {
102
+ readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError>
103
+ readonly create: (input: CreateInput) => Effect.Effect<Copy, Error>
104
+ readonly remove: (input: RemoveInput) => Effect.Effect<void, Error>
105
+ readonly refresh: (input: RefreshInput) => Effect.Effect<RefreshResult, Error>
106
+ }
107
+
108
+ export class Service extends Context.Service<Service, Interface>()("@neurocode/ProjectCopy") {}
109
+
110
+ export const refreshAfterBoot = Effect.gen(function* () {
111
+ const location = yield* Location.Service
112
+ const copies = yield* Service
113
+ yield* Effect.gen(function* () {
114
+ yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id })
115
+ const result = yield* copies.refresh({ projectID: location.project.id })
116
+ yield* Effect.logInfo("project copy refresh done", {
117
+ projectID: location.project.id,
118
+ updated: result.updated,
119
+ removed: result.removed,
120
+ })
121
+ }).pipe(
122
+ Effect.catchCause((cause) => Effect.logWarning("project copy refresh failed", { cause })),
123
+ Effect.forkScoped,
124
+ Effect.asVoid,
125
+ )
126
+ })
127
+
128
+ const layer = Layer.effect(
129
+ Service,
130
+ Effect.gen(function* () {
131
+ const fs = yield* FSUtil.Service
132
+ const git = yield* Git.Service
133
+ const directories = yield* ProjectDirectories.Service
134
+ const db = (yield* Database.Service).db
135
+ const events = yield* EventV2.Service
136
+
137
+ const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) {
138
+ if (update) yield* events.publish(Event.Updated, { projectID })
139
+ })
140
+
141
+ const canonical = Effect.fnUntraced(function* (input: AbsolutePath) {
142
+ const resolved = AbsolutePath.make(yield* fs.resolve(input))
143
+ if (!(yield* fs.isDir(resolved))) return yield* new DirectoryUnavailableError({ directory: input })
144
+ return resolved
145
+ })
146
+
147
+ const registry = new Map<StrategyID, Strategy>()
148
+
149
+ const register = Effect.fn("ProjectCopy.register")(function* (strategy: Strategy) {
150
+ if (registry.has(strategy.id)) return yield* new DuplicateStrategyError({ strategy: strategy.id })
151
+ registry.set(strategy.id, strategy)
152
+ })
153
+
154
+ // Register default strategies
155
+ yield* register(makeGitWorktreeStrategy({ git, canonical })).pipe(Effect.orDie)
156
+
157
+ const strategies = () => Array.from(registry.values())
158
+
159
+ const source = Effect.fnUntraced(function* (input: AbsolutePath, projectID: Project.ID) {
160
+ const sourceDirectory = yield* canonical(input)
161
+ if (!(yield* directories.contains({ projectID, directory: sourceDirectory })))
162
+ return yield* new SourceDirectoryNotFoundError({ directory: sourceDirectory })
163
+ return sourceDirectory
164
+ })
165
+
166
+ const getStrategy = Effect.fnUntraced(function* (id: StrategyID) {
167
+ const found = registry.get(id)
168
+ if (!found) return yield* new StrategyUnavailableError({ strategy: id })
169
+ return found
170
+ })
171
+
172
+ const create = Effect.fn("ProjectCopy.create")(function* (input: CreateInput) {
173
+ const selected = yield* getStrategy(input.strategy)
174
+ const sourceDirectory = yield* source(input.sourceDirectory, input.projectID)
175
+ yield* fs.makeDirectory(input.directory, { recursive: true }).pipe(Effect.orDie)
176
+ const name = input.name ?? Slug.create()
177
+ let suffix = 1
178
+ let copyDirectory = AbsolutePath.make(path.join(input.directory, name))
179
+ while (yield* fs.existsSafe(copyDirectory)) {
180
+ suffix++
181
+ if (suffix > 10) return yield* new DestinationExistsError({ directory: copyDirectory })
182
+ copyDirectory = AbsolutePath.make(path.join(input.directory, `${name}-${suffix}`))
183
+ }
184
+
185
+ const result = yield* selected.create({
186
+ directory: copyDirectory,
187
+ sourceDirectory,
188
+ })
189
+ yield* changed(
190
+ input.projectID,
191
+ yield* directories.create({
192
+ projectID: input.projectID,
193
+ directory: result.directory,
194
+ strategy: input.strategy,
195
+ behavior: "replace",
196
+ }),
197
+ )
198
+ return result
199
+ })
200
+
201
+ const remove = Effect.fn("ProjectCopy.remove")(function* (input: RemoveInput) {
202
+ const copyDirectory = yield* canonical(input.directory)
203
+ const stored = yield* directories.get({ projectID: input.projectID, directory: copyDirectory })
204
+ if (!stored?.strategy) return yield* new InvalidDirectoryError({ directory: copyDirectory })
205
+ yield* (yield* getStrategy(StrategyID.make(stored.strategy))).remove({
206
+ directory: copyDirectory,
207
+ force: input.force,
208
+ })
209
+ yield* changed(
210
+ input.projectID,
211
+ yield* directories.remove({ projectID: input.projectID, directory: copyDirectory }),
212
+ )
213
+ })
214
+
215
+ const refresh = Effect.fn("ProjectCopy.refresh")(function* (input: RefreshInput) {
216
+ const stored = yield* directories.list(input.projectID)
217
+ const checked = yield* Effect.forEach(
218
+ stored,
219
+ (item) => fs.isDir(item.directory).pipe(Effect.map((exists) => ({ ...item, exists }))),
220
+ { concurrency: "unbounded" },
221
+ )
222
+ const sourceDirectories = checked
223
+ .filter((item) => item.strategy === undefined && item.exists)
224
+ .map((item) => item.directory)
225
+ const discovered = yield* Effect.forEach(
226
+ sourceDirectories,
227
+ (sourceDirectory) =>
228
+ Effect.forEach(strategies(), (strategy) =>
229
+ strategy.list(sourceDirectory).pipe(
230
+ Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed([])),
231
+ Effect.map((items) =>
232
+ items.map((item) => ({
233
+ directory: item.directory,
234
+ strategy: item.type === "copy" ? strategy.id : undefined,
235
+ })),
236
+ ),
237
+ ),
238
+ ),
239
+ { concurrency: "unbounded" },
240
+ ).pipe(
241
+ Effect.map((sets) => new Map(sets.flat(2).map((item) => [item.directory, item] as const)).values().toArray()),
242
+ )
243
+ const removed = checked.filter((item) => !item.exists).map((item) => item.directory)
244
+ const result = yield* db
245
+ .transaction((tx) =>
246
+ Effect.all({
247
+ updated: Effect.forEach(discovered, (item) =>
248
+ directories.create(
249
+ {
250
+ projectID: input.projectID,
251
+ directory: item.directory,
252
+ strategy: item.strategy,
253
+ behavior: "replace",
254
+ },
255
+ tx,
256
+ ),
257
+ ),
258
+ removed: Effect.forEach(removed, (directory) =>
259
+ directories.remove({ projectID: input.projectID, directory }, tx),
260
+ ),
261
+ }),
262
+ )
263
+ .pipe(Effect.orDie)
264
+ const changes = {
265
+ updated: discovered.filter((_, index) => result.updated[index]).map((item) => item.directory),
266
+ removed: removed.filter((_, index) => result.removed[index]),
267
+ }
268
+ yield* changed(input.projectID, changes.updated.length > 0 || changes.removed.length > 0)
269
+ return changes
270
+ })
271
+
272
+ return Service.of({
273
+ register,
274
+ create,
275
+ remove,
276
+ refresh,
277
+ })
278
+ }),
279
+ )
280
+
281
+ export const locationLayer = layer
282
+ export const node = makeLocationNode({
283
+ service: Service,
284
+ layer: layer,
285
+ deps: [FSUtil.node, Git.node, ProjectDirectories.node, EventV2.node, Database.node],
286
+ })
287
+
288
+ export const refreshNode = makeLocationNode({
289
+ name: "project-copy-refresh",
290
+ layer: Layer.effectDiscard(refreshAfterBoot),
291
+ deps: [node, Location.node],
292
+ })