@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.
- package/bin/neurocode +199 -0
- package/bunfig.toml +2 -0
- package/drizzle.config.ts +10 -0
- package/package.json +131 -0
- package/schema.json +2071 -0
- package/script/fix-node-pty.ts +28 -0
- package/script/migration.ts +196 -0
- package/src/account/sql.ts +39 -0
- package/src/account.ts +101 -0
- package/src/agent.ts +111 -0
- package/src/aisdk.ts +235 -0
- package/src/background-job.ts +365 -0
- package/src/catalog.ts +301 -0
- package/src/command.ts +64 -0
- package/src/config/agent.ts +25 -0
- package/src/config/attachments.ts +15 -0
- package/src/config/command.ts +12 -0
- package/src/config/compaction.ts +15 -0
- package/src/config/experimental.ts +18 -0
- package/src/config/formatter.ts +12 -0
- package/src/config/lsp.ts +18 -0
- package/src/config/markdown.ts +36 -0
- package/src/config/mcp.ts +48 -0
- package/src/config/plugin/agent.ts +179 -0
- package/src/config/plugin/command.ts +83 -0
- package/src/config/plugin/external.ts +91 -0
- package/src/config/plugin/provider.ts +113 -0
- package/src/config/plugin/reference.ts +70 -0
- package/src/config/plugin/skill.ts +50 -0
- package/src/config/plugin.ts +13 -0
- package/src/config/provider.ts +71 -0
- package/src/config/reference.ts +22 -0
- package/src/config/tool-output.ts +9 -0
- package/src/config/watcher.ts +7 -0
- package/src/config.ts +227 -0
- package/src/control-plane/move-session.ts +148 -0
- package/src/control-plane/workspace.sql.ts +20 -0
- package/src/credential/sql.ts +14 -0
- package/src/credential.ts +138 -0
- package/src/cross-spawn-spawner.ts +507 -0
- package/src/data-migration.sql.ts +6 -0
- package/src/database/database.ts +57 -0
- package/src/database/migration/20260127222353_familiar_lady_ursula.ts +107 -0
- package/src/database/migration/20260211171708_add_project_commands.ts +11 -0
- package/src/database/migration/20260213144116_wakeful_the_professor.ts +23 -0
- package/src/database/migration/20260225215848_workspace.ts +19 -0
- package/src/database/migration/20260227213759_add_session_workspace_id.ts +12 -0
- package/src/database/migration/20260228203230_blue_harpoon.ts +30 -0
- package/src/database/migration/20260303231226_add_workspace_fields.ts +15 -0
- package/src/database/migration/20260309230000_move_org_to_state.ts +15 -0
- package/src/database/migration/20260312043431_session_message_cursor.ts +16 -0
- package/src/database/migration/20260323234822_events.ts +26 -0
- package/src/database/migration/20260410174513_workspace-name.ts +29 -0
- package/src/database/migration/20260413175956_chief_energizer.ts +24 -0
- package/src/database/migration/20260423070820_add_icon_url_override.ts +14 -0
- package/src/database/migration/20260427172553_slow_nightmare.ts +30 -0
- package/src/database/migration/20260428004200_add_session_path.ts +11 -0
- package/src/database/migration/20260501142318_next_venus.ts +12 -0
- package/src/database/migration/20260504145000_add_sync_owner.ts +11 -0
- package/src/database/migration/20260507164347_add_workspace_time.ts +11 -0
- package/src/database/migration/20260510033149_session_usage.ts +56 -0
- package/src/database/migration/20260511000411_data_migration_state.ts +16 -0
- package/src/database/migration/20260511173437_session-metadata.ts +16 -0
- package/src/database/migration/20260601010001_normalize_storage_paths.ts +22 -0
- package/src/database/migration/20260601202201_amazing_prowler.ts +11 -0
- package/src/database/migration/20260602002951_lowly_union_jack.ts +24 -0
- package/src/database/migration/20260602182828_add_project_directories.ts +20 -0
- package/src/database/migration/20260603001617_session_message_projection_indexes.ts +19 -0
- package/src/database/migration/20260603040000_session_message_projection_order.ts +19 -0
- package/src/database/migration/20260603141458_session_input_inbox.ts +25 -0
- package/src/database/migration/20260603160727_jittery_ezekiel_stane.ts +20 -0
- package/src/database/migration/20260604172448_event_sourced_session_input.ts +47 -0
- package/src/database/migration/20260605003541_add_session_context_snapshot.ts +21 -0
- package/src/database/migration/20260605042240_add_context_epoch_agent.ts +11 -0
- package/src/database/migration/20260611035744_credential.ts +25 -0
- package/src/database/migration/20260611192811_lush_chimera.ts +25 -0
- package/src/database/migration/20260612174303_project_dir_strategy.ts +29 -0
- package/src/database/migration/20260622142730_simplify_session_context_epoch.ts +13 -0
- package/src/database/migration/20260622170816_reset_v2_session_state.ts +17 -0
- package/src/database/migration/20260622202450_simplify_session_input.ts +17 -0
- package/src/database/migration.gen.ts +44 -0
- package/src/database/migration.ts +81 -0
- package/src/database/path.ts +91 -0
- package/src/database/schema.gen.ts +274 -0
- package/src/database/schema.sql.ts +10 -0
- package/src/database/sqlite.bun.ts +183 -0
- package/src/database/sqlite.node.ts +178 -0
- package/src/database/sqlite.ts +8 -0
- package/src/effect/app-node-builder.ts +23 -0
- package/src/effect/app-node-platform.ts +18 -0
- package/src/effect/app-node.ts +14 -0
- package/src/effect/dfdf +1 -0
- package/src/effect/keyed-mutex.ts +45 -0
- package/src/effect/layer-node.ts +333 -0
- package/src/effect/memo-map.ts +3 -0
- package/src/effect/runtime.ts +21 -0
- package/src/effect/service-use.ts +43 -0
- package/src/event/sql.ts +25 -0
- package/src/event.ts +638 -0
- package/src/file-mutation.ts +207 -0
- package/src/file.ts +6 -0
- package/src/filesystem/fff.bun.ts +140 -0
- package/src/filesystem/fff.node.ts +138 -0
- package/src/filesystem/ignore.ts +67 -0
- package/src/filesystem/protected.ts +53 -0
- package/src/filesystem/search.ts +239 -0
- package/src/filesystem/watcher.ts +140 -0
- package/src/filesystem.ts +118 -0
- package/src/flag/flag.ts +78 -0
- package/src/fs-util.ts +274 -0
- package/src/git.ts +987 -0
- package/src/github-copilot/README.md +5 -0
- package/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
- package/src/github-copilot/chat/get-response-metadata.ts +15 -0
- package/src/github-copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
- package/src/github-copilot/chat/openai-compatible-api-types.ts +64 -0
- package/src/github-copilot/chat/openai-compatible-chat-language-model.ts +815 -0
- package/src/github-copilot/chat/openai-compatible-chat-options.ts +28 -0
- package/src/github-copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
- package/src/github-copilot/chat/openai-compatible-prepare-tools.ts +83 -0
- package/src/github-copilot/copilot-provider.ts +100 -0
- package/src/github-copilot/openai-compatible-error.ts +27 -0
- package/src/github-copilot/responses/convert-to-openai-responses-input.ts +335 -0
- package/src/github-copilot/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/github-copilot/responses/openai-config.ts +18 -0
- package/src/github-copilot/responses/openai-error.ts +22 -0
- package/src/github-copilot/responses/openai-responses-api-types.ts +214 -0
- package/src/github-copilot/responses/openai-responses-language-model.ts +1770 -0
- package/src/github-copilot/responses/openai-responses-prepare-tools.ts +173 -0
- package/src/github-copilot/responses/openai-responses-settings.ts +1 -0
- package/src/github-copilot/responses/tool/code-interpreter.ts +87 -0
- package/src/github-copilot/responses/tool/file-search.ts +127 -0
- package/src/github-copilot/responses/tool/image-generation.ts +114 -0
- package/src/github-copilot/responses/tool/local-shell.ts +64 -0
- package/src/github-copilot/responses/tool/web-search-preview.ts +103 -0
- package/src/github-copilot/responses/tool/web-search.ts +102 -0
- package/src/global.ts +87 -0
- package/src/id/id.ts +47 -0
- package/src/image/photon.ts +94 -0
- package/src/image.ts +81 -0
- package/src/installation/version.ts +8 -0
- package/src/instruction-context.ts +101 -0
- package/src/integration/connection.ts +12 -0
- package/src/integration.ts +520 -0
- package/src/location-mutation.ts +162 -0
- package/src/location-service-map.ts +18 -0
- package/src/location-services.ts +115 -0
- package/src/location.ts +39 -0
- package/src/markdown.d.ts +4 -0
- package/src/model.ts +41 -0
- package/src/models-dev.ts +260 -0
- package/src/npm-config.ts +40 -0
- package/src/npm.ts +269 -0
- package/src/oauth/page.ts +276 -0
- package/src/observability/logging.ts +71 -0
- package/src/observability/otlp.ts +79 -0
- package/src/observability/shared.ts +1 -0
- package/src/observability.ts +24 -0
- package/src/patch.ts +197 -0
- package/src/permission/saved.ts +79 -0
- package/src/permission/sql.ts +20 -0
- package/src/permission.ts +310 -0
- package/src/plugin/agent.ts +206 -0
- package/src/plugin/command/initialize.txt +65 -0
- package/src/plugin/command/review.txt +100 -0
- package/src/plugin/command.ts +25 -0
- package/src/plugin/host.ts +219 -0
- package/src/plugin/internal.ts +153 -0
- package/src/plugin/layer-map.example.ts +94 -0
- package/src/plugin/models-dev.ts +183 -0
- package/src/plugin/promise.ts +93 -0
- package/src/plugin/provider/alibaba.ts +15 -0
- package/src/plugin/provider/amazon-bedrock.ts +126 -0
- package/src/plugin/provider/anthropic.ts +27 -0
- package/src/plugin/provider/azure.ts +82 -0
- package/src/plugin/provider/cerebras.ts +26 -0
- package/src/plugin/provider/cloudflare-ai-gateway.ts +81 -0
- package/src/plugin/provider/cloudflare-workers-ai.ts +81 -0
- package/src/plugin/provider/cohere.ts +15 -0
- package/src/plugin/provider/deepinfra.ts +15 -0
- package/src/plugin/provider/dynamic.ts +31 -0
- package/src/plugin/provider/gateway.ts +15 -0
- package/src/plugin/provider/github-copilot.ts +52 -0
- package/src/plugin/provider/gitlab.ts +65 -0
- package/src/plugin/provider/google-vertex.ts +173 -0
- package/src/plugin/provider/google.ts +15 -0
- package/src/plugin/provider/groq.ts +15 -0
- package/src/plugin/provider/kilo.ts +21 -0
- package/src/plugin/provider/llmgateway.ts +26 -0
- package/src/plugin/provider/mistral.ts +15 -0
- package/src/plugin/provider/nvidia.ts +22 -0
- package/src/plugin/provider/openai-compatible.ts +17 -0
- package/src/plugin/provider/openai.ts +292 -0
- package/src/plugin/provider/opencode.ts +311 -0
- package/src/plugin/provider/openrouter.ts +36 -0
- package/src/plugin/provider/perplexity.ts +15 -0
- package/src/plugin/provider/sap-ai-core.ts +46 -0
- package/src/plugin/provider/snowflake-cortex.ts +89 -0
- package/src/plugin/provider/togetherai.ts +15 -0
- package/src/plugin/provider/venice.ts +15 -0
- package/src/plugin/provider/vercel.ts +27 -0
- package/src/plugin/provider/xai.ts +22 -0
- package/src/plugin/provider/zenmux.ts +21 -0
- package/src/plugin/provider.ts +71 -0
- package/src/plugin/skill/customize-neurocode.md +452 -0
- package/src/plugin/skill.ts +31 -0
- package/src/plugin/variant.ts +39 -0
- package/src/plugin.ts +167 -0
- package/src/policy.ts +49 -0
- package/src/process.ts +261 -0
- package/src/project/copy-strategies.ts +35 -0
- package/src/project/copy.ts +292 -0
- package/src/project/directories.ts +158 -0
- package/src/project/schema.ts +16 -0
- package/src/project/sql.ts +35 -0
- package/src/project.ts +136 -0
- package/src/provider.ts +25 -0
- package/src/pty/protocol.ts +37 -0
- package/src/pty/pty.bun.ts +26 -0
- package/src/pty/pty.node.ts +29 -0
- package/src/pty/pty.ts +25 -0
- package/src/pty/schema.ts +1 -0
- package/src/pty/ticket.ts +56 -0
- package/src/pty.ts +318 -0
- package/src/public-event-manifest.ts +7 -0
- package/src/question.ts +153 -0
- package/src/reference/guidance.ts +69 -0
- package/src/reference.ts +125 -0
- package/src/repository-cache.ts +259 -0
- package/src/repository.ts +214 -0
- package/src/ripgrep/binary.ts +132 -0
- package/src/ripgrep.ts +281 -0
- package/src/schema.ts +79 -0
- package/src/session/compaction.ts +241 -0
- package/src/session/context-epoch.ts +174 -0
- package/src/session/error.ts +24 -0
- package/src/session/event.ts +2 -0
- package/src/session/execution/local.ts +46 -0
- package/src/session/execution.ts +34 -0
- package/src/session/history.ts +101 -0
- package/src/session/info.ts +50 -0
- package/src/session/input.ts +288 -0
- package/src/session/message-updater.ts +397 -0
- package/src/session/message.ts +2 -0
- package/src/session/projector.ts +458 -0
- package/src/session/prompt.ts +1 -0
- package/src/session/revert.ts +121 -0
- package/src/session/run-coordinator.ts +104 -0
- package/src/session/runner/index.ts +28 -0
- package/src/session/runner/llm.ts +432 -0
- package/src/session/runner/max-steps.ts +16 -0
- package/src/session/runner/model.ts +218 -0
- package/src/session/runner/publish-llm-event.ts +423 -0
- package/src/session/runner/to-llm-message.ts +171 -0
- package/src/session/schema.ts +9 -0
- package/src/session/sql.ts +176 -0
- package/src/session/store.ts +63 -0
- package/src/session/todo.ts +78 -0
- package/src/session.ts +486 -0
- package/src/share/sql.ts +13 -0
- package/src/shell.ts +226 -0
- package/src/skill/discovery.ts +213 -0
- package/src/skill/guidance.ts +76 -0
- package/src/skill.ts +132 -0
- package/src/snapshot.ts +266 -0
- package/src/state.ts +128 -0
- package/src/system-context/builtins.ts +50 -0
- package/src/system-context/index.ts +320 -0
- package/src/system-context/registry.ts +49 -0
- package/src/tool/AGENTS.md +59 -0
- package/src/tool/application-tools.ts +57 -0
- package/src/tool/apply-patch.ts +219 -0
- package/src/tool/bash.ts +207 -0
- package/src/tool/builtins.ts +48 -0
- package/src/tool/edit.ts +223 -0
- package/src/tool/glob.ts +105 -0
- package/src/tool/grep.ts +137 -0
- package/src/tool/http-body.ts +30 -0
- package/src/tool/question.ts +94 -0
- package/src/tool/read-filesystem.ts +366 -0
- package/src/tool/read.ts +117 -0
- package/src/tool/registry.ts +147 -0
- package/src/tool/skill.ts +109 -0
- package/src/tool/todowrite.ts +62 -0
- package/src/tool/tool.ts +162 -0
- package/src/tool/tools.ts +13 -0
- package/src/tool/webfetch.ts +218 -0
- package/src/tool/websearch.ts +260 -0
- package/src/tool/write.ts +101 -0
- package/src/tool-output-store.ts +211 -0
- package/src/util/array.ts +10 -0
- package/src/util/binary.ts +41 -0
- package/src/util/effect-flock.ts +284 -0
- package/src/util/encode.ts +51 -0
- package/src/util/error.ts +70 -0
- package/src/util/flock.ts +358 -0
- package/src/util/glob.ts +34 -0
- package/src/util/hash.ts +11 -0
- package/src/util/identifier.ts +1 -0
- package/src/util/iife.ts +3 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/module.ts +10 -0
- package/src/util/path.ts +37 -0
- package/src/util/retry.ts +42 -0
- package/src/util/slug.ts +74 -0
- package/src/util/token.ts +5 -0
- package/src/util/which.ts +14 -0
- package/src/util/wildcard.ts +14 -0
- package/src/v1/config/agent.ts +89 -0
- package/src/v1/config/attachment.ts +25 -0
- package/src/v1/config/command.ts +13 -0
- package/src/v1/config/config.ts +192 -0
- package/src/v1/config/console-state.ts +16 -0
- package/src/v1/config/error.ts +39 -0
- package/src/v1/config/formatter.ts +13 -0
- package/src/v1/config/layout.ts +6 -0
- package/src/v1/config/lsp.ts +80 -0
- package/src/v1/config/mcp.ts +63 -0
- package/src/v1/config/migrate.ts +254 -0
- package/src/v1/config/permission.ts +50 -0
- package/src/v1/config/plugin.ts +9 -0
- package/src/v1/config/provider-options.ts +227 -0
- package/src/v1/config/provider.ts +121 -0
- package/src/v1/config/server.ts +19 -0
- package/src/v1/config/skills.ts +13 -0
- package/src/v1/permission.ts +33 -0
- package/src/v1/session.ts +68 -0
- package/src/v2-schema.ts +3 -0
- package/src/workspace.ts +6 -0
- package/sst-env.d.ts +10 -0
- package/test/agent.test.ts +131 -0
- package/test/application-tools.test.ts +287 -0
- package/test/background-job.test.ts +106 -0
- package/test/catalog.test.ts +353 -0
- package/test/command.test.ts +56 -0
- package/test/config/agent.test.ts +349 -0
- package/test/config/command.test.ts +83 -0
- package/test/config/config.test.ts +798 -0
- package/test/config/fixtures/plugin/directory-plugin.ts +13 -0
- package/test/config/plugin.test.ts +248 -0
- package/test/config/provider-options.test.ts +224 -0
- package/test/config/provider.test.ts +269 -0
- package/test/config/skill.test.ts +80 -0
- package/test/credential.test.ts +36 -0
- package/test/database-migration.test.ts +644 -0
- package/test/effect/cross-spawn-spawner.test.ts +426 -0
- package/test/effect/keyed-mutex.test.ts +73 -0
- package/test/effect/layer-node/layer-node-types.test.ts +143 -0
- package/test/effect/layer-node/layer-node.test.ts +261 -0
- package/test/effect/layer-node/node-build.test.ts +125 -0
- package/test/effect/observability.test.ts +109 -0
- package/test/event.test.ts +1124 -0
- package/test/file-mutation.test.ts +368 -0
- package/test/filesystem/filesystem.test.ts +387 -0
- package/test/filesystem/ignore.test.ts +10 -0
- package/test/filesystem/search.test.ts +44 -0
- package/test/filesystem/watcher.test.ts +266 -0
- package/test/fixture/effect-flock-worker.ts +60 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/fixture/git.ts +49 -0
- package/test/fixture/location.ts +26 -0
- package/test/fixture/tmpdir.ts +25 -0
- package/test/fixtures/recordings/session-runner/openai-chat-streams-text.json +27 -0
- package/test/git.test.ts +164 -0
- package/test/github-copilot/convert-to-copilot-messages.test.ts +523 -0
- package/test/github-copilot/copilot-chat-model.test.ts +592 -0
- package/test/github-copilot/openai-responses-language-model.test.ts +206 -0
- package/test/global.test.ts +16 -0
- package/test/instruction-context.test.ts +323 -0
- package/test/integration.test.ts +349 -0
- package/test/legacy-event-schema.test.ts +16 -0
- package/test/lib/effect.ts +53 -0
- package/test/lib/tool.ts +20 -0
- package/test/location-filesystem.test.ts +69 -0
- package/test/location-layer.test.ts +226 -0
- package/test/location-mutation.test.ts +178 -0
- package/test/location.test.ts +42 -0
- package/test/model.test.ts +23 -0
- package/test/models.test.ts +290 -0
- package/test/move-session.test.ts +235 -0
- package/test/npm-config.test.ts +51 -0
- package/test/npm.test.ts +84 -0
- package/test/oauth-page.test.ts +15 -0
- package/test/patch.test.ts +68 -0
- package/test/permission.test.ts +315 -0
- package/test/plugin/command.test.ts +47 -0
- package/test/plugin/fixture.ts +52 -0
- package/test/plugin/fixtures/config-effect-plugin.ts +15 -0
- package/test/plugin/fixtures/config-promise-plugin.ts +13 -0
- package/test/plugin/fixtures/invalid-plugin.ts +1 -0
- package/test/plugin/fixtures/models-dev.json +14 -0
- package/test/plugin/fixtures/provider-factory.ts +9 -0
- package/test/plugin/host.ts +303 -0
- package/test/plugin/models-dev.test.ts +170 -0
- package/test/plugin/promise.test.ts +67 -0
- package/test/plugin/provider-alibaba.test.ts +92 -0
- package/test/plugin/provider-amazon-bedrock.test.ts +622 -0
- package/test/plugin/provider-anthropic.test.ts +94 -0
- package/test/plugin/provider-azure-cognitive-services.test.ts +199 -0
- package/test/plugin/provider-azure.test.ts +293 -0
- package/test/plugin/provider-cerebras.test.ts +136 -0
- package/test/plugin/provider-cloudflare-ai-gateway.test.ts +426 -0
- package/test/plugin/provider-cloudflare-workers-ai.test.ts +279 -0
- package/test/plugin/provider-cohere.test.ts +123 -0
- package/test/plugin/provider-deepinfra.test.ts +159 -0
- package/test/plugin/provider-dynamic.test.ts +184 -0
- package/test/plugin/provider-gateway.test.ts +115 -0
- package/test/plugin/provider-github-copilot.test.ts +278 -0
- package/test/plugin/provider-gitlab.test.ts +308 -0
- package/test/plugin/provider-google-vertex-anthropic.test.ts +264 -0
- package/test/plugin/provider-google-vertex.test.ts +387 -0
- package/test/plugin/provider-google.test.ts +80 -0
- package/test/plugin/provider-groq.test.ts +121 -0
- package/test/plugin/provider-kilo.test.ts +99 -0
- package/test/plugin/provider-llmgateway.test.ts +80 -0
- package/test/plugin/provider-mistral.test.ts +129 -0
- package/test/plugin/provider-nvidia.test.ts +97 -0
- package/test/plugin/provider-openai-compatible.test.ts +107 -0
- package/test/plugin/provider-openai.test.ts +176 -0
- package/test/plugin/provider-opencode.test.ts +423 -0
- package/test/plugin/provider-openrouter.test.ts +113 -0
- package/test/plugin/provider-perplexity.test.ts +122 -0
- package/test/plugin/provider-sap-ai-core.test.ts +159 -0
- package/test/plugin/provider-snowflake-cortex.test.ts +261 -0
- package/test/plugin/provider-togetherai.test.ts +129 -0
- package/test/plugin/provider-venice.test.ts +115 -0
- package/test/plugin/provider-vercel.test.ts +83 -0
- package/test/plugin/provider-xai.test.ts +125 -0
- package/test/plugin/provider-zenmux.test.ts +110 -0
- package/test/plugin/skill.test.ts +25 -0
- package/test/plugin/variant.test.ts +67 -0
- package/test/plugin.test.ts +71 -0
- package/test/policy.test.ts +85 -0
- package/test/preload.ts +5 -0
- package/test/process/process.test.ts +368 -0
- package/test/project-copy.test.ts +400 -0
- package/test/project-directories.test.ts +61 -0
- package/test/project.test.ts +221 -0
- package/test/provider-mistral.test.ts +282 -0
- package/test/provider-xai-responses.test.ts +32 -0
- package/test/pty/info-schema.test.ts +32 -0
- package/test/pty/protocol.test.ts +27 -0
- package/test/pty/pty-session.test.ts +240 -0
- package/test/pty/ticket.test.ts +62 -0
- package/test/question.test.ts +114 -0
- package/test/reference-guidance.test.ts +76 -0
- package/test/reference.test.ts +78 -0
- package/test/repository-cache.test.ts +153 -0
- package/test/repository.test.ts +71 -0
- package/test/ripgrep.test.ts +65 -0
- package/test/session-compaction.test.ts +27 -0
- package/test/session-create.test.ts +425 -0
- package/test/session-history.test.ts +165 -0
- package/test/session-projector.test.ts +533 -0
- package/test/session-prompt.test.ts +584 -0
- package/test/session-run-coordinator.test.ts +418 -0
- package/test/session-runner-message.test.ts +501 -0
- package/test/session-runner-model.test.ts +347 -0
- package/test/session-runner-recorded.test.ts +193 -0
- package/test/session-runner-tool-events.test.ts +136 -0
- package/test/session-runner-tool-registry.test.ts +452 -0
- package/test/session-runner.test.ts +3365 -0
- package/test/session-todo.test.ts +94 -0
- package/test/session-tool-progress.test.ts +158 -0
- package/test/shared-schema.test.ts +206 -0
- package/test/shell.test.ts +108 -0
- package/test/skill/guidance.test.ts +144 -0
- package/test/skill-discovery.test.ts +165 -0
- package/test/skill.test.ts +125 -0
- package/test/snapshot.test.ts +178 -0
- package/test/state.test.ts +115 -0
- package/test/system-context/builtins.test.ts +129 -0
- package/test/system-context/index.test.ts +307 -0
- package/test/system-context/registry.test.ts +114 -0
- package/test/tool-apply-patch.test.ts +436 -0
- package/test/tool-bash.test.ts +437 -0
- package/test/tool-edit.test.ts +434 -0
- package/test/tool-output-store.test.ts +247 -0
- package/test/tool-question.test.ts +162 -0
- package/test/tool-read-filesystem.test.ts +118 -0
- package/test/tool-read.test.ts +674 -0
- package/test/tool-skill.test.ts +149 -0
- package/test/tool-todowrite.test.ts +125 -0
- package/test/tool-webfetch.test.ts +281 -0
- package/test/tool-websearch.test.ts +316 -0
- package/test/tool-write.test.ts +305 -0
- package/test/util/effect-flock.test.ts +392 -0
- package/test/util/flock.test.ts +428 -0
- package/test/util/which.test.ts +100 -0
- 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
|
+
})
|