@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
@@ -0,0 +1,520 @@
1
+ export * as Integration from "./integration"
2
+
3
+ import { makeLocationNode } from "./effect/app-node"
4
+ import {
5
+ Cause,
6
+ Clock,
7
+ Context,
8
+ Duration,
9
+ Effect,
10
+ Exit,
11
+ Layer,
12
+ Schedule,
13
+ Schema,
14
+ Scope,
15
+ SynchronizedRef,
16
+ Types,
17
+ } from "effect"
18
+ import { Integration } from "@neurocode-ai/schema/integration"
19
+ import { Credential } from "./credential"
20
+ import { State } from "./state"
21
+ import { EventV2 } from "./event"
22
+ import { IntegrationConnection } from "./integration/connection"
23
+
24
+ export const ID = Integration.ID
25
+ export type ID = Integration.ID
26
+
27
+ export const MethodID = Integration.MethodID
28
+ export type MethodID = Integration.MethodID
29
+
30
+ export const AttemptID = Integration.AttemptID
31
+ export type AttemptID = typeof AttemptID.Type
32
+
33
+ export const When = Integration.When
34
+ export type When = Integration.When
35
+
36
+ export const TextPrompt = Integration.TextPrompt
37
+ export type TextPrompt = Integration.TextPrompt
38
+
39
+ export const SelectPrompt = Integration.SelectPrompt
40
+ export type SelectPrompt = Integration.SelectPrompt
41
+
42
+ export const Prompt = Integration.Prompt
43
+ export type Prompt = Integration.Prompt
44
+
45
+ export const OAuthMethod = Integration.OAuthMethod
46
+ export type OAuthMethod = Integration.OAuthMethod
47
+
48
+ export const KeyMethod = Integration.KeyMethod
49
+ export type KeyMethod = Integration.KeyMethod
50
+
51
+ export const EnvMethod = Integration.EnvMethod
52
+ export type EnvMethod = Integration.EnvMethod
53
+
54
+ export const Method = Integration.Method
55
+ export type Method = Integration.Method
56
+
57
+ export const Info = Integration.Info
58
+ export type Info = Integration.Info
59
+
60
+ export const Inputs = Integration.Inputs
61
+ export type Inputs = Integration.Inputs
62
+
63
+ export type OAuthAuthorization = {
64
+ readonly url: string
65
+ readonly instructions: string
66
+ } & (
67
+ | {
68
+ readonly mode: "auto"
69
+ readonly callback: Effect.Effect<Credential.OAuth, unknown>
70
+ }
71
+ | {
72
+ readonly mode: "code"
73
+ readonly callback: (code: string) => Effect.Effect<Credential.OAuth, unknown>
74
+ }
75
+ )
76
+
77
+ export interface OAuthImplementation {
78
+ readonly integrationID: ID
79
+ readonly method: OAuthMethod
80
+ readonly authorize: (inputs: Inputs) => Effect.Effect<OAuthAuthorization, unknown, Scope.Scope>
81
+ readonly refresh?: (credential: Credential.OAuth) => Effect.Effect<Credential.OAuth, unknown>
82
+ readonly label?: (credential: Credential.OAuth) => string | undefined
83
+ }
84
+
85
+ export interface KeyImplementation {
86
+ readonly integrationID: ID
87
+ readonly method: KeyMethod
88
+ }
89
+
90
+ export interface EnvImplementation {
91
+ readonly integrationID: ID
92
+ readonly method: EnvMethod
93
+ }
94
+
95
+ export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation
96
+
97
+ export const Attempt = Integration.Attempt
98
+ export type Attempt = Integration.Attempt
99
+
100
+ export const AttemptStatus = Integration.AttemptStatus
101
+ export type AttemptStatus = typeof AttemptStatus.Type
102
+
103
+ export class CodeRequiredError extends Schema.TaggedErrorClass<CodeRequiredError>()("Integration.CodeRequired", {
104
+ attemptID: AttemptID,
105
+ }) {}
106
+
107
+ export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationError>()("Integration.Authorization", {
108
+ cause: Schema.Defect(),
109
+ }) {}
110
+
111
+ export type Error = CodeRequiredError | AuthorizationError
112
+
113
+ export const Event = Integration.Event
114
+
115
+ export const Ref = Integration.Ref
116
+ export type Ref = Integration.Ref
117
+
118
+ type Entry = {
119
+ ref: Types.DeepMutable<Ref>
120
+ methods: Types.DeepMutable<Method>[]
121
+ implementations: Map<MethodID, Types.DeepMutable<OAuthImplementation>>
122
+ }
123
+
124
+ type Data = {
125
+ integrations: Map<ID, Entry>
126
+ }
127
+
128
+ export type Draft = {
129
+ list: () => readonly Ref[]
130
+ get: (id: ID) => Ref | undefined
131
+ update: (id: ID, update: (integration: Types.DeepMutable<Ref>) => void) => void
132
+ remove: (id: ID) => void
133
+ method: {
134
+ list: (integrationID: ID) => readonly Method[]
135
+ update: (implementation: Implementation) => void
136
+ remove: (integrationID: ID, method: Method) => void
137
+ }
138
+ }
139
+
140
+ export interface Interface extends State.Transformable<Draft> {
141
+ /** Registers a scoped transform over the integration registry. */
142
+ /** Returns one integration with its methods and current connections. */
143
+ readonly get: (id: ID) => Effect.Effect<Info | undefined>
144
+ /** Returns all integrations with their methods and current connections. */
145
+ readonly list: () => Effect.Effect<Info[]>
146
+ readonly connection: {
147
+ /** Returns the active connection for one integration. */
148
+ readonly active: (id: ID) => Effect.Effect<IntegrationConnection.Info | undefined>
149
+ /** Resolves a connection into usable credential material. */
150
+ readonly resolve: (
151
+ connection: IntegrationConnection.Info,
152
+ ) => Effect.Effect<Credential.Value | undefined, AuthorizationError>
153
+ /** Runs a key method and stores the resulting credential. */
154
+ readonly key: (input: {
155
+ /** Integration receiving the credential. */
156
+ readonly integrationID: ID
157
+ /** Secret entered by the user. */
158
+ readonly key: string
159
+ /** User-facing label for the stored credential. */
160
+ readonly label?: string
161
+ }) => Effect.Effect<void, AuthorizationError>
162
+ /** Starts a stateful OAuth attempt. */
163
+ readonly oauth: (input: {
164
+ /** Integration being authenticated. */
165
+ readonly integrationID: ID
166
+ /** OAuth method selected by the caller. */
167
+ readonly methodID: MethodID
168
+ /** Answers to the method's optional prompts. */
169
+ readonly inputs: Inputs
170
+ /** User-facing label for the credential created on completion. */
171
+ readonly label?: string
172
+ }) => Effect.Effect<Attempt, AuthorizationError>
173
+ /** Updates a stored credential exposed as a connection. */
174
+ readonly update: (
175
+ credentialID: Credential.ID,
176
+ updates: Partial<Pick<Credential.Info, "label">>,
177
+ ) => Effect.Effect<void>
178
+ /** Removes a stored credential connection. */
179
+ readonly remove: (credentialID: Credential.ID) => Effect.Effect<void>
180
+ }
181
+ readonly attempt: {
182
+ /** Returns the current state of an OAuth attempt. */
183
+ readonly status: (attemptID: AttemptID) => Effect.Effect<AttemptStatus>
184
+ /** Completes the attempt and stores its credential. */
185
+ readonly complete: (input: {
186
+ /** Opaque handle returned by `oauth`. */
187
+ readonly attemptID: AttemptID
188
+ /** Authorization code required by attempts in code mode. */
189
+ readonly code?: string
190
+ }) => Effect.Effect<void, CodeRequiredError | AuthorizationError>
191
+ /** Cancels an attempt and releases its resources. */
192
+ readonly cancel: (attemptID: AttemptID) => Effect.Effect<void>
193
+ }
194
+ }
195
+
196
+ export class Service extends Context.Service<Service, Interface>()("@neurocode/v2/Integration") {}
197
+
198
+ const attemptLifetime = Duration.toMillis(Duration.minutes(10))
199
+ const terminalRetention = Duration.toMillis(Duration.minutes(1))
200
+ const scrubInterval = Duration.seconds(30)
201
+
202
+ type AttemptTime = { created: number; expires: number }
203
+ type PendingAttempt = {
204
+ status: "pending"
205
+ completing: boolean
206
+ authorization: OAuthAuthorization
207
+ integrationID: ID
208
+ methodID: MethodID
209
+ label?: string
210
+ scope: Scope.Closeable
211
+ time: AttemptTime
212
+ }
213
+ type TerminalAttempt = {
214
+ status: "complete" | "failed" | "expired"
215
+ message?: string
216
+ removeAt: number
217
+ time: AttemptTime
218
+ }
219
+ type AttemptEntry = PendingAttempt | TerminalAttempt
220
+
221
+ export const locationLayer = Layer.effect(
222
+ Service,
223
+ Effect.gen(function* () {
224
+ const credentials = yield* Credential.Service
225
+ const events = yield* EventV2.Service
226
+ const scope = yield* Scope.Scope
227
+ const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
228
+ const state = State.create<Data, Draft>({
229
+ initial: () => ({ integrations: new Map<ID, Entry>() }),
230
+ draft: (draft) => ({
231
+ list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[],
232
+ get: (id) => draft.integrations.get(id)?.ref as Ref | undefined,
233
+ update: (id, update) => {
234
+ const current = draft.integrations.get(id) ?? {
235
+ ref: { id, name: id },
236
+ methods: [],
237
+ implementations: new Map(),
238
+ }
239
+ if (!draft.integrations.has(id)) draft.integrations.set(id, current)
240
+ update(current.ref)
241
+ current.ref.id = id
242
+ },
243
+ remove: (id) => draft.integrations.delete(id),
244
+ method: {
245
+ list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [],
246
+ update: (implementation) => {
247
+ const current = draft.integrations.get(implementation.integrationID) ?? {
248
+ ref: {
249
+ id: implementation.integrationID,
250
+ name: implementation.integrationID,
251
+ },
252
+ methods: [],
253
+ implementations: new Map<MethodID, Types.DeepMutable<OAuthImplementation>>(),
254
+ }
255
+ if (!draft.integrations.has(implementation.integrationID)) {
256
+ draft.integrations.set(implementation.integrationID, current)
257
+ }
258
+ const index = current.methods.findIndex((method) => {
259
+ if (method.type !== implementation.method.type) return false
260
+ if (method.type !== "oauth" || implementation.method.type !== "oauth") return true
261
+ return method.id === implementation.method.id
262
+ })
263
+ if (index === -1) current.methods.push(implementation.method as Types.DeepMutable<Method>)
264
+ else current.methods[index] = implementation.method as Types.DeepMutable<Method>
265
+ if (implementation.method.type === "oauth") {
266
+ current.implementations.set(
267
+ implementation.method.id,
268
+ implementation as Types.DeepMutable<OAuthImplementation>,
269
+ )
270
+ }
271
+ },
272
+ remove: (integrationID, method) => {
273
+ const current = draft.integrations.get(integrationID)
274
+ if (!current) return
275
+ const index = current.methods.findIndex((candidate) => {
276
+ if (candidate.type !== method.type) return false
277
+ if (candidate.type !== "oauth" || method.type !== "oauth") return true
278
+ return candidate.id === method.id
279
+ })
280
+ if (index !== -1) current.methods.splice(index, 1)
281
+ if (method.type === "oauth") current.implementations.delete(method.id)
282
+ },
283
+ },
284
+ }),
285
+ finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
286
+ })
287
+
288
+ const resolveConnections = (entry: Entry | undefined, saved: readonly Credential.Info[]) => {
289
+ const credentials = saved
290
+ .map((credential) => ({
291
+ type: "credential" as const,
292
+ id: credential.id,
293
+ label: credential.label,
294
+ }))
295
+ .toReversed()
296
+ const env = (entry?.methods ?? [])
297
+ .filter((method) => method.type === "env")
298
+ .flatMap((method) => method.names.filter((name) => process.env[name]))
299
+ .map((name) => ({ type: "env" as const, name }))
300
+ return [...credentials, ...env]
301
+ }
302
+
303
+ const project = (entry: Entry, connections: IntegrationConnection.Info[]) =>
304
+ new Info({
305
+ id: entry.ref.id,
306
+ name: entry.ref.name,
307
+ methods: entry.methods,
308
+ connections,
309
+ })
310
+
311
+ const authorize = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
312
+ effect.pipe(Effect.mapError((cause) => new AuthorizationError({ cause })))
313
+
314
+ const close = (attemptScope: Scope.Closeable) =>
315
+ Scope.close(attemptScope, Exit.void).pipe(Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
316
+
317
+ const message = (cause: Cause.Cause<unknown>) => {
318
+ const error = Cause.squash(cause)
319
+ return error instanceof Error ? error.message : String(error)
320
+ }
321
+
322
+ const settle = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit<Credential.OAuth, unknown>) {
323
+ const now = yield* Clock.currentTimeMillis
324
+ const result = yield* SynchronizedRef.modify(attempts, (current) => {
325
+ const attempt = current.get(attemptID)
326
+ if (!attempt || attempt.status !== "pending") return [undefined, current]
327
+ const terminal: TerminalAttempt = Exit.isSuccess(exit)
328
+ ? { status: "complete", time: attempt.time, removeAt: now + terminalRetention }
329
+ : { status: "failed", message: message(exit.cause), time: attempt.time, removeAt: now + terminalRetention }
330
+ return [attempt, new Map(current).set(attemptID, terminal)]
331
+ })
332
+ if (!result) return
333
+ if (Exit.isSuccess(exit)) {
334
+ const implementation = state.get().integrations.get(result.integrationID)?.implementations.get(result.methodID)
335
+ yield* credentials.create({
336
+ integrationID: result.integrationID,
337
+ label: result.label ?? implementation?.label?.(exit.value),
338
+ value: exit.value,
339
+ })
340
+ yield* events.publish(Event.ConnectionUpdated, { integrationID: result.integrationID })
341
+ yield* events.publish(Event.Updated, {})
342
+ }
343
+ yield* close(result.scope)
344
+ })
345
+
346
+ const scrub = Effect.fnUntraced(function* () {
347
+ const now = yield* Clock.currentTimeMillis
348
+ const expired = yield* SynchronizedRef.modify(attempts, (current) => {
349
+ const next = new Map(current)
350
+ const scopes: Scope.Closeable[] = []
351
+ for (const [id, attempt] of current) {
352
+ if (attempt.status === "pending" && attempt.time.expires <= now) {
353
+ scopes.push(attempt.scope)
354
+ next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention })
355
+ continue
356
+ }
357
+ if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id)
358
+ }
359
+ return [scopes, next]
360
+ })
361
+ yield* Effect.forEach(expired, close, { discard: true })
362
+ })
363
+
364
+ yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope))
365
+
366
+ return Service.of({
367
+ transform: state.transform,
368
+ reload: state.reload,
369
+ get: Effect.fn("Integration.get")(function* (id) {
370
+ const entry = state.get().integrations.get(id)
371
+ if (!entry) return undefined
372
+ return project(entry, resolveConnections(entry, yield* credentials.list(id)))
373
+ }),
374
+ list: Effect.fn("Integration.list")(function* () {
375
+ const saved = Map.groupBy(yield* credentials.all(), (credential) => credential.integrationID)
376
+ return Array.from(state.get().integrations.values(), (entry) =>
377
+ project(entry, resolveConnections(entry, saved.get(entry.ref.id) ?? [])),
378
+ ).toSorted((a, b) => a.name.localeCompare(b.name))
379
+ }),
380
+ connection: {
381
+ active: Effect.fn("Integration.connection.active")(function* (id) {
382
+ const entry = state.get().integrations.get(id)
383
+ return resolveConnections(entry, yield* credentials.list(id))[0]
384
+ }),
385
+ resolve: Effect.fn("Integration.connection.resolve")(function* (connection) {
386
+ if (connection.type === "env") {
387
+ const key = process.env[connection.name]
388
+ return key ? Credential.Key.make({ type: "key", key }) : undefined
389
+ }
390
+ const credential = yield* credentials.get(connection.id)
391
+ if (!credential) return undefined
392
+ if (credential.value.type === "key") return credential.value
393
+ const implementation = state
394
+ .get()
395
+ .integrations.get(credential.integrationID)
396
+ ?.implementations.get(credential.value.methodID)
397
+ if (!implementation?.refresh) return credential.value
398
+ const now = yield* Clock.currentTimeMillis
399
+ if (credential.value.expires > now + Duration.toMillis(Duration.minutes(5))) return credential.value
400
+ const value = yield* authorize(implementation.refresh(credential.value))
401
+ yield* credentials.update(credential.id, { value })
402
+ return value
403
+ }),
404
+ key: Effect.fn("Integration.connection.key")(function* (input) {
405
+ const method = state
406
+ .get()
407
+ .integrations.get(input.integrationID)
408
+ ?.methods.some((method) => method.type === "key")
409
+ if (!method) return yield* Effect.die(`Key method not found: ${input.integrationID}`)
410
+ yield* credentials.create({
411
+ integrationID: input.integrationID,
412
+ label: input.label,
413
+ value: Credential.Key.make({ type: "key", key: input.key }),
414
+ })
415
+ yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID })
416
+ yield* events.publish(Event.Updated, {})
417
+ }),
418
+ oauth: Effect.fn("Integration.connection.oauth")(function* (input) {
419
+ const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
420
+ if (!method) {
421
+ return yield* Effect.die(`OAuth method not found: ${input.integrationID}/${input.methodID}`)
422
+ }
423
+ const attemptScope = yield* Scope.fork(scope)
424
+ const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
425
+ Scope.provide(attemptScope),
426
+ Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)),
427
+ )
428
+ const id = AttemptID.create()
429
+ const created = yield* Clock.currentTimeMillis
430
+ const time = { created, expires: created + attemptLifetime }
431
+ yield* SynchronizedRef.update(attempts, (current) =>
432
+ new Map(current).set(id, {
433
+ status: "pending",
434
+ completing: authorization.mode === "auto",
435
+ authorization,
436
+ integrationID: input.integrationID,
437
+ methodID: input.methodID,
438
+ label: input.label,
439
+ scope: attemptScope,
440
+ time,
441
+ }),
442
+ )
443
+ if (authorization.mode === "auto") {
444
+ yield* authorization.callback.pipe(
445
+ Effect.exit,
446
+ Effect.flatMap((exit) => settle(id, exit)),
447
+ Effect.forkIn(attemptScope, { startImmediately: true }),
448
+ )
449
+ }
450
+ return new Attempt({
451
+ attemptID: id,
452
+ url: authorization.url,
453
+ instructions: authorization.instructions,
454
+ mode: authorization.mode,
455
+ time,
456
+ })
457
+ }),
458
+ update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) {
459
+ const credential = yield* credentials.get(credentialID)
460
+ yield* credentials.update(credentialID, updates)
461
+ if (credential) {
462
+ yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID })
463
+ }
464
+ yield* events.publish(Event.Updated, {})
465
+ }),
466
+ remove: Effect.fn("Integration.connection.remove")(function* (credentialID) {
467
+ const credential = yield* credentials.get(credentialID)
468
+ yield* credentials.remove(credentialID)
469
+ if (credential) {
470
+ yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID })
471
+ }
472
+ yield* events.publish(Event.Updated, {})
473
+ }),
474
+ },
475
+ attempt: {
476
+ status: Effect.fn("Integration.attempt.status")(function* (attemptID) {
477
+ const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID)
478
+ if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`)
479
+ if (attempt.status === "failed") {
480
+ return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
481
+ }
482
+ return { status: attempt.status, time: attempt.time }
483
+ }),
484
+ complete: Effect.fn("Integration.attempt.complete")(function* (input) {
485
+ const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
486
+ const match = current.get(input.attemptID)
487
+ if (!match || match.status !== "pending" || match.completing) return [match, current]
488
+ if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
489
+ return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
490
+ })
491
+ if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`)
492
+ if (attempt.status !== "pending") return
493
+ if (attempt.authorization.mode === "code" && input.code === undefined) {
494
+ return yield* new CodeRequiredError({ attemptID: input.attemptID })
495
+ }
496
+ if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`)
497
+ const callback =
498
+ attempt.authorization.mode === "auto"
499
+ ? attempt.authorization.callback
500
+ : attempt.authorization.callback(input.code as string)
501
+ const exit = yield* authorize(callback).pipe(Effect.exit)
502
+ yield* settle(input.attemptID, exit)
503
+ if (Exit.isFailure(exit)) return yield* exit
504
+ }),
505
+ cancel: Effect.fn("Integration.attempt.cancel")(function* (attemptID) {
506
+ const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
507
+ const match = current.get(attemptID)
508
+ if (!match || match.status !== "pending") return [undefined, current]
509
+ const next = new Map(current)
510
+ next.delete(attemptID)
511
+ return [match, next]
512
+ })
513
+ if (attempt) yield* Scope.close(attempt.scope, Exit.void)
514
+ }),
515
+ },
516
+ })
517
+ }),
518
+ )
519
+
520
+ export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [Credential.node, EventV2.node] })
@@ -0,0 +1,162 @@
1
+ export * as LocationMutation from "./location-mutation"
2
+
3
+ import { makeLocationNode } from "./effect/app-node"
4
+ import path from "path"
5
+ import { Context, Effect, Layer, Schema } from "effect"
6
+ import { FSUtil } from "./fs-util"
7
+ import { Location } from "./location"
8
+
9
+ export const Kind = Schema.Literals(["file", "directory"])
10
+ export type Kind = typeof Kind.Type
11
+
12
+ /**
13
+ * Mutation paths do not accept project references. Relative paths must stay
14
+ * inside the active Location. Absolute paths outside it require separate
15
+ * `external_directory` approval.
16
+ */
17
+ export const ResolveInput = Schema.Struct({
18
+ path: Schema.String,
19
+ /** Selects the external approval boundary; it does not validate the target type. */
20
+ kind: Kind.pipe(Schema.optional),
21
+ })
22
+ export type ResolveInput = typeof ResolveInput.Type
23
+
24
+ export class PathError extends Schema.TaggedErrorClass<PathError>()("LocationMutation.PathError", {
25
+ path: Schema.String,
26
+ reason: Schema.Literals(["relative_escape", "location_escape", "non_directory_ancestor"]),
27
+ }) {}
28
+
29
+ export interface ExternalDirectoryAuthorization {
30
+ readonly action: "external_directory"
31
+ /** Canonical existing directory used as the external approval boundary. */
32
+ readonly directory: string
33
+ /** `external_directory` permission resource. */
34
+ readonly resource: string
35
+ readonly save: string
36
+ }
37
+
38
+ export const externalDirectoryPermission = (input: ExternalDirectoryAuthorization) => ({
39
+ action: input.action,
40
+ resources: [input.resource],
41
+ save: [input.save],
42
+ })
43
+
44
+ export interface Target {
45
+ /** Canonical existing path, or missing path below a canonical directory. */
46
+ readonly canonical: string
47
+ /** Permission resource: Location-relative for internal paths, canonical for external paths. */
48
+ readonly resource: string
49
+ readonly externalDirectory?: ExternalDirectoryAuthorization
50
+ }
51
+
52
+ export interface Interface {
53
+ /**
54
+ * Resolve a path and derive its permission resources. Relative paths must
55
+ * stay inside the Location. Absolute paths outside it require separate
56
+ * `external_directory` approval. This does not approve the mutation.
57
+ */
58
+ readonly resolve: (input: ResolveInput) => Effect.Effect<Target, PathError | FSUtil.Error>
59
+ }
60
+
61
+ export class Service extends Context.Service<Service, Interface>()("@neurocode/v2/LocationMutation") {}
62
+
63
+ interface ResolvedPath {
64
+ readonly canonical: string
65
+ readonly type?:
66
+ | "File"
67
+ | "Directory"
68
+ | "SymbolicLink"
69
+ | "BlockDevice"
70
+ | "CharacterDevice"
71
+ | "FIFO"
72
+ | "Socket"
73
+ | "Unknown"
74
+ readonly directory: string
75
+ }
76
+
77
+ const slash = (value: string) => value.replaceAll("\\", "/")
78
+
79
+ const layer = Layer.effect(
80
+ Service,
81
+ Effect.gen(function* () {
82
+ const fs = yield* FSUtil.Service
83
+ const location = yield* Location.Service
84
+ const locationRoot = yield* fs.realPath(location.directory)
85
+
86
+ function notFound<A>(effect: Effect.Effect<A, FSUtil.Error>) {
87
+ return effect.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)))
88
+ }
89
+
90
+ const resolvePath = Effect.fnUntraced(function* (absolute: string) {
91
+ const existing = yield* notFound(fs.realPath(absolute))
92
+ if (existing !== undefined) {
93
+ const info = yield* fs.stat(existing)
94
+ return {
95
+ canonical: existing,
96
+ type: info.type,
97
+ directory: info.type === "Directory" ? existing : path.dirname(existing),
98
+ } satisfies ResolvedPath
99
+ }
100
+
101
+ let anchor = path.dirname(absolute)
102
+ while (true) {
103
+ const canonical = yield* notFound(fs.realPath(anchor))
104
+ if (canonical !== undefined) {
105
+ const info = yield* fs.stat(canonical)
106
+ if (info.type !== "Directory") {
107
+ return yield* new PathError({ path: absolute, reason: "non_directory_ancestor" })
108
+ }
109
+ return {
110
+ canonical: path.resolve(canonical, path.relative(anchor, absolute)),
111
+ directory: canonical,
112
+ } satisfies ResolvedPath
113
+ }
114
+ const parent = path.dirname(anchor)
115
+ if (parent === anchor) return yield* new PathError({ path: absolute, reason: "non_directory_ancestor" })
116
+ anchor = parent
117
+ }
118
+ })
119
+
120
+ const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) {
121
+ const relative = !path.isAbsolute(input.path)
122
+ const absolute = path.resolve(location.directory, input.path)
123
+ const lexicallyInternal = FSUtil.contains(location.directory, absolute)
124
+ if (relative && !lexicallyInternal) return yield* new PathError({ path: input.path, reason: "relative_escape" })
125
+
126
+ const resolved = yield* resolvePath(absolute)
127
+ if (lexicallyInternal && !FSUtil.contains(locationRoot, resolved.canonical)) {
128
+ return yield* new PathError({ path: input.path, reason: "location_escape" })
129
+ }
130
+
131
+ const external = !lexicallyInternal
132
+ const resource = external
133
+ ? slash(resolved.canonical)
134
+ : slash(path.relative(locationRoot, resolved.canonical) || ".")
135
+ const externalDirectory =
136
+ input.kind === "directory" && resolved.type === "Directory" ? resolved.canonical : resolved.directory
137
+ const externalResource = slash(path.join(externalDirectory, "*"))
138
+ return {
139
+ canonical: resolved.canonical,
140
+ resource,
141
+ externalDirectory: external
142
+ ? {
143
+ action: "external_directory",
144
+ directory: externalDirectory,
145
+ resource: externalResource,
146
+ save: externalResource,
147
+ }
148
+ : undefined,
149
+ } satisfies Target
150
+ })
151
+
152
+ return Service.of({ resolve })
153
+ }),
154
+ )
155
+
156
+ export const locationLayer = layer
157
+
158
+ export const node = makeLocationNode({
159
+ service: Service,
160
+ layer: layer.pipe(Layer.orDie),
161
+ deps: [FSUtil.node, Location.node],
162
+ })