diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a65b99277..7498a84ae9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,8 @@ jobs: turbo-${{ runner.os }}- - name: Run unit tests - run: bun turbo test:ci + timeout-minutes: 20 + run: bun turbo test:ci --log-order=stream --log-prefix=task env: OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} diff --git a/AGENTS.md b/AGENTS.md index 66d88c2cbc..6ed0761b89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -138,3 +138,14 @@ const table = sqliteTable("session", { ## Type Checking - Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly. + +## V2 Session Core + +- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries. +- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry. +- Keep `SessionExecution` process-global and Session-ID based. It discovers placement through the read-side `SessionStore` and `LocationServiceMap.get(session.location)`; no layer should take a Session ID. +- Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics. +- Preserve one explicit `llm.stream(request)` call per provider turn and reload projected history before durable continuation. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop. +- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash activity recovery requires a separate explicit design before it may retry provider work. +- Keep delivery vocabulary explicit. Prompts steer by default and coalesce into the active activity at the next safe provider-turn boundary. Explicit `queue` inputs open FIFO future activities one at a time after the active activity settles. +- Keep EventV2 replay owner claims separate from clustered Session execution ownership. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000000..a391bf95cc --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,77 @@ +# OpenCode Session Runtime + +OpenCode sessions preserve durable conversational history while assembling the runtime context an agent needs to act correctly in its current environment. + +## Language + +**System Context**: +The structured collection of contextual facts presented to the model as initial instructions and chronological updates. +_Avoid_: System prompt + +**Context Component**: +One independently loaded fact within the **System Context**, represented by a stable key and one effectfully loaded baseline/update rendering. +_Avoid_: Prompt fragment + +**Mid-Conversation System Message**: +A durable chronological instruction that tells the model the newly effective state of a changed **Context Component**. +_Avoid_: System update, system notification, raw text diff + +**Context Epoch**: +The span during which one initially rendered **System Context** remains immutable, ending at compaction or another baseline-replacing transition. + +**Baseline System Context**: +The full **System Context** rendered at the start of a **Context Epoch**. +_Avoid_: Live system prompt + +**Context Checkpoint**: +The durable model-hidden comparison state used to detect which **Context Components** changed since context was last admitted to a provider turn. + +**Unavailable Context**: +An expected temporary inability to load a **Context Component** value; the runtime retains its prior effective state and emits no update, or omits it until first successfully loaded. + +**Safe Provider-Turn Boundary**: +The point immediately before a provider call, after durable input promotion and any required tool settlement, where context changes may be admitted chronologically. + +## Relationships + +- A **System Context** contains one or more **Context Components**. +- A changed **Context Component** may produce one **Mid-Conversation System Message** containing its newly effective state. +- A **Mid-Conversation System Message** persists its originating **Context Component** key and the exact rendered text sent to the model. +- A **Context Checkpoint** advances atomically with the corresponding durable **Mid-Conversation System Message**. +- A **Context Checkpoint** stores one rendered-content hash per stable **Context Component** key so core and plugin-defined components can evolve independently. +- Changes from multiple **Context Components** admitted at one safe boundary combine into one **Mid-Conversation System Message**. +- Context changes are sampled and admitted lazily at a **Safe Provider-Turn Boundary**, never pushed asynchronously when their source changes. +- At a **Safe Provider-Turn Boundary**, newly promoted user input or settled tool results precede any combined **Mid-Conversation System Message**. +- The first provider turn renders the latest **Baseline System Context** and initializes its **Context Checkpoint** without emitting a redundant **Mid-Conversation System Message**. +- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Checkpoint**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history. +- A **Context Checkpoint** is an evolvable component map; a newly registered core or plugin-defined **Context Component** absent from an existing checkpoint emits its current state once at the next **Safe Provider-Turn Boundary**. +- **Context Component** keys are stable and namespaced; duplicate keys fail assembly. Built-in components preserve declaration order and plugin-defined components append in lexicographic key order so rendered context is deterministic. +- Each **Context Component** loader returns its model-visible baseline string and absolute current-state update string from one coherent sample; the update string is hashed for change detection. +- **Unavailable Context** uses stale-while-revalidate semantics and is distinct from a successfully loaded absence, which may emit removal text. +- Ordinary **Context Component** loaders return values directly; loaders that intentionally use stale-while-revalidate may explicitly return **Unavailable Context**. +- Nested project instruction files discovered while reading join the effective instructions returned by the instruction service and are admitted durably at the next **Safe Provider-Turn Boundary**. +- A discovered nested project instruction remains active for the session while it stays in the same location and is folded into later **Baseline System Contexts** after compaction. +- Location-scoped services naturally re-resolve effective context when a moved session next runs in its destination location. +- Instruction discovery, source identity, persistence, and file loading belong to the instruction service; the **System Context** abstraction only composes effectful producers and renders loaded values. +- Plugin-defined **Context Components** register through a scoped replayable registry so plugin hot reload adds and removes components predictably. +- Context source changes never wake idle sessions; the next naturally scheduled **Safe Provider-Turn Boundary** loads and compares current values lazily. +- Once admitted, a **Mid-Conversation System Message** remains durable even if the following provider attempt fails and is replayed unchanged on retry. +- **Mid-Conversation System Messages** remain durable model-projection history but are hidden from normal user-facing transcript surfaces. +- The date **Context Component** initially preserves host-local calendar-date behavior; a configured user timezone may replace that default later. +- A **Context Epoch** begins with one immutable **Baseline System Context**. +- A **Baseline System Context** is stored durably and reused verbatim across process restarts within its **Context Epoch**. +- A **Baseline System Context** durably preserves deterministic keyed top-level component strings rather than eagerly joining all text; request assembly lowers them into canonical LLM system parts. +- Compaction or a model/provider switch starts a new **Context Epoch** because the baseline can be replaced without preserving the prior provider cache. +- A model/provider switch always starts a new **Context Epoch** while preserving chronological conversation history. +- A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise. +- When an effective instruction file changes, its **Mid-Conversation System Message** includes the complete current contents and supersedes the prior version from that source; when it is removed, the message states that it no longer applies. + +## Example dialogue + +> **Dev:** "The date changed while the session was active. Should the **Mid-Conversation System Message** say what the old date was?" +> **Domain expert:** "No. Emit the newly effective date so the agent can act on the current **System Context**." + +## Flagged ambiguities + +- Legacy `experimental.chat.system.transform` can mutate the assembled baseline system prompt arbitrarily, but V2 plugins do not yet expose an equivalent hook. Decide separately whether to port it, replace dynamic uses with plugin-defined **Context Components**, or narrow its semantics. +- A location change likely starts a new **Context Epoch** so location-dependent instructions and discovery can be rebuilt cleanly, but implementation should verify whether an append-only update is sufficient and meaningfully preserves cache. diff --git a/bun.lock b/bun.lock index 2bb58f67d7..aaed38d305 100644 --- a/bun.lock +++ b/bun.lock @@ -265,6 +265,7 @@ "@npmcli/config": "10.8.1", "@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/effect-sqlite-node": "workspace:*", + "@opencode-ai/llm": "workspace:*", "@openrouter/ai-sdk-provider": "2.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", @@ -281,6 +282,7 @@ "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", + "htmlparser2": "8.0.2", "ignore": "7.0.5", "immer": "11.1.4", "jsonc-parser": "3.3.1", @@ -288,12 +290,14 @@ "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "semver": "^7.6.3", + "turndown": "7.2.0", "venice-ai-sdk-provider": "2.0.2", "which": "6.0.1", "xdg-basedir": "5.1.0", "zod": "catalog:", }, "devDependencies": { + "@opencode-ai/http-recorder": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", @@ -309,6 +313,7 @@ "@types/npm-package-arg": "6.1.4", "@types/npmcli__arborist": "6.3.3", "@types/semver": "catalog:", + "@types/turndown": "5.0.5", "@types/which": "3.0.4", "drizzle-kit": "catalog:", }, @@ -854,7 +859,6 @@ "@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", - "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", }, "overrides": { @@ -939,7 +943,7 @@ "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], - "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.21.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw=="], @@ -955,15 +959,15 @@ "@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqcCq2PiFY1dbK/0Ck45KuvE8jfdxRuuAE9Y5w46dAk6U+9vPOeg1CDcmR+ncqmrYrhRl3nmyDttyDahyjCzAw=="], - "@ai-sdk/deepgram": ["@ai-sdk/deepgram@2.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqzitR171deAOWTmdqkP6okGrOvDzdDxqLnW7040OjdfsuyhtR26iL6v+zPGUtmVukwWrJnKklNbomui8y7+mw=="], + "@ai-sdk/deepgram": ["@ai-sdk/deepgram@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VscTV68g6sXRY4O1yl72/O8y6+tBDvSQax6bqX06hRKWBGxsJ8Jr3LZsNmZnK9Od5Icx565ijK0QgrlNaN4TdQ=="], "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-y6RoOP7DGWmDSiSxrUSt5p18sbz+Ixe5lMVPmdE7x+Tr5rlrzvftyHhjWHfqlAtoYERZTGFbP6tPW1OfQcrb4A=="], - "@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cn4+xV0menm/4JKEDElnVGiUilHvi6AD4ZK/sY7DXP/Wb7Yb3Vr86NyYM6mGBE/Shk3mWHoHbzggVnF5x0uMEA=="], + "@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9DhYurbAvcurOEGN6u2myYDybrrzGfcrkG8hwmFjwTrePW6KCMggm0YxP7e8RkLYcQKqCEMgFlyEB4BM6EmiKg=="], - "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-l4t+kgOtDav2P2BJ50gZfhOYbKcGblnD0U8jXOF3WH3dczYmYfTC7JGH1/MTheurSy6UnhLw7ee4wL6StCTQ+w=="], + "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EtvsWfGrqx3OhzJdoi82qH+4yzEPPKZr2utyQ+w8cHKoFeg0+8Lou9Z3uixy73WEwz8Z1+AR8QT9fZ64AWGYPA=="], - "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.46", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XRKR0zgRyegdmtK5CDUEjlyRp0Fo+XVCdoG+301U1SGtgRIAYG3ObVtgzVJBVpJdHFSLHuYeLTnNiQoUxD7+FQ=="], + "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.53", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.48", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HjeiGsdxSzrCkOf2l2V+K+opzlqxBtduBq6BCiohAdgQk2KdZmI/67SMkBM6Kdze/BjUXiZlv0d7zNICPhxVDA=="], "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], @@ -1007,7 +1011,7 @@ "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.1", "", {}, "sha512-7dwEVigz9vUWDw3nRwLQ/yH/xYovlUA0ZD86xoeKEBmkz9O6iELG1yri67PgAPW6VLL/xInA4t7H0CK6VmtkKQ=="], - "@astrojs/language-server": ["@astrojs/language-server@2.16.6", "", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.3", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.15", "volar-service-css": "0.0.70", "volar-service-emmet": "0.0.70", "volar-service-html": "0.0.70", "volar-service-prettier": "0.0.70", "volar-service-typescript": "0.0.70", "volar-service-typescript-twoslash-queries": "0.0.70", "volar-service-yaml": "0.0.70", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-N990lu+HSFiG57owR0XBkr02BYMgiLCshLf+4QG4v6jjSWkBeQGnzqi+E1L08xFPPJ7eEeXnxPXGLaVv5pa4Ug=="], + "@astrojs/language-server": ["@astrojs/language-server@2.16.10", "", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.4", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.16", "volar-service-css": "0.0.70", "volar-service-emmet": "0.0.70", "volar-service-html": "0.0.70", "volar-service-prettier": "0.0.70", "volar-service-typescript": "0.0.70", "volar-service-typescript-twoslash-queries": "0.0.70", "volar-service-yaml": "0.0.70", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "./bin/nodeServer.js" } }, "sha512-87VQ/5GSdHlRnUA+hGuerYyIGAj+9RbZmATyuKLEUePinUXhQ5YkRnRrHhOD9sSi5JOErLjrLkHnfZFEvGrV8w=="], "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.2.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^3.0.0", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg=="], @@ -1015,7 +1019,7 @@ "@astrojs/prism": ["@astrojs/prism@3.2.0", "", { "dependencies": { "prismjs": "^1.29.0" } }, "sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw=="], - "@astrojs/sitemap": ["@astrojs/sitemap@3.7.2", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA=="], + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.3", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA=="], "@astrojs/solid-js": ["@astrojs/solid-js@5.1.0", "", { "dependencies": { "vite": "^6.3.5", "vite-plugin-solid": "^2.11.6" }, "peerDependencies": { "solid-devtools": "^0.30.1", "solid-js": "^1.8.5" }, "optionalPeers": ["solid-devtools"] }, "sha512-VmPHOU9k7m6HHCT2Y1mNzifilUnttlowBM36frGcfj5wERJE9Ci0QtWJbzdf6AlcoIirb7xVw+ByupU011Di9w=="], @@ -1025,7 +1029,7 @@ "@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.0", "", {}, "sha512-qZxHwVnmb5FXuvRsaIGaqWgnftjCuMY+GSbaVZdBmE4j8AfgPqKPxYp8SUERyJcjpKCEmO4wD6ybuGH8A2kVRQ=="], - "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.3", "", { "dependencies": { "yaml": "^2.8.2" } }, "sha512-PJzRmgQzUxI2uwpdX2lXSHtP4G8ocp24/t+bZyf5Fy0SZLSF9f9KXZoMlFM/XCGue+B0nH/2IZ7FpBYQATBsCg=="], + "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="], "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], @@ -1047,7 +1051,7 @@ "@aws-sdk/client-firehose": ["@aws-sdk/client-firehose@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-tDrtgczN2lQsflLDPYu/wdOoyCZLVYtgzmWnYzSEOBWd/cp2AbuQ7D+FemSwUTzyoMTuhhIevyEJKzqsF+QYxA=="], - "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-ryEYNVdilyWkKsOs/7Xy/l7+qjtSz4sll8NpcWD6AtONxjG/5OMaAhxxDkQb4iBoNMKnISxsARzQAp/Wa8pXIg=="], + "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1057.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-JoaE3QNvPqOSHJtcAFfza7BRoGUNerIKlSVhsKCBsa1i54Vto1YWUS7itkUELK2rpvS8kNZMPliPxDdi/oV5dw=="], "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="], @@ -1057,23 +1061,23 @@ "@aws-sdk/core": ["@aws-sdk/core@3.932.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-AS8gypYQCbNojwgjvZGkJocC2CoEICDx9ZJ15ILsv+MlcCVLtUJSRSx3VzJOUY2EEIaGLRrPNlIqyn/9/fySvA=="], - "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.22", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-ih6ORpme4i2qJqGckOQ9Lt2iiZ+5tm3bnfsT5TwoPyFnuDURXv3OdhYa3Nr/m0iJr38biqKYKdGKb5GR1KB2hw=="], + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.38", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-OHkK6xOx/IHkSbQdDWxnVCLU+j28EFl8wyWgBILQDFAPY8n240C/O4gjmFx+zFU12lL8njgJQ5GWAIWq88CnSQ=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/node-http-handler": "^4.5.2", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" } }, "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.43", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-login": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA=="], "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.933.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-ini": "3.933.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-L2dE0Y7iMLammQewPKNeEh1z/fdJyYEU+/QsLBD9VEh+SXcN/FIyTi21Isw8wPZN6lMB9PDVtISzBnF8HuSFrw=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/token-providers": "3.1026.0", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/token-providers": "3.1056.0", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ=="], "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.993.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="], @@ -1103,7 +1107,7 @@ "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.932.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-NCIRJvoRc9246RZHIusY1+n/neeG2yGhBGdKhghmrNdM+mLLN6Ii7CKFZjx3DhxtpHMpl1HWLTMhdVrGwP2upw=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1026.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1056.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA=="], "@aws-sdk/types": ["@aws-sdk/types@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A=="], @@ -1123,8 +1127,6 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], - "@azure-rest/core-client": ["@azure-rest/core-client@2.6.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ=="], - "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], @@ -1147,91 +1149,79 @@ "@azure/core-xml": ["@azure/core-xml@1.5.1", "", { "dependencies": { "fast-xml-parser": "^5.5.9", "tslib": "^2.8.1" } }, "sha512-xcNRHqCoSp4AunOALEae6A8f3qATb83gSrm31Iqb01OzblvC3/W/bfXozcq78EzIdzZzuH1bZ2NvRR0TdX709w=="], - "@azure/identity": ["@azure/identity@4.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^5.1.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw=="], - - "@azure/keyvault-common": ["@azure/keyvault-common@2.1.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw=="], - - "@azure/keyvault-keys": ["@azure/keyvault-keys@4.10.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/keyvault-common": "^2.0.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag=="], - "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], - "@azure/msal-browser": ["@azure/msal-browser@5.6.3", "", { "dependencies": { "@azure/msal-common": "16.4.1" } }, "sha512-sTjMtUm+bJpENU/1WlRzHEsgEHppZDZ1EtNyaOODg/sQBtMxxJzGB+MOCM+T2Q5Qe1fKBrdxUmjyRxm0r7Ez9w=="], - - "@azure/msal-common": ["@azure/msal-common@16.4.1", "", {}, "sha512-Bl8f+w37xkXsYh7QRkAKCFGYtWMYuOVO7Lv+BxILrvGz3HbIEF22Pt0ugyj0QPOl6NLrHcnNUQ9yeew98P/5iw=="], - - "@azure/msal-node": ["@azure/msal-node@5.1.2", "", { "dependencies": { "@azure/msal-common": "16.4.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-DoeSJ9U5KPAIZoHsPywvfEj2MhBniQe0+FSpjLUTdWoIkI999GB5USkW6nNEHnIaLVxROHXvprWA1KzdS1VQ4A=="], - "@azure/storage-blob": ["@azure/storage-blob@12.31.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.3.0", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg=="], "@azure/storage-common": ["@azure/storage-common@12.3.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ=="], - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], - "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], - "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], + "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ=="], - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], - "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="], + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.0", "", {}, "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA=="], - "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.11.0", "", { "dependencies": { "@bufbuild/protobuf": "2.11.0", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-lyZVNFUHArIOt4W0+dwYBe5GBwbKzbOy8ObaloEqsw9Mmiwv2O48TwddDoHN4itylC+BaEGqFdI1W8WQt2vWJQ=="], + "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.12.0", "", { "dependencies": { "@bufbuild/protobuf": "2.12.0", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-ORlDITp8AFUXzIhLRoMCG+ud+D3MPKWb5HQXBoskMMnjeyEjE1H1qLonVNPyOr8lkx3xSfYUo8a0dvOZJVAzow=="], "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], @@ -1287,7 +1277,7 @@ "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], - "@electron/rebuild": ["@electron/rebuild@4.0.3", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA=="], + "@electron/rebuild": ["@electron/rebuild@4.0.4", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg=="], "@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], @@ -1421,9 +1411,7 @@ "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="], - "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], - - "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], "@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="], @@ -1467,11 +1455,11 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], - "@internationalized/date": ["@internationalized/date@3.12.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ=="], + "@internationalized/date": ["@internationalized/date@3.12.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw=="], - "@internationalized/number": ["@internationalized/number@3.6.6", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ=="], + "@internationalized/number": ["@internationalized/number@3.6.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg=="], - "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], + "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], @@ -1497,8 +1485,6 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="], - "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], @@ -1605,9 +1591,9 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="], + "@nodable/entities": ["@nodable/entities@2.1.1", "", {}, "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1617,7 +1603,7 @@ "@npm/types": ["@npm/types@1.0.2", "", {}, "sha512-KXZccTDEnWqNrrx6JjpJKU/wJvNeg9BDgjS0XhmlZab7br921HtyVbsYzJr4L+xIvjdJ20Wh9dgxgCI2a5CEQw=="], - "@npmcli/agent": ["@npmcli/agent@4.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA=="], + "@npmcli/agent": ["@npmcli/agent@4.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg=="], "@npmcli/arborist": ["@npmcli/arborist@9.4.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/map-workspaces": "^5.0.0", "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^4.0.0", "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^5.0.0", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", "minimatch": "^10.0.3", "nopt": "^9.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.0", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "pacote": "^21.0.2", "parse-conflict-json": "^5.0.1", "proc-log": "^6.0.0", "proggy": "^4.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", "walk-up-path": "^4.0.0" }, "bin": { "arborist": "bin/index.js" } }, "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA=="], @@ -1769,7 +1755,7 @@ "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], "@opentui/core": ["@opentui/core@0.3.1", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.3.1", "@opentui/core-darwin-x64": "0.3.1", "@opentui/core-linux-arm64": "0.3.1", "@opentui/core-linux-x64": "0.3.1", "@opentui/core-win32-arm64": "0.3.1", "@opentui/core-win32-x64": "0.3.1" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-kQFSsSCgtlasSqTigCgKmM67xaquGvTg+vwimDnFSZtcBEt4E3dz7qLrbeh5FVvTA+RMbwe+Bozq03PW+SgjXw=="], @@ -1829,6 +1815,86 @@ "@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-T2ijfqZLpV2bgGGocXV4SXTuMoouqN0asYTIm+7jVOLvT5XgDogf3ZvCmiEnSWmxl21+r5wHcs8voU2iUROXAg=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="], + + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.127.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.127.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.127.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.127.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ=="], + + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g=="], + + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ=="], + + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA=="], + + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.127.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ=="], + + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ=="], + + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g=="], + + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.127.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q=="], + + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ=="], + + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg=="], + + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.127.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ=="], + + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.127.0", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ=="], + + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.127.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw=="], + + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.127.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw=="], + + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="], + + "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="], + + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.20.0", "", { "os": "android", "cpu": "arm64" }, "sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.20.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.20.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.20.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg=="], + + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.20.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.20.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.20.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw=="], + + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.20.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.20.0", "", { "os": "linux", "cpu": "none" }, "sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw=="], + + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.20.0", "", { "os": "linux", "cpu": "none" }, "sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.20.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.20.0", "", { "os": "linux", "cpu": "x64" }, "sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.20.0", "", { "os": "linux", "cpu": "x64" }, "sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ=="], + + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.20.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.20.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.20.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.20.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw=="], + "@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.96.0", "", { "os": "android", "cpu": "arm64" }, "sha512-wOm+ZsqFvyZ7B9RefUMsj0zcXw77Z2pXA51nbSQyPXqr+g0/pDGxriZWP8Sdpz/e4AEaKPA9DvrwyOZxu7GRDQ=="], "@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.96.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-td1sbcvzsyuoNRiNdIRodPXRtFFwxzPpC/6/yIUtRRhKn30XQcizxupIvQQVpJWWchxkphbBDh6UN+u+2CJ8Zw=="], @@ -1983,21 +2049,21 @@ "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="], "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], "@radix-ui/colors": ["@radix-ui/colors@1.0.1", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="], @@ -2065,57 +2131,57 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], - "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], @@ -2133,23 +2199,23 @@ "@sentry/bundler-plugin-core": ["@sentry/bundler-plugin-core@4.6.0", "", { "dependencies": { "@babel/core": "^7.18.5", "@sentry/babel-plugin-component-annotate": "4.6.0", "@sentry/cli": "^2.57.0", "dotenv": "^16.3.1", "find-up": "^5.0.0", "glob": "^9.3.2", "magic-string": "0.30.8", "unplugin": "1.0.1" } }, "sha512-Fub2XQqrS258jjS8qAxLLU1k1h5UCNJ76i8m4qZJJdogWWaF8t00KnnTyp9TEDJzrVD64tRXS8+HHENxmeUo3g=="], - "@sentry/cli": ["@sentry/cli@2.58.5", "", { "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "2.58.5", "@sentry/cli-linux-arm": "2.58.5", "@sentry/cli-linux-arm64": "2.58.5", "@sentry/cli-linux-i686": "2.58.5", "@sentry/cli-linux-x64": "2.58.5", "@sentry/cli-win32-arm64": "2.58.5", "@sentry/cli-win32-i686": "2.58.5", "@sentry/cli-win32-x64": "2.58.5" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-tavJ7yGUZV+z3Ct2/ZB6mg339i08sAk6HDkgqmSRuQEu2iLS5sl9HIvuXfM6xjv8fwlgFOSy++WNABNAcGHUbg=="], + "@sentry/cli": ["@sentry/cli@2.58.6", "", { "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "2.58.6", "@sentry/cli-linux-arm": "2.58.6", "@sentry/cli-linux-arm64": "2.58.6", "@sentry/cli-linux-i686": "2.58.6", "@sentry/cli-linux-x64": "2.58.6", "@sentry/cli-win32-arm64": "2.58.6", "@sentry/cli-win32-i686": "2.58.6", "@sentry/cli-win32-x64": "2.58.6" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg=="], - "@sentry/cli-darwin": ["@sentry/cli-darwin@2.58.5", "", { "os": "darwin" }, "sha512-lYrNzenZFJftfwSya7gwrHGxtE+Kob/e1sr9lmHMFOd4utDlmq0XFDllmdZAMf21fxcPRI1GL28ejZ3bId01fQ=="], + "@sentry/cli-darwin": ["@sentry/cli-darwin@2.58.6", "", { "os": "darwin" }, "sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA=="], - "@sentry/cli-linux-arm": ["@sentry/cli-linux-arm@2.58.5", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm" }, "sha512-KtHweSIomYL4WVDrBrYSYJricKAAzxUgX86kc6OnlikbyOhoK6Fy8Vs6vwd52P6dvWPjgrMpUYjW2M5pYXQDUw=="], + "@sentry/cli-linux-arm": ["@sentry/cli-linux-arm@2.58.6", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm" }, "sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw=="], - "@sentry/cli-linux-arm64": ["@sentry/cli-linux-arm64@2.58.5", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm64" }, "sha512-/4gywFeBqRB6tR/iGMRAJ3HRqY6Z7Yp4l8ZCbl0TDLAfHNxu7schEw4tSnm2/Hh9eNMiOVy4z58uzAWlZXAYBQ=="], + "@sentry/cli-linux-arm64": ["@sentry/cli-linux-arm64@2.58.6", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm64" }, "sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g=="], - "@sentry/cli-linux-i686": ["@sentry/cli-linux-i686@2.58.5", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "ia32" }, "sha512-G7261dkmyxqlMdyvyP06b+RTIVzp1gZNgglj5UksxSouSUqRd/46W/2pQeOMPhloDYo9yLtCN2YFb3Mw4aUsWw=="], + "@sentry/cli-linux-i686": ["@sentry/cli-linux-i686@2.58.6", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "ia32" }, "sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg=="], - "@sentry/cli-linux-x64": ["@sentry/cli-linux-x64@2.58.5", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "x64" }, "sha512-rP04494RSmt86xChkQ+ecBNRYSPbyXc4u0IA7R7N1pSLCyO74e5w5Al+LnAq35cMfVbZgz5Sm0iGLjyiUu4I1g=="], + "@sentry/cli-linux-x64": ["@sentry/cli-linux-x64@2.58.6", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "x64" }, "sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q=="], - "@sentry/cli-win32-arm64": ["@sentry/cli-win32-arm64@2.58.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-AOJ2nCXlQL1KBaCzv38m3i2VmSHNurUpm7xVKd6yAHX+ZoVBI8VT0EgvwmtJR2TY2N2hNCC7UrgRmdUsQ152bA=="], + "@sentry/cli-win32-arm64": ["@sentry/cli-win32-arm64@2.58.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A=="], - "@sentry/cli-win32-i686": ["@sentry/cli-win32-i686@2.58.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-EsuboLSOnlrN7MMPJ1eFvfMDm+BnzOaSWl8eYhNo8W/BIrmNgpRUdBwnWn9Q2UOjJj5ZopukmsiMYtU/D7ml9g=="], + "@sentry/cli-win32-i686": ["@sentry/cli-win32-i686@2.58.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg=="], - "@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.58.5", "", { "os": "win32", "cpu": "x64" }, "sha512-IZf+XIMiQwj+5NzqbOQfywlOitmCV424Vtf9c+ep61AaVScUFD1TSrQbOcJJv5xGxhlxNOMNgMeZhdexdzrKZg=="], + "@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.58.6", "", { "os": "win32", "cpu": "x64" }, "sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA=="], "@sentry/core": ["@sentry/core@10.36.0", "", {}, "sha512-EYJjZvofI+D93eUsPLDIUV0zQocYqiBRyXS6CCV6dHz64P/Hob5NJQOwPa8/v6nD+UvJXvwsFfvXOHhYZhZJOQ=="], @@ -2175,7 +2241,7 @@ "@sigstore/bundle": ["@sigstore/bundle@4.0.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A=="], - "@sigstore/core": ["@sigstore/core@3.2.0", "", {}, "sha512-kxHrDQ9YgfrWUSXU0cjsQGv8JykOFZQ9ErNKbFPWzk3Hgpwu8x2hHrQ9IdA8yl+j9RTLTC3sAF3Tdq1IQCP4oA=="], + "@sigstore/core": ["@sigstore/core@3.2.1", "", {}, "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g=="], "@sigstore/protobuf-specs": ["@sigstore/protobuf-specs@0.5.1", "", {}, "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g=="], @@ -2183,7 +2249,7 @@ "@sigstore/tuf": ["@sigstore/tuf@4.0.2", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", "tuf-js": "^4.1.0" } }, "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ=="], - "@sigstore/verify": ["@sigstore/verify@3.1.0", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag=="], + "@sigstore/verify": ["@sigstore/verify@3.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA=="], "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], @@ -2197,109 +2263,93 @@ "@slack/socket-mode": ["@slack/socket-mode@1.3.6", "", { "dependencies": { "@slack/logger": "^3.0.0", "@slack/web-api": "^6.12.1", "@types/node": ">=12.0.0", "@types/ws": "^7.4.7", "eventemitter3": "^5", "finity": "^0.5.4", "ws": "^7.5.3" } }, "sha512-G+im7OP7jVqHhiNSdHgv2VVrnN5U7KY845/5EZimZkrD4ZmtV0P3BiWkgeJhPtdLuM7C7i6+M6h6Bh+S4OOalA=="], - "@slack/types": ["@slack/types@2.20.1", "", {}, "sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A=="], + "@slack/types": ["@slack/types@2.21.1", "", {}, "sha512-I8vmSjNYWsaxuWPx6dz4yeh0h7vRBWbgAMK14LEmblbZ404BtrPbXs6jDPx4cYgGf8msDGF4A9opLZBu21FViQ=="], "@slack/web-api": ["@slack/web-api@6.13.0", "", { "dependencies": { "@slack/logger": "^3.0.0", "@slack/types": "^2.11.0", "@types/is-stream": "^1.1.0", "@types/node": ">=12.0.0", "axios": "^1.7.4", "eventemitter3": "^3.1.0", "form-data": "^2.5.0", "is-electron": "2.2.2", "is-stream": "^1.1.0", "p-queue": "^6.6.1", "p-retry": "^4.0.0" } }, "sha512-dv65crIgdh9ZYHrevLU6XFHTQwTyDmNqEqzuIrV+Vqe/vgiG6w37oex5ePDU1RGm2IJ90H8iOvHFvzdEO/vB+g=="], - "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.5.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-HehAZr4sq2m+4zHgEqDvtWENy/B5yywMKA8Pl4gBcU3F4ekelpZqDLDxQHdJlguaKNyTq31cZYjLWomzdujQrA=="], - "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="], + "@smithy/core": ["@smithy/core@3.24.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Kt8phUg45M15EjhYAbZ+fFikYneijLu9Liugz8ZsYz2i8j0hzGv27LWKpEHYRfvj+LyCOSijpcR/2i8RouV+cA=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.4.15", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.4.0", "@smithy/util-middleware": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-BJdMBY5YO9iHh+lPLYdHv6LbX+J8IcPCYMl1IJdBt2KDWNHwONHrPVHk3ttYBqJd9wxv84wlbN0f7GlQzcQtNQ=="], - - "@smithy/core": ["@smithy/core@3.23.14", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg=="], - - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-tHhdiWZfG1ZIh2YcRfPJmY2gHcBmqbAzqm3ER4TIDFYsSEqTD5tICT7cgQ/kI8LRakxp12myOYyK68XPn7MnHw=="], "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="], - "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.13", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-M9rMkTar7JcRrvUHsK1271AuWDmrISIPQpQ4TSHmYZ4KMisGnMH0gfjCWnBwdndR7skvvp/UheHhZGvO3Cr8/g=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA=="], + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-lUwPPu7DNNVJjeS+gV7g2rDHbW9X1wSRQIsIyzOgBtP7KDMefLhz0kz42AWAxZIFPcOO3pUbtq76LSkVcxLKRw=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.13", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A=="], + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-QydEYKqvdiS6dJb0tOfDiogt12FzzImt2FnL7gMD72hNrkiUAUKqtStRmkTrdzDKFJ46abe3yH94luCuhtnCkQ=="], - "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.13", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-SK3VMeH0fibgdTg2QeB+O4p7Yy/2E5HBOHJeC58FshkDdeuX8lOgO7PfjYfLyPLP1ch55j91cQqKBzDS0mRjSQ=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.16", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ=="], + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-DNInwxNX32WtmhiKVrplzFtkKk5ePNHitJYPCnsPrD2EHm06iWJKQo8F8eq5ss94yp/xSfmojYD7nFBsgzrHHQ=="], - "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.14", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-rtQ5es8r/5v4rav7q5QTsfx9CtCyzrz/g7ZZZBH2xtMmd6G/KQrLOWfSHTvFOUPlVy59RQvxeBYJaLRoybMEyA=="], + "@smithy/hash-node": ["@smithy/hash-node@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-/tUIDaB36qjLq/CIhMRIiFXCT7rVGBGAhFmMA9PbC/iW2u3QPNATZuFSdK0JBO3qeSPoHBeudFMmsbFq2Mf5EQ=="], - "@smithy/hash-node": ["@smithy/hash-node@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA=="], + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-dLboKYf5ezU+b8SDDzVNjSHWHYPiU9aTI7IfIh9GhUpvCkwfdw1zUtK6dAGFHOrI5l1nVmsEWZrcAHophlNKug=="], - "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-WdQ7HwUjINXETeh6dqUeob1UHIYx8kAn9PSp1HhM2WWegiZBYVy2WXIs1lB07SZLan/udys9SBnQGt9MQbDpdg=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-c8C1GzrU4PcY1QT/HP0ILCTLutyVONT93kPSisOyHoZaXlKQZtV6+RKqolhBtPolGULf59vq2yseagU6+WY82w=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg=="], + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-AzWk7NstKv+z3h0GmZlQkDdgcnh3tvBWnBr0zoBY/agV/zaMqEBnpqgF1S+sJAy5yfE1b2KZqiz+uHHV70vOYg=="], - "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], + "@smithy/md5-js": ["@smithy/md5-js@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-U/zFWFDuNFspkLAtUbatmpevrRjXwQkoGPJTg1hapUsjLKK+aN3u4seX4+aSBzLom+RnZSdWncfSIgG100vsGg=="], - "@smithy/md5-js": ["@smithy/md5-js@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-cNm7I9NXolFxtS20ojROddOEpSAeI1Obq6pd1Kj5HtHws3s9Fkk8DdHDfQSs5KuxCewZuVK6UqrJnfJmiMzDuQ=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-lzOzJ4c0t3vkBut02CjdWNgduN3mUWjc1WK9TPr75KVV6OgVWico9wMDn9ZnQN97VJPYfweBW6Dm5CElvQl8BQ=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.5.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-8DnkSoUMQAcuT/DHdigsFPti8M/Dm6TPCAsrIQ/bUDGxRkrgGuI++3dXRr8CoUyc9r0kGSCcZHjJje407ydgBQ=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.29", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/middleware-serde": "^4.2.17", "@smithy/node-config-provider": "^4.3.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-middleware": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.6.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-fumMIfh5xOFjirylbSzmBX9bgQtrWFtQrosPfkjsJSBzqXVbQMNDGIC8oJBz4V3bokIm2F0CL3bziLtbXR7cbA=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.5.1", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/protocol-http": "^5.3.13", "@smithy/service-error-classification": "^4.2.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-middleware": "^4.2.13", "@smithy/util-retry": "^4.3.1", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-/zY+Gp7Qj2D2hVm3irkCyONER7E9MiX3cUUm/k2ZmhkzZkrPgwVS4aJ5NriZUEN/M0D1hhjrgjUmX04HhRwdWA=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-+7glfRrb7byruZCPAM53TvmK8cx/ghzAThB4EvPzHynAYobtISl0g+DzzSVEC0NQob5BunP9gC9GP+Fcz6H9yw=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.17", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-Yj4wjBQZXHePRIy9cBIKfCOn/kPjRlgDPGlr7DjIhwrnz8kWu7Ux7UwPr51P/wcug5oq4nWdBXSY4TV5afBdew=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-c2G9QJ4xVZLwAkAf+WQESSSCkKbtt33ytje1klGvTcBn6cKuqV28E+62wbRPHwuTikkB3LQ7CBnNrayCoJur5A=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.13", "", { "dependencies": { "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-3dA9TQ+ybRSZ/m0wnbZhiBy4Dezjgq1Ib/ZZrYTpJDBgpoLLU/SDzZc/g0x0MNAdOJe1wPcM+x2PBRmoOur+Sw=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.2", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA=="], + "@smithy/property-provider": ["@smithy/property-provider@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-QNc22/FgfEm/9/rkefShfQUVckH3HWiQ2RPs+40hwAdY65hbg88gombeHwkfMzmVDZjolcyQeyOjnxZRmpavIA=="], - "@smithy/property-provider": ["@smithy/property-provider@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-jOD+4WNWQLntiLJn3r82C7BLheEbRCKTbU5U5bskZmT7nwRiGkh0IghuHwHRZ1ZEFXpHltQxxp9/koOPsdluJg=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.3.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.5.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-W7IPDXj8AZdyH5EWEXmOvN7ao8iN0JKJ0FNLpGcqj08HZc0MmqGcJnGgh3DfUdGYtzrPIEudxs+ovq/EWZgLjg=="], - "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-QBJKWGqIknH0dc9LWpfH1mkdokAx6iXYN3UcQ3eY6uIEyScuoQAhfl94ge7ozUy9WgFUdE8xsvwBjaYBbWmPNA=="], - "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.13.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-pg9QRQESz3m/5HgAW/z9lA3ln8MSsCWNWc82MX40Djlxpcj/+7DZQ0yIk7tGWYJCVZog/9LBdNl1uEVRAhqm5Q=="], - "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0" } }, "sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw=="], + "@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.8", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw=="], + "@smithy/url-parser": ["@smithy/url-parser@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-f7kUYrRdLiAHz10WXQXiUkuBFaL2c2ZBD2kSwZyQBh73lWFTvXwdpS9l5irQ/uldk8YMJpm66BozmqCg/3uZvA=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.3.13", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg=="], + "@smithy/util-base64": ["@smithy/util-base64@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-2J8l+DoX3IIiP75X5SYkJ3mIgOkxW29MxOs7oPjbXLuInQ7UL6zLw2IJHbQ44+eKDBBhTjvt+GgwsTTNBGt8zA=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.12.9", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-stack": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" } }, "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ=="], + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-nQtYwXg4spM6uc0Luq3yck+WXZ1VPfrYkC2SqkQ+YOGks0qR2bKKlSCjidSqfpq+VAY/RJe1O5V+CtBmnT63KQ=="], - "@smithy/types": ["@smithy/types@4.14.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ=="], + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-BAsAed9yWExECwNIi61Le6D8ZTY71MFEFrf3d4L2+uzcbTjFAWxOtymkA1vCV8bNZQN9TGgZo4c68JDsnjNShA=="], - "@smithy/url-parser": ["@smithy/url-parser@4.2.13", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw=="], + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-CthqHx5VTlNIsS5rJni+pIfkGgYPnVFsy9qYiv8e+hMQDPemZod5wTa+2DkrI+vubX51sD6qqcgH3UHqdTf2bw=="], - "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-7yflDiFlO+bVXjI7BJe3B8jx5HyGCI146xrkZRwK9pO2ParfgWzgGfPGK3KsXkxcU+EBzIz1kFnX7fJRxAMbQA=="], - "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-LbE6AGHhQOunqIN5UyWDMgpPwmUHUzrV2NtUOQ+lt6Stpipzo6S7uDyeGtO0GGgUD1balEPCNu8Xfl1AQNiruQ=="], - "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-72gNpNDQ2iIGbaNmeaF9I58shWsEuD5tNI7my5uXlm1CSPH5i8IKI/nzU50qqB8y+kgw/qTLGgsf0We5qeM/aA=="], - "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.5.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-NJhe8KmNjeZ7V+gJsQR5xw0IN47N8pBKosed40xfhelDuYkg8VQ5CVGDcHTEuJq3e3zQb21vnoOOReQothejhA=="], - "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-+ip3QrXGjDOzV/ciNWPTm6bhJuXjmzugMR19ouXgA26QqhEo0zuXM7pvYE9S4VfX13YmPgSYDPkF4+2bPqIwAg=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.45", "", { "dependencies": { "@smithy/property-provider": "^4.2.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-N1IR4bMHIDbqO3GxkJHgqNGsnrd7MNrj+EVqhFqKeRqSBV5I3KCjNllKfnbF9KV0YteGhfLqcMR5CYsPLJqpqw=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.50", "", { "dependencies": { "@smithy/config-resolver": "^4.4.15", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-xpjncL5XozFA3No7WypTsPU1du0fFS8flIyO+Wh2nhCy7bpEapvU7BR55Bg+wrfw+1cRA+8G8UsTjaxgzrMzXg=="], + "@smithy/util-retry": ["@smithy/util-retry@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-W9Ovy9i02yGqtLlpqZNQuXNxXc5OPfXujnembxN/FxyBtGjJd8vKY0PQYEJ8FNybTOcXG+ZxsSsX23HOb3zQzg=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.4.0", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-QQHGPKkw6NPcU6TJ1rNEEa201srPtZiX4k61xL163vvs9sTqW/XKz+UEuJ00uvPqoN+5Rs4Ka1UJ7+Mp03IXJw=="], - - "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], - - "@smithy/util-middleware": ["@smithy/util-middleware@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow=="], - - "@smithy/util-retry": ["@smithy/util-retry@4.3.1", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FwmicpgWOkP5kZUjN3y+3JIom8NLGqSAJBeoIgK0rIToI817TEBHCrd0A2qGeKQlgDeP+Jzn4i0H/NLAXGy9uQ=="], - - "@smithy/util-stream": ["@smithy/util-stream@4.5.22", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.16", "@smithy/node-http-handler": "^4.5.2", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew=="], - - "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], + "@smithy/util-stream": ["@smithy/util-stream@4.6.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-PFzBVEBP5k8R+mK/c+VAKmtpUTL+KzBIXWJ6oM0GWOb31K+QgymXV9IW03XLPM1wtkC7oAb9ZBN2aswSSVbNFg=="], "@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@smithy/util-waiter": ["@smithy/util-waiter@4.2.15", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-oUt9o7n8hBv3BL56sLSneL0XeigZSuem0Hr78JaoK33D9oKieyCvVP8eTSe3j7g2mm/S1DvzxKieG7JEWNJUNg=="], - - "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], + "@smithy/util-waiter": ["@smithy/util-waiter@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-EYviebytZE6vplW0AGwZ2Rc3sNuVR83lfUCNZu11VchUiKhMwJqrRWy7iVDTNEwG/vEwItno591Iad6/prj6Bw=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], @@ -2335,7 +2385,7 @@ "@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ=="], - "@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="], + "@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12", "solid-start": "*" }, "optionalPeers": ["@tauri-apps/plugin-store", "solid-start"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="], "@solid-primitives/timer": ["@solid-primitives/timer@1.4.4", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ayjyb3+v1hyU92vuLUN0tVHq2mmTCPGxSDLGJMsDydRqx9ZfJIc9xj6cxK4XvdY3pif3ps2mIv52pjgToybEpQ=="], @@ -2359,29 +2409,29 @@ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], - "@storybook/addon-a11y": ["@storybook/addon-a11y@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.3.5" } }, "sha512-5k6lpgfIeLxvNhE8v3wEzdiu73ONKjF4gmH1AHvfqYd8kIVzQJai0KCDxgvqNncXHQhIWkaf1fg6+9hKaYJyaw=="], + "@storybook/addon-a11y": ["@storybook/addon-a11y@10.4.1", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.4.1" } }, "sha512-MGft/IXjJ20a9KbaSVG9bHTAAoanbucKrgEiJJRNqpim8DsXA01+XTdSk17LmiOCB203Rrq9mWgdQ6+79cc8iA=="], - "@storybook/addon-docs": ["@storybook/addon-docs@10.3.5", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.3.5", "@storybook/icons": "^2.0.1", "@storybook/react-dom-shim": "10.3.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.3.5" } }, "sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA=="], + "@storybook/addon-docs": ["@storybook/addon-docs@10.4.1", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.4.1", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.4.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.4.1" }, "optionalPeers": ["@types/react"] }, "sha512-IYqUdjoZe4VO2LFZlKL/gwy7DsQSWCq6hX+zc1MBmZo04yycDASk1tte57n9pdlW3ajw9yYMF/+lVBi+xQjyvw=="], - "@storybook/addon-links": ["@storybook/addon-links@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.3.5" }, "optionalPeers": ["react"] }, "sha512-Xe2wCGZ+hpZ0cDqAIBHk+kPc8nODNbu585ghd5bLrlYJMDVXoNM/fIlkrLgjIDVbfpgeJLUEg7vldJrn+FyOLw=="], + "@storybook/addon-links": ["@storybook/addon-links@10.4.1", "", { "dependencies": { "@storybook/global": "^5.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.4.1" }, "optionalPeers": ["@types/react", "react"] }, "sha512-h/5D23GwMuHA55sB7XDyhByF9psF7UFmaQOn72pjNAarew5eOpue5A+jXk3AKEYokHbvgQaoz+FrvWo9GEfSKQ=="], - "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.3.5", "", { "peerDependencies": { "storybook": "^10.3.5" } }, "sha512-s3/gIy9Tqxji27iclLY+KSk8kGeow1JxXMl1lPLyu8n6XVvv+tFrUPhAvUTs+fVenG6JQEWc0uzpYBdFRWbMtw=="], + "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.4.1", "", { "peerDependencies": { "storybook": "^10.4.1" } }, "sha512-XJ3vaPeXLc8GRrnYKoi0zmAMyT34XTnD6SZNcSV0ceEQrmfZKpLbn6wei1e4oqQRctkRH2QFl/ha7SqqB3yYmQ=="], - "@storybook/addon-vitest": ["@storybook/addon-vitest@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.3.5", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-PQDeeMwoF55kvzlhFqVKOryBJskkVk71AbDh7F0y8PdRRxlGbTvIUkKXktHZWBdESo0dV6BkeVxGQ4ZpiFxirg=="], + "@storybook/addon-vitest": ["@storybook/addon-vitest@10.4.1", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.4.1", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-ymrX9EOou1x3d21iDhjP3j3XfhOAiflhlPZWKcipULBoJCq/aZPbV68EghzovkJNuGRl9ezMYxbbKxwrMmCmGg=="], - "@storybook/builder-vite": ["@storybook/builder-vite@10.3.5", "", { "dependencies": { "@storybook/csf-plugin": "10.3.5", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.3.5", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw=="], + "@storybook/builder-vite": ["@storybook/builder-vite@10.4.1", "", { "dependencies": { "@storybook/csf-plugin": "10.4.1", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.4.1", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-/oyQrXoNOqN8SW5hNnYP+I1uvgFxKxWXj/EP6NXYzc5SQwImofgru+D2+6gDhL0+Q//+Hx05DJoQO2omvUJ8bQ=="], - "@storybook/csf-plugin": ["@storybook/csf-plugin@10.3.5", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.3.5", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw=="], + "@storybook/csf-plugin": ["@storybook/csf-plugin@10.4.1", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.4.1", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-WdPepGBxDGOUDjYd8KxMtcf+us/2PAcnBczl77XtrnxxHNs0jWesxKkiJ9yiuGrge4BPhDeAj6rxjbBoaHxLBA=="], "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], - "@storybook/icons": ["@storybook/icons@2.0.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg=="], + "@storybook/icons": ["@storybook/icons@2.0.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-KZBCpXsshAIjczYNXR/rlxEtCUX/eAbpFNwKi8bcOomrLA4t/SyPz5RF+lVPO2oZBUE4sAkt43mfJUevQDSEEw=="], - "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.3.5", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.3.5" } }, "sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g=="], + "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.4.1", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.4.1" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-6QFqfDNH4DMrt7yHKRfpqRopsVUc/Az+sXIdJ39IetYnHUxL3nW4NVaPc6uy/8Qi8urzUyEXL/nn7cpSIP2aPQ=="], "@stripe/stripe-js": ["@stripe/stripe-js@8.6.1", "", {}, "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA=="], - "@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], + "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], @@ -2425,12 +2475,6 @@ "@tanstack/solid-query": ["@tanstack/solid-query@5.91.4", "", { "dependencies": { "@tanstack/query-core": "5.91.2" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-oCEgn8iT7WnF/7ISd7usBpUK1C9EdvQfg8ZUpKNKZ4edVClICZrCX6f3/Bp8ZlwQnL21KLc2rp+CejEuehlRxg=="], - "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="], - - "@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A=="], - - "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], - "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], @@ -2447,7 +2491,7 @@ "@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], @@ -2531,8 +2575,6 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/mssql": ["@types/mssql@9.1.11", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-vcujgrDbDezCxNDO4KY6gjwduLYOKfrexpRUwhoysRvcXZ3+IgZ/PMYFDgh8c3cQIxZ6skAwYo+H6ibMrBWPjQ=="], - "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], @@ -2557,14 +2599,12 @@ "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], - "@types/qs": ["@types/qs@6.15.0", "", {}, "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow=="], + "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], "@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], - "@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="], - "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], @@ -2629,7 +2669,7 @@ "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.5", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], "@upstash/redis": ["@upstash/redis@1.38.0", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg=="], @@ -2641,17 +2681,17 @@ "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - "@vitest/mocker": ["@vitest/mocker@4.1.4", "", { "dependencies": { "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg=="], + "@vitest/mocker": ["@vitest/mocker@4.1.7", "", { "dependencies": { "@vitest/spy": "4.1.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.4", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.7", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw=="], - "@vitest/runner": ["@vitest/runner@4.1.4", "", { "dependencies": { "@vitest/utils": "4.1.4", "pathe": "^2.0.3" } }, "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ=="], + "@vitest/runner": ["@vitest/runner@4.1.7", "", { "dependencies": { "@vitest/utils": "4.1.7", "pathe": "^2.0.3" } }, "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "@vitest/utils": "4.1.4", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.7", "", { "dependencies": { "@vitest/pretty-format": "4.1.7", "@vitest/utils": "4.1.7", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw=="], "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - "@vitest/utils": ["@vitest/utils@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw=="], + "@vitest/utils": ["@vitest/utils@4.1.7", "", { "dependencies": { "@vitest/pretty-format": "4.1.7", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw=="], "@volar/kit": ["@volar/kit@2.4.28", "", { "dependencies": { "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg=="], @@ -2673,7 +2713,7 @@ "@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="], - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], "@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="], @@ -2699,7 +2739,7 @@ "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -2715,7 +2755,7 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + "ansis": ["ansis@4.3.1", "", {}, "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA=="], "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], @@ -2789,17 +2829,17 @@ "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], - "axe-core": ["axe-core@4.11.3", "", {}, "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg=="], + "axe-core": ["axe-core@4.11.4", "", {}, "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA=="], - "axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="], + "axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="], + "b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.6", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], @@ -2809,23 +2849,23 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], + "bare-events": ["bare-events@2.8.3", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw=="], - "bare-fs": ["bare-fs@4.7.0", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA=="], + "bare-fs": ["bare-fs@4.7.1", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw=="], - "bare-os": ["bare-os@3.8.7", "", {}, "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w=="], + "bare-os": ["bare-os@3.9.1", "", {}, "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ=="], "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], - "bare-stream": ["bare-stream@2.13.0", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA=="], + "bare-stream": ["bare-stream@2.13.1", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow=="], - "bare-url": ["bare-url@2.4.0", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA=="], + "bare-url": ["bare-url@2.4.3", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ=="], "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw=="], "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], @@ -2835,19 +2875,17 @@ "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], - "bin-links": ["bin-links@6.0.0", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w=="], + "bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="], "binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="], - "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], - "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], + "body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="], "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], @@ -2861,7 +2899,7 @@ "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -2915,7 +2953,7 @@ "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001788", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -2957,8 +2995,6 @@ "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], - "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], @@ -2975,7 +3011,7 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="], "cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="], @@ -3087,7 +3123,7 @@ "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], @@ -3103,8 +3139,6 @@ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], @@ -3137,7 +3171,7 @@ "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], - "devalue": ["devalue@5.7.1", "", {}, "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA=="], + "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], @@ -3215,13 +3249,13 @@ "electron-is-dev": ["electron-is-dev@3.0.1", "", {}, "sha512-8TjjAh8Ec51hUi3o4TaU0mD3GMTOESi866oRNavj9A3IQJ7pmv+MJVmdZBFGw4GFT36X7bkqnuDNYvkQgvyI8Q=="], - "electron-log": ["electron-log@5.4.3", "", {}, "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ=="], + "electron-log": ["electron-log@5.4.4", "", {}, "sha512-istWgaXjBfURBSS8LWVW9C3jsc6+ac+tY1lXrQEOTp0lVj+a4OlO1Tmqb36GgnEUDv92DGC9VI1HNXwJinWpgA=="], "electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], "electron-store": ["electron-store@10.1.0", "", { "dependencies": { "conf": "^14.0.0", "type-fest": "^4.41.0" } }, "sha512-oL8bRy7pVCLpwhmXy05Rh/L6O93+k9t6dqSw0+MckIc3OmCTZm6Mp04Q4f/J0rtu84Ky6ywkR8ivtGOmrq+16w=="], - "electron-to-chromium": ["electron-to-chromium@1.5.336", "", {}, "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.364", "", {}, "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw=="], "electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="], @@ -3239,17 +3273,15 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="], + "engine.io-client": ["engine.io-client@6.6.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.20.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg=="], "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], + "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], - "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], @@ -3271,7 +3303,7 @@ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -3323,7 +3355,7 @@ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], @@ -3333,9 +3365,9 @@ "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], - "express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="], + "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], "expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="], @@ -3355,8 +3387,6 @@ "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], - "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], - "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -3367,13 +3397,13 @@ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - "fast-json-stringify": ["fast-json-stringify@6.3.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA=="], + "fast-json-stringify": ["fast-json-stringify@6.4.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ=="], "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], - "fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="], + "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], "fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="], @@ -3397,7 +3427,7 @@ "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], - "find-my-way": ["find-my-way@9.5.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="], + "find-my-way": ["find-my-way@9.6.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ=="], "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], @@ -3407,7 +3437,7 @@ "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], - "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], "fontace": ["fontace@0.3.1", "", { "dependencies": { "@types/fontkit": "^2.0.8", "fontkit": "^2.0.4" } }, "sha512-9f5g4feWT1jWT8+SbL85aLIRLIXUaDygaM2xPXRmzPYxrOMNok79Lr3FGJoKVNKibE0WCunNiEVG2mwuE+2qEg=="], @@ -3461,7 +3491,7 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -3475,7 +3505,7 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - "get-tsconfig": ["get-tsconfig@4.13.8", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-J87BxkLXykmisLQ+KA4x2+O6rVf+PJrtFUO8lGyiRg4lyxJLJ8/v0sRAKdVZQOy6tR6lMRAF1NqzCf9BQijm0w=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#20bd361", {}, "anomalyco-ghostty-web-20bd361", "sha512-dW0nwaiBBcun9y5WJSvm3HxDLe5o9V0xLCndQvWonRVubU8CS1PHxZpLffyPt1YujPWC13ez03aWxcuKBPYYGQ=="], @@ -3507,7 +3537,7 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphql": ["graphql@16.13.2", "", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="], + "graphql": ["graphql@16.14.0", "", {}, "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q=="], "graphql-request": ["graphql-request@6.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="], @@ -3531,7 +3561,7 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], @@ -3585,7 +3615,7 @@ "hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="], - "hosted-git-info": ["hosted-git-info@9.0.2", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg=="], + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], @@ -3639,8 +3669,6 @@ "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], @@ -3655,9 +3683,9 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="], + "ioredis": ["ioredis@5.11.0", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -3685,7 +3713,7 @@ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], @@ -3715,8 +3743,6 @@ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], @@ -3745,8 +3771,6 @@ "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], @@ -3777,7 +3801,7 @@ "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="], @@ -3785,9 +3809,7 @@ "js-beautify": ["js-beautify@1.15.4", "", { "dependencies": { "config-chain": "^1.1.13", "editorconfig": "^1.0.4", "glob": "^10.4.2", "js-cookie": "^3.0.5", "nopt": "^7.2.1" }, "bin": { "css-beautify": "js/bin/css-beautify.js", "html-beautify": "js/bin/html-beautify.js", "js-beautify": "js/bin/js-beautify.js" } }, "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA=="], - "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], - - "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], + "js-cookie": ["js-cookie@3.0.8", "", {}, "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -3893,14 +3915,10 @@ "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], - "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], - "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], - "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], @@ -3915,8 +3933,6 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - "loglevelnext": ["loglevelnext@6.0.0", "", {}, "sha512-FDl1AI2sJGjHHG3XKJd6sG3/6ncgiGCQ0YkW46nxe7SfqQq6hujd9CvFXIXtkGBUN83KPZ2KSOJK8q5P0bSSRQ=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -3931,7 +3947,7 @@ "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], - "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], @@ -3945,7 +3961,7 @@ "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], - "make-fetch-happen": ["make-fetch-happen@15.0.5", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg=="], + "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], @@ -4139,8 +4155,6 @@ "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], - "mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="], - "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], @@ -4157,9 +4171,7 @@ "nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="], + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], @@ -4173,7 +4185,7 @@ "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], - "node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="], + "node-abi": ["node-abi@4.31.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], @@ -4185,7 +4197,7 @@ "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], - "node-gyp": ["node-gyp@12.2.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^15.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ=="], + "node-gyp": ["node-gyp@12.3.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg=="], "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], @@ -4195,7 +4207,7 @@ "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], - "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="], "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], @@ -4221,7 +4233,7 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + "nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -4249,9 +4261,9 @@ "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], - "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], - "oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="], + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], @@ -4269,12 +4281,14 @@ "opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="], - "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], "oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="], + "oxc-parser": ["oxc-parser@0.127.0", "", { "dependencies": { "@oxc-project/types": "^0.127.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.127.0", "@oxc-parser/binding-android-arm64": "0.127.0", "@oxc-parser/binding-darwin-arm64": "0.127.0", "@oxc-parser/binding-darwin-x64": "0.127.0", "@oxc-parser/binding-freebsd-x64": "0.127.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", "@oxc-parser/binding-linux-arm64-musl": "0.127.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-musl": "0.127.0", "@oxc-parser/binding-openharmony-arm64": "0.127.0", "@oxc-parser/binding-wasm32-wasi": "0.127.0", "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA=="], + + "oxc-resolver": ["oxc-resolver@11.20.0", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.20.0", "@oxc-resolver/binding-android-arm64": "11.20.0", "@oxc-resolver/binding-darwin-arm64": "11.20.0", "@oxc-resolver/binding-darwin-x64": "11.20.0", "@oxc-resolver/binding-freebsd-x64": "11.20.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.20.0", "@oxc-resolver/binding-linux-arm-musleabihf": "11.20.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.20.0", "@oxc-resolver/binding-linux-arm64-musl": "11.20.0", "@oxc-resolver/binding-linux-ppc64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-musl": "11.20.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-musl": "11.20.0", "@oxc-resolver/binding-openharmony-arm64": "11.20.0", "@oxc-resolver/binding-wasm32-wasi": "11.20.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.20.0", "@oxc-resolver/binding-win32-x64-msvc": "11.20.0" } }, "sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g=="], + "oxc-transform": ["oxc-transform@0.96.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm64": "0.96.0", "@oxc-transform/binding-darwin-arm64": "0.96.0", "@oxc-transform/binding-darwin-x64": "0.96.0", "@oxc-transform/binding-freebsd-x64": "0.96.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.96.0", "@oxc-transform/binding-linux-arm64-gnu": "0.96.0", "@oxc-transform/binding-linux-arm64-musl": "0.96.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.96.0", "@oxc-transform/binding-linux-s390x-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-musl": "0.96.0", "@oxc-transform/binding-wasm32-wasi": "0.96.0", "@oxc-transform/binding-win32-arm64-msvc": "0.96.0", "@oxc-transform/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dQPNIF+gHpSkmC0+Vg9IktNyhcn28Y8R3eTLyzn52UNymkasLicl3sFAtz7oEVuFmCpgGjaUTKkwk+jW2cHpDQ=="], "oxlint": ["oxlint@1.60.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.60.0", "@oxlint/binding-android-arm64": "1.60.0", "@oxlint/binding-darwin-arm64": "1.60.0", "@oxlint/binding-darwin-x64": "1.60.0", "@oxlint/binding-freebsd-x64": "1.60.0", "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", "@oxlint/binding-linux-arm-musleabihf": "1.60.0", "@oxlint/binding-linux-arm64-gnu": "1.60.0", "@oxlint/binding-linux-arm64-musl": "1.60.0", "@oxlint/binding-linux-ppc64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-musl": "1.60.0", "@oxlint/binding-linux-s390x-gnu": "1.60.0", "@oxlint/binding-linux-x64-gnu": "1.60.0", "@oxlint/binding-linux-x64-musl": "1.60.0", "@oxlint/binding-openharmony-arm64": "1.60.0", "@oxlint/binding-win32-arm64-msvc": "1.60.0", "@oxlint/binding-win32-ia32-msvc": "1.60.0", "@oxlint/binding-win32-x64-msvc": "1.60.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw=="], @@ -4381,7 +4395,7 @@ "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], - "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], @@ -4391,11 +4405,11 @@ "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], - "poe-oauth": ["poe-oauth@0.0.6", "", {}, "sha512-dI8xrVl7RSFh0B+cb4GGuCjIfGtDT9VpbpVkP0UKcunpXF0eFw+6GencoJ7k+E02ZYqopBQApMVWGq70/GP69w=="], + "poe-oauth": ["poe-oauth@0.0.8", "", {}, "sha512-zlaRVLR6vuxBIYUkZoTIVo3f8h3qd27gv9Ms+kmGiYEiiV4TdccddTdNcGyI0DnuJ9tVi+5LP3Bvzez59IFbjw=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "postcss-css-variables": ["postcss-css-variables@0.18.0", "", { "dependencies": { "balanced-match": "^1.0.0", "escape-string-regexp": "^1.0.3", "extend": "^3.0.1" }, "peerDependencies": { "postcss": "^8.2.6" } }, "sha512-lYS802gHbzn1GI+lXvy9MYIYDuGnl1WB4FTKoqMQqJ3Mab09A7a/1wZvGTkCEZJTM8mSbIyb1mJYn8f0aPye0Q=="], @@ -4453,7 +4467,7 @@ "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], - "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -4467,7 +4481,7 @@ "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], - "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], + "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -4603,8 +4617,6 @@ "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], - "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], @@ -4627,7 +4639,7 @@ "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], - "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], + "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -4639,7 +4651,7 @@ "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -4647,7 +4659,7 @@ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - "safe-regex2": ["safe-regex2@5.1.0", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw=="], + "safe-regex2": ["safe-regex2@5.1.1", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], @@ -4665,7 +4677,7 @@ "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], @@ -4713,7 +4725,7 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "sigstore": ["sigstore@4.1.0", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/sign": "^4.1.0", "@sigstore/tuf": "^4.0.1", "@sigstore/verify": "^3.1.0" } }, "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA=="], + "sigstore": ["sigstore@4.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/sign": "^4.1.1", "@sigstore/tuf": "^4.0.2", "@sigstore/verify": "^3.1.1" } }, "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w=="], "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], @@ -4735,7 +4747,7 @@ "socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="], - "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], @@ -4775,7 +4787,7 @@ "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], @@ -4811,19 +4823,19 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="], - "storybook": ["storybook@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw=="], + "storybook": ["storybook@10.4.1", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-V1Zd2e+gBFufqAQVZ1JR8KLqALsEZ3JYSBnWwQbKa6zCfWWanR6AFMyuOkLt2gZOgGp3h2Riuz88pGNVTQSG0A=="], - "storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.12", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", "@storybook/builder-vite": "^10.3.1", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.11" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": "^4.0.0 || ^5.0.0 || ^6.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["typescript"] }, "sha512-KfKhJRdxbhFLHkBzLKSEk5sO2M/+KV9cdpki5Xdl5pwNP8kcoQnZ3b/okZk8dMRV6x19j86bKc7zDfc5bPSMwA=="], + "storybook-solidjs-vite": ["storybook-solidjs-vite@10.1.1", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", "@storybook/builder-vite": "^10.4.0", "@storybook/global": "^5.0.0", "semver": "7.8.1" }, "peerDependencies": { "@solidjs/web": "^2.0.0-0", "solid-js": "^1.8.0-0 || ^2.0.0-0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": "^4.0.0 || ^5.0.0 || ^6.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vite-plugin-solid": "^2.0.0-0 || ^3.0.0-0" }, "optionalPeers": ["@solidjs/web", "typescript"] }, "sha512-4acj1yxVPM3PieEGFPJukPeIXmpboJprewiX0KMrdYvtAZy8zbkZ7QBf8iENyKNJOayeXWzMm+z7hWBQDUirYg=="], "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], - "streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="], + "streamx": ["streamx@2.26.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-VvNG1K72Po/xwJzxZFnZ++Tbrv4lwSptsbkFuzXCJAYZvCK5nnxsvXU6ajqkv7chyiI1Y0YXq2Jh8Iy8Y7NF/A=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -4877,15 +4889,11 @@ "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], - "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], + "tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], - "tar-stream": ["tar-stream@3.1.8", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ=="], - - "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], - - "tedious": ["tedious@19.2.1", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.5", "@types/node": ">=18", "bl": "^6.1.4", "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA=="], + "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="], "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], @@ -4895,7 +4903,7 @@ "terracotta": ["terracotta@1.1.0", "", { "dependencies": { "solid-use": "^0.9.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww=="], - "terser": ["terser@5.46.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ=="], + "terser": ["terser@5.48.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q=="], "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], @@ -4903,7 +4911,7 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], + "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], @@ -4919,7 +4927,7 @@ "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], @@ -4927,13 +4935,13 @@ "titleize": ["titleize@4.0.0", "", {}, "sha512-ZgUJ1K83rhdu7uh7EHAC2BgY5DzoX8V5rTvoWI4vFysggi6YjLe5gUXABPWAU7VkvGP7P/0YiWq+dcPeYDsf1g=="], - "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="], "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], + "toad-cache": ["toad-cache@3.7.1", "", {}, "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -5003,7 +5011,7 @@ "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="], "typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="], @@ -5011,7 +5019,7 @@ "typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="], - "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], @@ -5037,10 +5045,6 @@ "unifont": ["unifont@0.5.2", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg=="], - "unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="], - - "unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="], - "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -5095,7 +5099,7 @@ "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], - "valibot": ["valibot@1.3.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="], + "valibot": ["valibot@1.4.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g=="], "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], @@ -5123,7 +5127,7 @@ "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], - "vitest": ["vitest@4.1.4", "", { "dependencies": { "@vitest/expect": "4.1.4", "@vitest/mocker": "4.1.4", "@vitest/pretty-format": "4.1.4", "@vitest/runner": "4.1.4", "@vitest/snapshot": "4.1.4", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.4", "@vitest/browser-preview": "4.1.4", "@vitest/browser-webdriverio": "4.1.4", "@vitest/coverage-istanbul": "4.1.4", "@vitest/coverage-v8": "4.1.4", "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg=="], + "vitest": ["vitest@4.1.7", "", { "dependencies": { "@vitest/expect": "4.1.7", "@vitest/mocker": "4.1.7", "@vitest/pretty-format": "4.1.7", "@vitest/runner": "4.1.7", "@vitest/snapshot": "4.1.7", "@vitest/spy": "4.1.7", "@vitest/utils": "4.1.7", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.7", "@vitest/browser-preview": "4.1.7", "@vitest/browser-webdriverio": "4.1.7", "@vitest/coverage-istanbul": "4.1.7", "@vitest/coverage-v8": "4.1.7", "@vitest/ui": "4.1.7", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA=="], "volar-service-css": ["volar-service-css@0.0.70", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw=="], @@ -5161,8 +5165,6 @@ "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], @@ -5171,7 +5173,7 @@ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "webpack-sources": ["webpack-sources@3.4.0", "", {}, "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ=="], + "webpack-sources": ["webpack-sources@3.5.0", "", {}, "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.5.0", "", {}, "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw=="], @@ -5191,7 +5193,7 @@ "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], - "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + "which-typed-array": ["which-typed-array@1.1.21", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw=="], "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], @@ -5219,7 +5221,7 @@ "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], - "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], @@ -5231,7 +5233,7 @@ "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yaml-language-server": ["yaml-language-server@1.20.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA=="], @@ -5259,7 +5261,7 @@ "zod-openapi": ["zod-openapi@5.4.6", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-P2jsOOBAq/6hCwUsMCjUATZ8szkMsV5VAwZENfyxp2Hc/XPJQpVwAgevWZc65xZauCwWB9LAn7zYeiCJFAEL+A=="], - "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], "zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], @@ -5277,7 +5279,7 @@ "@actions/github/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@actions/http-client/undici": ["undici@6.25.0", "", {}, "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg=="], + "@actions/http-client/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="], "@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], @@ -5299,9 +5301,25 @@ "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@ai-sdk/deepgram/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + + "@ai-sdk/elevenlabs/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + + "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z9MC6M4Oh/yUY/F/eszOtO8wc2nMz99XmZQKd2gWTtyIfe716xTfrKe3aYZKg20NZDtyjqPPKPSR+wqz7q1T7Q=="], + + "@ai-sdk/fireworks/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], "@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], @@ -5345,89 +5363,65 @@ "@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@astrojs/sitemap/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@astrojs/sitemap/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@astrojs/solid-js/vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], - "@astrojs/solid-js/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], - "@astrojs/starlight/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-crypto/sha1-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-crypto/sha1-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-crypto/sha256-js/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-crypto/sha256-js/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-crypto/util/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-crypto/util/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-sdk/client-athena/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="], - - "@aws-sdk/client-athena/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="], - - "@aws-sdk/client-athena/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="], - - "@aws-sdk/client-athena/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - "@aws-sdk/client-athena/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.30", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-ini": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-DcDXAl32I/YZnJ9LyX/XpRXOtoTYIwgmYxoNMGkyvtomdjPpkXPGhz93VJyzKFFNffz/SZwEkoAuWOkeOzo90Q=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.15", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-4Org8yUew+Y1+buTcH5A39unAdkVRnQxcOp3XexvFAVctbtznDytk3UKbkXq8FYWEVdz1ycxnAqHaKHePyGQEg=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-T/FpIr0OcR1kad/u5ZTDE2ziFG7QM6pq1MN8atHBmyyOJgqWjGez9TQ0W2WCixS7EE9fsUQKWn70l5M+e+O/qg=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-sWd7bGH+thMsNGYdqPdLuH7SDEkpplWCDKSCWhjkMPXwz3o/9BK3ZNrd5JGUB8y+cPbs3rXIe3Ah7iSlKXj5FQ=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/config-resolver": "^4.4.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-ol9yhY2nzPTrQoKX9WZ8cps2JABcDt0Dlhr59FRYYW/2S5h07PFrhfZZzD8sXZ8XpYfObTIJ+evmbcz41m1SJQ=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], "@aws-sdk/client-cognito-identity/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-h4VTGl6lxJkM/odeRPzXB5YZbkxVr5FnJ2Bwv78+IY9Ah7QsXSBhefvvz1CQDoQNzOLYsNMQ3PhMQM7i6tpoPQ=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.15", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-hYTWRlhOnhs2tYCYgNK30jaQKyl4FvXQl0AK9tKRZq8sunS9ygi+USYiG6LCb7DuqT0Gl3jONP9jtb4D4NYhHw=="], "@aws-sdk/client-cognito-identity/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@aws-sdk/client-firehose/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="], - - "@aws-sdk/client-firehose/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="], - - "@aws-sdk/client-firehose/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="], - - "@aws-sdk/client-firehose/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - "@aws-sdk/client-firehose/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug=="], + "@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.42", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.37", "@aws-sdk/credential-provider-http": "^3.972.39", "@aws-sdk/credential-provider-ini": "^3.972.41", "@aws-sdk/credential-provider-process": "^3.972.37", "@aws-sdk/credential-provider-sso": "^3.972.41", "@aws-sdk/credential-provider-web-identity": "^3.972.41", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ=="], + "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="], - "@aws-sdk/client-lambda/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/client-lambda/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-sdk/client-lambda/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="], - - "@aws-sdk/client-lambda/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="], - - "@aws-sdk/client-lambda/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="], - - "@aws-sdk/client-lambda/@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="], + "@aws-sdk/client-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], "@aws-sdk/client-sso/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], @@ -5453,31 +5447,33 @@ "@aws-sdk/client-sts/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.782.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-dMFkUBgh2Bxuw8fYZQoH/u3H4afQ12VSkzEi//qFiDTwbKYq+u+RYjc8GLDM6JSK1BShMu5AVR7HD4ap1TYUnA=="], + "@aws-sdk/client-sts/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.19", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.27", "@aws-sdk/middleware-host-header": "^3.972.9", "@aws-sdk/middleware-logger": "^3.972.9", "@aws-sdk/middleware-recursion-detection": "^3.972.10", "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/region-config-resolver": "^3.972.11", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@aws-sdk/util-user-agent-browser": "^3.972.9", "@aws-sdk/util-user-agent-node": "^3.973.15", "@smithy/config-resolver": "^4.4.14", "@smithy/core": "^3.23.14", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/hash-node": "^4.2.13", "@smithy/invalid-dependency": "^4.2.13", "@smithy/middleware-content-length": "^4.2.13", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-retry": "^4.5.0", "@smithy/middleware-serde": "^4.2.17", "@smithy/middleware-stack": "^4.2.13", "@smithy/node-config-provider": "^4.3.13", "@smithy/node-http-handler": "^4.5.2", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.45", "@smithy/util-defaults-mode-node": "^4.2.49", "@smithy/util-endpoints": "^3.3.4", "@smithy/util-middleware": "^4.2.13", "@smithy/util-retry": "^4.3.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/credential-provider-env/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/credential-provider-env/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-sdk/credential-provider-http/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/credential-provider-http/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/credential-provider-http/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/credential-provider-http/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.19", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.27", "@aws-sdk/middleware-host-header": "^3.972.9", "@aws-sdk/middleware-logger": "^3.972.9", "@aws-sdk/middleware-recursion-detection": "^3.972.10", "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/region-config-resolver": "^3.972.11", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@aws-sdk/util-user-agent-browser": "^3.972.9", "@aws-sdk/util-user-agent-node": "^3.973.15", "@smithy/config-resolver": "^4.4.14", "@smithy/core": "^3.23.14", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/hash-node": "^4.2.13", "@smithy/invalid-dependency": "^4.2.13", "@smithy/middleware-content-length": "^4.2.13", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-retry": "^4.5.0", "@smithy/middleware-serde": "^4.2.17", "@smithy/middleware-stack": "^4.2.13", "@smithy/node-config-provider": "^4.3.13", "@smithy/node-http-handler": "^4.5.2", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.45", "@smithy/util-defaults-mode-node": "^4.2.49", "@smithy/util-endpoints": "^3.3.4", "@smithy/util-middleware": "^4.2.13", "@smithy/util-retry": "^4.3.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.19", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.27", "@aws-sdk/middleware-host-header": "^3.972.9", "@aws-sdk/middleware-logger": "^3.972.9", "@aws-sdk/middleware-recursion-detection": "^3.972.10", "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/region-config-resolver": "^3.972.11", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@aws-sdk/util-user-agent-browser": "^3.972.9", "@aws-sdk/util-user-agent-node": "^3.973.15", "@smithy/config-resolver": "^4.4.14", "@smithy/core": "^3.23.14", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/hash-node": "^4.2.13", "@smithy/invalid-dependency": "^4.2.13", "@smithy/middleware-content-length": "^4.2.13", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-retry": "^4.5.0", "@smithy/middleware-serde": "^4.2.17", "@smithy/middleware-stack": "^4.2.13", "@smithy/node-config-provider": "^4.3.13", "@smithy/node-http-handler": "^4.5.2", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.45", "@smithy/util-defaults-mode-node": "^4.2.49", "@smithy/util-endpoints": "^3.3.4", "@smithy/util-middleware": "^4.2.13", "@smithy/util-retry": "^4.3.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ozge/c7NdHUDyHqro6+P5oHt8wfKSUBN+olttiVfBe9Mw3wBMpPa3gQ0pZnG+gwBkKskBuip2bMR16tqYvUSEA=="], @@ -5491,59 +5487,59 @@ "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-c7Eccw2lhFx2/+qJn3g+uIDWRuWi2A6Sz3PVvckFUEzPsP0dPUo19hlvtarwP5GzrsXn0yEPRVhpewsIaSCGaQ=="], - "@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.19", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.27", "@aws-sdk/middleware-host-header": "^3.972.9", "@aws-sdk/middleware-logger": "^3.972.9", "@aws-sdk/middleware-recursion-detection": "^3.972.10", "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/region-config-resolver": "^3.972.11", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@aws-sdk/util-user-agent-browser": "^3.972.9", "@aws-sdk/util-user-agent-node": "^3.973.15", "@smithy/config-resolver": "^4.4.14", "@smithy/core": "^3.23.14", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/hash-node": "^4.2.13", "@smithy/invalid-dependency": "^4.2.13", "@smithy/middleware-content-length": "^4.2.13", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-retry": "^4.5.0", "@smithy/middleware-serde": "^4.2.17", "@smithy/middleware-stack": "^4.2.13", "@smithy/node-config-provider": "^4.3.13", "@smithy/node-http-handler": "^4.5.2", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.45", "@smithy/util-defaults-mode-node": "^4.2.49", "@smithy/util-endpoints": "^3.3.4", "@smithy/util-middleware": "^4.2.13", "@smithy/util-retry": "^4.3.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.19", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.27", "@aws-sdk/middleware-host-header": "^3.972.9", "@aws-sdk/middleware-logger": "^3.972.9", "@aws-sdk/middleware-recursion-detection": "^3.972.10", "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/region-config-resolver": "^3.972.11", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@aws-sdk/util-user-agent-browser": "^3.972.9", "@aws-sdk/util-user-agent-node": "^3.973.15", "@smithy/config-resolver": "^4.4.14", "@smithy/core": "^3.23.14", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/hash-node": "^4.2.13", "@smithy/invalid-dependency": "^4.2.13", "@smithy/middleware-content-length": "^4.2.13", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-retry": "^4.5.0", "@smithy/middleware-serde": "^4.2.17", "@smithy/middleware-stack": "^4.2.13", "@smithy/node-config-provider": "^4.3.13", "@smithy/node-http-handler": "^4.5.2", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.45", "@smithy/util-defaults-mode-node": "^4.2.49", "@smithy/util-endpoints": "^3.3.4", "@smithy/util-middleware": "^4.2.13", "@smithy/util-retry": "^4.3.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@aws-sdk/credential-providers/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/credential-providers/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.30", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-ini": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw=="], + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="], - "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], "@aws-sdk/middleware-flexible-checksums/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], "@aws-sdk/middleware-sdk-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ=="], + "@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-DcDXAl32I/YZnJ9LyX/XpRXOtoTYIwgmYxoNMGkyvtomdjPpkXPGhz93VJyzKFFNffz/SZwEkoAuWOkeOzo90Q=="], - "@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog=="], + "@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.15", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-4Org8yUew+Y1+buTcH5A39unAdkVRnQxcOp3XexvFAVctbtznDytk3UKbkXq8FYWEVdz1ycxnAqHaKHePyGQEg=="], - "@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ=="], + "@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-T/FpIr0OcR1kad/u5ZTDE2ziFG7QM6pq1MN8atHBmyyOJgqWjGez9TQ0W2WCixS7EE9fsUQKWn70l5M+e+O/qg=="], - "@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ=="], + "@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-sWd7bGH+thMsNGYdqPdLuH7SDEkpplWCDKSCWhjkMPXwz3o/9BK3ZNrd5JGUB8y+cPbs3rXIe3Ah7iSlKXj5FQ=="], - "@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/config-resolver": "^4.4.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg=="], + "@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-ol9yhY2nzPTrQoKX9WZ8cps2JABcDt0Dlhr59FRYYW/2S5h07PFrhfZZzD8sXZ8XpYfObTIJ+evmbcz41m1SJQ=="], - "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], "@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], - "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw=="], + "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-h4VTGl6lxJkM/odeRPzXB5YZbkxVr5FnJ2Bwv78+IY9Ah7QsXSBhefvvz1CQDoQNzOLYsNMQ3PhMQM7i6tpoPQ=="], - "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.15", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w=="], + "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-hYTWRlhOnhs2tYCYgNK30jaQKyl4FvXQl0AK9tKRZq8sunS9ygi+USYiG6LCb7DuqT0Gl3jONP9jtb4D4NYhHw=="], "@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@aws-sdk/token-providers/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/token-providers/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.19", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.27", "@aws-sdk/middleware-host-header": "^3.972.9", "@aws-sdk/middleware-logger": "^3.972.9", "@aws-sdk/middleware-recursion-detection": "^3.972.10", "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/region-config-resolver": "^3.972.11", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@aws-sdk/util-user-agent-browser": "^3.972.9", "@aws-sdk/util-user-agent-node": "^3.973.15", "@smithy/config-resolver": "^4.4.14", "@smithy/core": "^3.23.14", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/hash-node": "^4.2.13", "@smithy/invalid-dependency": "^4.2.13", "@smithy/middleware-content-length": "^4.2.13", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-retry": "^4.5.0", "@smithy/middleware-serde": "^4.2.17", "@smithy/middleware-stack": "^4.2.13", "@smithy/node-config-provider": "^4.3.13", "@smithy/node-http-handler": "^4.5.2", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.45", "@smithy/util-defaults-mode-node": "^4.2.49", "@smithy/util-endpoints": "^3.3.4", "@smithy/util-middleware": "^4.2.13", "@smithy/util-retry": "^4.3.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q=="], + "@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="], - "@aws-sdk/token-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/token-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], @@ -5553,13 +5549,7 @@ "@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - "@azure/core-http/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], - - "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.5.12", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-nUR0q8PPfoA/svPM43Gup7vLOZWppaNrYgGmrVqrAVJa7cOH4hMG6FX9M4mQ8dZA1/ObGZHzES7Ed88hxEBSJg=="], - - "@azure/identity/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - - "@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -5577,7 +5567,7 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "@develar/schema-utils/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -5599,23 +5589,15 @@ "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], - "@electron/rebuild/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "@electron/rebuild/node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="], - - "@electron/rebuild/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "@electron/universal/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + "@electron/universal/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], "@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "@electron/windows-sign/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + "@electron/windows-sign/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], "@expressive-code/plugin-shiki/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], - "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], - - "@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.4.0", "", {}, "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ=="], "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -5623,6 +5605,8 @@ "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@jsx-email/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@jsx-email/cli/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], @@ -5639,33 +5623,31 @@ "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "@modelcontextprotocol/sdk/hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="], + "@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], - "@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], - "@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/auth-app/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], - "@octokit/auth-oauth-app/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/auth-oauth-app/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/auth-oauth-app/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@octokit/auth-oauth-device/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/auth-oauth-device/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/auth-oauth-device/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@octokit/auth-oauth-user/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/auth-oauth-user/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/auth-oauth-user/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], @@ -5679,11 +5661,11 @@ "@octokit/endpoint/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], - "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/graphql/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="], - "@octokit/oauth-methods/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/oauth-methods/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/oauth-methods/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -5719,8 +5701,6 @@ "@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], - "@opencode-ai/core/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], "@opencode-ai/desktop/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], @@ -5731,8 +5711,6 @@ "@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@opencode-ai/script/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], @@ -5741,8 +5719,14 @@ "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + "@opentui/solid/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], + "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], @@ -5781,7 +5765,7 @@ "@slack/socket-mode/@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], - "@slack/socket-mode/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "@slack/socket-mode/ws": ["ws@7.5.11", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="], "@slack/web-api/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], @@ -5791,41 +5775,19 @@ "@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], - "@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="], - - "@smithy/hash-node/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@smithy/hash-stream-node/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@smithy/md5-js/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@smithy/signature-v4/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@smithy/util-base64/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], "@solidjs/start/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], "@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], - "@solidjs/start/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], - - "@standard-community/standard-json/effect": ["effect@4.0.0-beta.48", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw=="], - - "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.48", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw=="], - "@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], @@ -5851,7 +5813,7 @@ "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.4", "", {}, "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ=="], + "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.7", "", {}, "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q=="], "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], @@ -5859,19 +5821,11 @@ "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="], + "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="], - "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], + "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], - "ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uz8tIlkDgQJG9Js2Wh9JHzd4kI9+hYJqf9XXJLx60vyN5mRIqhr49iwR5zGP5Gl8odp2PeR3Gh2k+5bh3Z1HHw=="], - - "ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.95", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/google": "3.0.53", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-xL44fHlTtDM7RLkMTgyqMfkfthA38JS91bbMaHItObIhte1PAIY936ZV1PLl/Z9A/oBAXjHWbXo5xDoHzB7LEg=="], - - "ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.75", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-V8UKK4fNpI9cnrtsZBvUp9O9J6Y9fTKBRoSLyEaNGPirACewixmLDbXsSgAeownPVWiWpK34bFysd+XouI5Ywg=="], - - "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], - - "ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "ajv-keywords/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -5885,14 +5839,14 @@ "app-builder-lib/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "app-builder-lib/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], - "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], "astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], @@ -5907,6 +5861,8 @@ "astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], @@ -5915,8 +5871,6 @@ "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "body-parser/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], - "builder-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "builder-util/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -5941,10 +5895,6 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "db0/drizzle-orm": ["drizzle-orm@1.0.0-beta.19-d95b7a4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-bZZKKeoRKrMVU6zKTscjrSH0+WNb1WEi3N0Jl4wEyQ7aQpTgHzdYY6IJQ1P0M74HuSJVeX4UpkFB/S6dtqLEJg=="], - - "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "dir-compare/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -5953,9 +5903,7 @@ "dmg-builder/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "dmg-license/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], - - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "dmg-license/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], @@ -5965,8 +5913,6 @@ "effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "effect/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "electron-builder/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -5977,11 +5923,11 @@ "electron-updater/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "electron-updater/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], - "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "engine.io-client/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], "esbuild-plugin-copy/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -5999,8 +5945,6 @@ "express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], - "express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], - "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], @@ -6009,24 +5953,20 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "gitlab-ai-provider/openai": ["openai@6.34.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw=="], + "gitlab-ai-provider/openai": ["openai@6.39.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-z3dO9fEWOXBzlXynVb/xZ/tujzUjFWQWn3C0n0mw6Vo0zJTbEkaN4b2cLWjhJ6haJQx8LlREoafHRl+Gu/Hl+A=="], "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "happy-dom/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "happy-dom/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "html-minifier-terser/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], - "html-minifier-terser/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "iconv-corefoundation/cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], @@ -6043,8 +5983,6 @@ "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], @@ -6063,15 +6001,13 @@ "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "motion/framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], - - "mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - - "mssql/tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], + "motion/framer-motion": ["framer-motion@12.40.0", "", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="], "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], - "nitro/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], + "nitro/undici": ["undici@7.26.0", "", {}, "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg=="], + + "node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="], "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -6079,7 +6015,7 @@ "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], - "nypm/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + "nypm/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], @@ -6091,24 +6027,10 @@ "opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], - "opencode/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - - "opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - - "opencode-poe-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "ora/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], @@ -6145,12 +6067,10 @@ "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -6177,10 +6097,6 @@ "storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "storybook/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], - - "storybook-solidjs-vite/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -6193,11 +6109,13 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], + "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "tree-sitter-bash/node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], + "tree-sitter-bash/node-addon-api": ["node-addon-api@8.8.0", "", {}, "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA=="], "tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -6219,13 +6137,13 @@ "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], - "vitest/@vitest/expect": ["@vitest/expect@4.1.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww=="], + "vitest/@vitest/expect": ["@vitest/expect@4.1.7", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.7", "@vitest/utils": "4.1.7", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w=="], - "vitest/@vitest/spy": ["@vitest/spy@4.1.4", "", {}, "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ=="], + "vitest/@vitest/spy": ["@vitest/spy@4.1.7", "", {}, "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q=="], - "vitest/es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + "vitest/es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], - "vitest/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + "vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], "vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], @@ -6249,8 +6167,6 @@ "yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - "zod-to-json-schema/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@actions/artifact/@actions/core/@actions/exec": ["@actions/exec@2.0.0", "", { "dependencies": { "@actions/io": "^2.0.0" } }, "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw=="], @@ -6263,10 +6179,6 @@ "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], - - "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="], - "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -6275,16 +6187,20 @@ "@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/google-vertex/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], - "@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/google/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], - "@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -6321,39 +6237,9 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-user-agent/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="], - - "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.24", "", { "dependencies": { "@nodable/entities": "2.1.0", "@smithy/types": "^4.14.1", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw=="], - - "@aws-sdk/client-lambda/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g=="], - - "@aws-sdk/client-lambda/@aws-sdk/core/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.37", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.39", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-env": "^3.972.37", "@aws-sdk/credential-provider-http": "^3.972.39", "@aws-sdk/credential-provider-login": "^3.972.41", "@aws-sdk/credential-provider-process": "^3.972.37", "@aws-sdk/credential-provider-sso": "^3.972.41", "@aws-sdk/credential-provider-web-identity": "^3.972.41", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.37", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - - "@aws-sdk/client-lambda/@aws-sdk/types/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - - "@aws-sdk/client-lambda/@smithy/core/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - - "@aws-sdk/client-lambda/@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - - "@aws-sdk/client-lambda/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw=="], @@ -6367,77 +6253,21 @@ "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.782.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/nested-clients": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xCna0opVPaueEbJoclj5C6OpDNi0Gynj+4d7tnuXGgQhTHPyAz8ZyClkVqpi5qvHTgxROdUEDxWqEO5jqRHZHQ=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ=="], + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/config-resolver": "^4.4.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw=="], - - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.15", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w=="], - - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/config-resolver": "^4.4.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.15", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/config-resolver": "^4.4.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.15", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="], "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], @@ -6445,119 +6275,45 @@ "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], - "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog=="], + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ=="], + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/config-resolver": "^4.4.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg=="], + "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="], + "@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.15", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/config-resolver": "^4.4.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.15", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], - - "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], - - "@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ=="], - - "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog=="], - - "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ=="], - - "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ=="], - - "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/config-resolver": "^4.4.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg=="], - - "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="], - - "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw=="], - - "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.15", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w=="], - - "@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], - - "@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], - - "@azure/identity/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@develar/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], - "@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - "@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@electron/rebuild/node-gyp/make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="], + "@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@electron/rebuild/node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], + "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], - "@electron/rebuild/node-gyp/proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], - - "@electron/rebuild/node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], - - "@electron/rebuild/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "@electron/rebuild/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - - "@electron/windows-sign/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "@electron/windows-sign/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], @@ -6571,8 +6327,6 @@ "@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - "@gitlab/opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "@hey-api/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], @@ -6633,9 +6387,7 @@ "@jsx-email/cli/vite/rollup": ["rollup@3.30.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA=="], - "@jsx-email/doiuse-email/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -6657,30 +6409,38 @@ "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/auth-app/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/auth-app/@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], "@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-app/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/auth-oauth-app/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/auth-oauth-app/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-oauth-device/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-device/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/auth-oauth-device/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/auth-oauth-device/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-user/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/auth-oauth-user/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/auth-oauth-user/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -6693,17 +6453,21 @@ "@octokit/graphql/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/graphql/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], "@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/oauth-methods/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/plugin-paginate-rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], "@octokit/plugin-paginate-rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], - "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -6717,7 +6481,7 @@ "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -6737,7 +6501,7 @@ "@octokit/rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], - "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -6747,8 +6511,6 @@ "@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], - "@opencode-ai/llm/@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="], - "@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], @@ -6787,41 +6549,17 @@ "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], - "@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@standard-community/standard-json/effect/fast-check": ["fast-check@4.6.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA=="], - - "@standard-community/standard-json/effect/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], - - "@standard-community/standard-json/effect/msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="], - - "@standard-community/standard-json/effect/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], - - "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@standard-community/standard-openapi/effect/fast-check": ["fast-check@4.6.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA=="], - - "@standard-community/standard-openapi/effect/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], - - "@standard-community/standard-openapi/effect/msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="], - - "@standard-community/standard-openapi/effect/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], - "@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], - - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - - "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -6853,6 +6591,8 @@ "astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -6867,7 +6607,7 @@ "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "dir-compare/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -6875,7 +6615,7 @@ "dmg-license/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "editorconfig/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "editorconfig/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "electron-builder/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], @@ -6889,7 +6629,7 @@ "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -6915,21 +6655,9 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "motion/framer-motion/motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], + "motion/framer-motion/motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="], - "motion/framer-motion/motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], - - "mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - - "opencode-poe-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - - "ora/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "ora/bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -6937,9 +6665,7 @@ "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - - "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -6957,7 +6683,7 @@ "tw-to-css/tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "tw-to-css/tailwindcss/postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], + "tw-to-css/tailwindcss/postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -6965,8 +6691,6 @@ "venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "venice-ai-sdk-provider/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], - "vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], @@ -7059,20 +6783,10 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.9", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/signature-v4-multi-region": "^3.996.27", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.9", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/signature-v4-multi-region": "^3.996.27", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1048.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.9", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/signature-v4-multi-region": "^3.996.27", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ=="], - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-5GlJBejo8wqMpSSEKb45WE82YxI2k73YuebjLH/eWDNQeE6VI5Bh9lA1YQ7xNkLLH8hIsb0pSfKVuwh0VEzVrg=="], @@ -7081,15 +6795,15 @@ "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], - "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], @@ -7097,40 +6811,20 @@ "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@electron/rebuild/node-gyp/make-fetch-happen/@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], - - "@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], - - "@electron/rebuild/node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - - "@electron/rebuild/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@electron/rebuild/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@electron/rebuild/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@electron/rebuild/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -7181,6 +6875,8 @@ "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -7191,19 +6887,25 @@ "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@sentry/bundler-plugin-core/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "@sentry/bundler-plugin-core/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "@sentry/bundler-plugin-core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -7213,15 +6915,7 @@ "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], - "@standard-community/standard-json/effect/msgpackr/msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - - "@standard-community/standard-openapi/effect/msgpackr/msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -7229,7 +6923,7 @@ "archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -7239,7 +6933,7 @@ "astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], - "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -7267,7 +6961,7 @@ "js-beautify/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "js-beautify/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "js-beautify/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -7279,7 +6973,7 @@ "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -7291,63 +6985,35 @@ "@astrojs/check/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], - - "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg=="], - - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], - - "@electron/rebuild/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@electron/rebuild/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], @@ -7357,30 +7023,6 @@ "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="], - "@standard-community/standard-json/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], - - "@standard-community/standard-json/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], - - "@standard-community/standard-json/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], - - "@standard-community/standard-json/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], - - "@standard-community/standard-json/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], - - "@standard-community/standard-json/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], - - "@standard-community/standard-openapi/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], - - "@standard-community/standard-openapi/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], - - "@standard-community/standard-openapi/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], - - "@standard-community/standard-openapi/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], - - "@standard-community/standard-openapi/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], - - "@standard-community/standard-openapi/effect/msgpackr/msgpackr-extract/@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], - "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], @@ -7409,23 +7051,7 @@ "tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g=="], - - "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g=="], - - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -7434,19 +7060,5 @@ "js-beautify/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "js-beautify/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], } } diff --git a/packages/core/migration/20260603001617_session_message_projection_indexes/migration.sql b/packages/core/migration/20260603001617_session_message_projection_indexes/migration.sql new file mode 100644 index 0000000000..ed6b728a1f --- /dev/null +++ b/packages/core/migration/20260603001617_session_message_projection_indexes/migration.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS `session_message_session_idx`;--> statement-breakpoint +DROP INDEX IF EXISTS `session_message_session_type_idx`;--> statement-breakpoint +CREATE INDEX `event_aggregate_seq_idx` ON `event` (`aggregate_id`,`seq`);--> statement-breakpoint +CREATE INDEX `session_message_session_time_created_id_idx` ON `session_message` (`session_id`,`time_created`,`id`);--> statement-breakpoint +CREATE INDEX `session_message_session_type_time_created_id_idx` ON `session_message` (`session_id`,`type`,`time_created`,`id`); \ No newline at end of file diff --git a/packages/core/migration/20260603001617_session_message_projection_indexes/snapshot.json b/packages/core/migration/20260603001617_session_message_projection_indexes/snapshot.json new file mode 100644 index 0000000000..34adfe1ff4 --- /dev/null +++ b/packages/core/migration/20260603001617_session_message_projection_indexes/snapshot.json @@ -0,0 +1,1708 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "6a0e33d0-4866-402f-b287-de400200b05e", + "prevIds": ["80f2378a-ed35-45cb-9d3b-9f4837fac801"], + "ddl": [ + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "permission_project_action_resource_idx", + "entityType": "indexes", + "table": "permission" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_time_created_id_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_time_created_id_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} diff --git a/packages/core/migration/20260603040000_session_message_projection_order/migration.sql b/packages/core/migration/20260603040000_session_message_projection_order/migration.sql new file mode 100644 index 0000000000..a56b07b676 --- /dev/null +++ b/packages/core/migration/20260603040000_session_message_projection_order/migration.sql @@ -0,0 +1,5 @@ +ALTER TABLE `session_message` ADD `seq` integer NOT NULL;--> statement-breakpoint +DROP INDEX IF EXISTS `session_message_session_time_created_id_idx`;--> statement-breakpoint +DROP INDEX IF EXISTS `session_message_session_type_time_created_id_idx`;--> statement-breakpoint +CREATE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint +CREATE INDEX `session_message_session_type_seq_idx` ON `session_message` (`session_id`,`type`,`seq`); diff --git a/packages/core/migration/20260603040000_session_message_projection_order/snapshot.json b/packages/core/migration/20260603040000_session_message_projection_order/snapshot.json new file mode 100644 index 0000000000..35aac3f7b8 --- /dev/null +++ b/packages/core/migration/20260603040000_session_message_projection_order/snapshot.json @@ -0,0 +1,1638 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "127d5585-9d6d-4b89-b126-15a36980392c", + "prevIds": ["6a0e33d0-4866-402f-b287-de400200b05e"], + "ddl": [ + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": ["active_account_id"], + "tableTo": "account", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": ["aggregate_id"], + "tableTo": "event_sequence", + "columnsTo": ["aggregate_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": ["message_id"], + "tableTo": "message", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": ["email", "url"], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": ["session_id", "position"], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": ["name"], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["aggregate_id"], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["session_id"], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "permission_project_action_resource_idx", + "entityType": "indexes", + "table": "permission" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} diff --git a/packages/core/migration/20260603141458_session_input_inbox/migration.sql b/packages/core/migration/20260603141458_session_input_inbox/migration.sql new file mode 100644 index 0000000000..c721ba897d --- /dev/null +++ b/packages/core/migration/20260603141458_session_input_inbox/migration.sql @@ -0,0 +1,12 @@ +CREATE TABLE `session_input` ( + `seq` integer PRIMARY KEY AUTOINCREMENT, + `id` text NOT NULL UNIQUE, + `session_id` text NOT NULL, + `prompt` text NOT NULL, + `delivery` text NOT NULL, + `promoted_seq` integer, + `time_created` integer NOT NULL, + CONSTRAINT `fk_session_input_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +CREATE INDEX `session_input_session_pending_seq_idx` ON `session_input` (`session_id`,`promoted_seq`,`seq`); \ No newline at end of file diff --git a/packages/core/migration/20260603141458_session_input_inbox/snapshot.json b/packages/core/migration/20260603141458_session_input_inbox/snapshot.json new file mode 100644 index 0000000000..7e51b1dfcf --- /dev/null +++ b/packages/core/migration/20260603141458_session_input_inbox/snapshot.json @@ -0,0 +1,1759 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "442462d9-4f4f-409f-ab00-0f8fb585f1a4", + "prevIds": ["127d5585-9d6d-4b89-b126-15a36980392c"], + "ddl": [ + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "session_input", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "promoted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": ["active_account_id"], + "tableTo": "account", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": ["aggregate_id"], + "tableTo": "event_sequence", + "columnsTo": ["aggregate_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": ["message_id"], + "tableTo": "message", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_input_session_id_session_id_fk", + "entityType": "fks", + "table": "session_input" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": ["email", "url"], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": ["session_id", "position"], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": ["name"], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["aggregate_id"], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": ["seq"], + "nameExplicit": false, + "name": "session_input_pk", + "table": "session_input", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["session_id"], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "permission_project_action_resource_idx", + "entityType": "indexes", + "table": "permission" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_input_session_pending_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_input_id_unique", + "entityType": "uniques", + "table": "session_input" + } + ], + "renames": [] +} diff --git a/packages/core/migration/20260603160727_jittery_ezekiel_stane/migration.sql b/packages/core/migration/20260603160727_jittery_ezekiel_stane/migration.sql new file mode 100644 index 0000000000..9a6909a48b --- /dev/null +++ b/packages/core/migration/20260603160727_jittery_ezekiel_stane/migration.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS `session_input_session_pending_seq_idx`;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS `event_aggregate_type_seq_idx` ON `event` (`aggregate_id`,`type`,`seq`);--> statement-breakpoint +CREATE INDEX IF NOT EXISTS `session_input_session_pending_delivery_seq_idx` ON `session_input` (`session_id`,`promoted_seq`,`delivery`,`seq`);--> statement-breakpoint +CREATE INDEX IF NOT EXISTS `session_message_session_time_created_id_idx` ON `session_message` (`session_id`,`time_created`,`id`); diff --git a/packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json b/packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json new file mode 100644 index 0000000000..49314e851b --- /dev/null +++ b/packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json @@ -0,0 +1,1956 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "fc92fa34-8074-44c3-88f0-a5417f7fd92d", + "prevIds": ["442462d9-4f4f-409f-ab00-0f8fb585f1a4"], + "ddl": [ + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "project_directory", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "session_input", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "promoted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_project_directory_project_id_project_id_fk", + "entityType": "fks", + "table": "project_directory" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_input_session_id_session_id_fk", + "entityType": "fks", + "table": "session_input" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "project_id", + "directory" + ], + "nameExplicit": false, + "name": "project_directory_pk", + "entityType": "pks", + "table": "project_directory" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "seq" + ], + "nameExplicit": false, + "name": "session_input_pk", + "table": "session_input", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_type_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "permission_project_action_resource_idx", + "entityType": "indexes", + "table": "permission" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + }, + { + "value": "delivery", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_input_session_pending_delivery_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_time_created_id_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_input_id_unique", + "entityType": "uniques", + "table": "session_input" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 09b40f96d8..ca9b90503a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -17,6 +17,7 @@ "opencode": "./bin/opencode" }, "exports": { + "./session/runner": "./src/session/runner/index.ts", "./*": "./src/*.ts" }, "imports": { @@ -39,6 +40,7 @@ "@types/npm-package-arg": "6.1.4", "@types/npmcli__arborist": "6.3.3", "@types/semver": "catalog:", + "@types/turndown": "5.0.5", "@types/which": "3.0.4", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", @@ -48,6 +50,7 @@ "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1", + "@opencode-ai/http-recorder": "workspace:*", "drizzle-kit": "catalog:" }, "dependencies": { @@ -80,6 +83,7 @@ "@npmcli/config": "10.8.1", "@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/effect-sqlite-node": "workspace:*", + "@opencode-ai/llm": "workspace:*", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", @@ -96,6 +100,7 @@ "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", + "htmlparser2": "8.0.2", "immer": "11.1.4", "ignore": "7.0.5", "jsonc-parser": "3.3.1", @@ -103,6 +108,7 @@ "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "semver": "^7.6.3", + "turndown": "7.2.0", "venice-ai-sdk-provider": "2.0.2", "which": "6.0.1", "xdg-basedir": "5.1.0", diff --git a/packages/core/src/background-job.ts b/packages/core/src/background-job.ts new file mode 100644 index 0000000000..f463273309 --- /dev/null +++ b/packages/core/src/background-job.ts @@ -0,0 +1,284 @@ +export * as BackgroundJob from "./background-job" + +import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect" +import { Identifier } from "./id/id" + +export type Status = "running" | "completed" | "error" | "cancelled" + +export type Info = { + id: string + type: string + title?: string + status: Status + started_at: number + completed_at?: number + output?: string + error?: string + metadata?: Record +} + +type Active = { + info: Info + done: Deferred.Deferred + scope: Scope.Closeable + token: object + pending: number + next: number + output?: { sequence: number; text: string } +} + +type State = { + jobs: SynchronizedRef.SynchronizedRef> + scope: Scope.Scope +} + +type FinishResult = { + info?: Info + done?: Deferred.Deferred + scope?: Scope.Closeable +} + +type StartResult = { info: Info } | { info: Info; scope: Scope.Closeable; token: object } + +type ExtendResult = { extended: false } | { extended: true; scope: Scope.Closeable; token: object; sequence: number } + +export type StartInput = { + id?: string + type: string + title?: string + metadata?: Record + run: Effect.Effect +} + +export type ExtendInput = { + id: string + run: Effect.Effect +} + +export type WaitInput = { + id: string + timeout?: number +} + +export type WaitResult = { + info?: Info + timedOut: boolean +} + +export interface Interface { + readonly list: () => Effect.Effect + readonly get: (id: string) => Effect.Effect + readonly start: (input: StartInput) => Effect.Effect + readonly extend: (input: ExtendInput) => Effect.Effect + readonly wait: (input: WaitInput) => Effect.Effect + readonly cancel: (id: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/BackgroundJob") {} + +function snapshot(job: Active): Info { + return { + ...job.info, + ...(job.info.metadata ? { metadata: { ...job.info.metadata } } : {}), + } +} + +function errorText(error: unknown) { + if (error instanceof Error) return error.message + return String(error) +} + +/** + * Makes one scoped, process-local registry. Entries are intentionally not + * durable: process restart or owner-scope closure loses status and interrupts + * live work. Persisted observation, restart recovery, and remote workers need a + * separate durable ownership slice rather than pretending this registry has + * those semantics. + */ +export const make = Effect.gen(function* () { + const state: State = { + jobs: yield* SynchronizedRef.make(new Map()), + scope: yield* Scope.Scope, + } + + const settle = Effect.fn("BackgroundJob.settle")(function* ( + id: string, + token: object, + sequence: number, + exit: Exit.Exit, + ) { + const completed_at = yield* Clock.currentTimeMillis + const result = yield* SynchronizedRef.modify(state.jobs, (jobs): readonly [FinishResult, Map] => { + const job = jobs.get(id) + if (!job) return [{}, jobs] + if (job.token !== token) return [{}, jobs] + if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs] + const pending = job.pending - 1 + const output = + Exit.isSuccess(exit) && (!job.output || sequence > job.output.sequence) + ? { sequence, text: exit.value } + : job.output + if (Exit.isSuccess(exit) && pending > 0) { + return [{}, new Map(jobs).set(id, { ...job, pending, output })] + } + const status: Exclude = Exit.isSuccess(exit) + ? "completed" + : Cause.hasInterruptsOnly(exit.cause) + ? "cancelled" + : "error" + const next = { + ...job, + pending: 0, + output, + info: { + ...job.info, + status, + completed_at, + ...(output ? { output: output.text } : {}), + ...(Exit.isFailure(exit) ? { error: errorText(Cause.squash(exit.cause)) } : {}), + }, + } + return [{ info: snapshot(next), done: job.done, scope: job.scope }, new Map(jobs).set(id, next)] + }) + if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore) + if (result.scope) { + yield* Scope.close(result.scope, Exit.void).pipe(Effect.forkIn(state.scope, { startImmediately: true })) + } + return result.info + }) + + const fork = Effect.fn("BackgroundJob.fork")(function* ( + scope: Scope.Scope, + id: string, + token: object, + sequence: number, + run: Effect.Effect, + ) { + return yield* run.pipe( + Effect.matchCauseEffect({ + onSuccess: (output) => settle(id, token, sequence, Exit.succeed(output)), + onFailure: (cause) => settle(id, token, sequence, Exit.failCause(cause)), + }), + Effect.asVoid, + Effect.forkIn(scope, { startImmediately: true }), + ) + }) + + const list: Interface["list"] = Effect.fn("BackgroundJob.list")(function* () { + return Array.from((yield* SynchronizedRef.get(state.jobs)).values()) + .map(snapshot) + .toSorted((a, b) => a.started_at - b.started_at) + }) + + const get: Interface["get"] = Effect.fn("BackgroundJob.get")(function* (id) { + const job = (yield* SynchronizedRef.get(state.jobs)).get(id) + if (!job) return + return snapshot(job) + }) + + const start: Interface["start"] = Effect.fn("BackgroundJob.start")(function* (input) { + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const id = input.id ?? Identifier.ascending("job") + const started_at = yield* Clock.currentTimeMillis + const done = yield* Deferred.make() + const result = yield* SynchronizedRef.modifyEffect( + state.jobs, + Effect.fnUntraced(function* (jobs) { + const existing = jobs.get(id) + if (existing?.info.status === "running") { + return [{ info: snapshot(existing) }, jobs] as readonly [StartResult, Map] + } + const scope = yield* Scope.fork(state.scope, "parallel") + const token = {} + const job = { + info: { + id, + type: input.type, + title: input.title, + status: "running" as const, + started_at, + metadata: input.metadata, + }, + done, + scope, + token, + pending: 1, + next: 1, + } + return [{ info: snapshot(job), scope, token }, new Map(jobs).set(id, job)] as readonly [ + StartResult, + Map, + ] + }), + ) + if ("scope" in result) yield* fork(result.scope, id, result.token, 0, restore(input.run)) + return result.info + }), + ) + }) + + const extend: Interface["extend"] = Effect.fn("BackgroundJob.extend")(function* (input) { + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const result = yield* SynchronizedRef.modify( + state.jobs, + (jobs): readonly [ExtendResult, Map] => { + const job = jobs.get(input.id) + if (!job || job.info.status !== "running") return [{ extended: false }, jobs] + return [ + { extended: true, scope: job.scope, token: job.token, sequence: job.next }, + new Map(jobs).set(input.id, { + ...job, + pending: job.pending + 1, + next: job.next + 1, + }), + ] + }, + ) + if (!result.extended) return false + yield* fork(result.scope, input.id, result.token, result.sequence, restore(input.run)) + return true + }), + ) + }) + + const wait: Interface["wait"] = Effect.fn("BackgroundJob.wait")(function* (input) { + const job = (yield* SynchronizedRef.get(state.jobs)).get(input.id) + if (!job) return { timedOut: false } + if (job.info.status !== "running") return { info: snapshot(job), timedOut: false } + if (input.timeout === undefined) return { info: yield* Deferred.await(job.done), timedOut: false } + if (input.timeout <= 0) return { info: snapshot(job), timedOut: true } + const info = yield* Deferred.await(job.done).pipe(Effect.timeoutOption(input.timeout)) + if (info._tag === "Some") return { info: info.value, timedOut: false } + return { info: snapshot(job), timedOut: true } + }) + + const cancel: Interface["cancel"] = Effect.fn("BackgroundJob.cancel")(function* (id) { + const completed_at = yield* Clock.currentTimeMillis + const result = yield* SynchronizedRef.modify(state.jobs, (jobs): readonly [FinishResult, Map] => { + const job = jobs.get(id) + if (!job) return [{}, jobs] + if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs] + const next = { + ...job, + pending: 0, + info: { + ...job.info, + status: "cancelled" as const, + completed_at, + }, + } + return [{ info: snapshot(next), done: job.done, scope: job.scope }, new Map(jobs).set(id, next)] + }) + if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore) + if (result.scope) yield* Scope.close(result.scope, Exit.void) + return result.info + }) + + return Service.of({ list, get, start, extend, wait, cancel }) +}) + +export const layer = Layer.effect(Service, make) + +export const defaultLayer = layer diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 81fde96209..55fb5c02d0 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -6,7 +6,7 @@ import { Context, Effect, Layer, Option, Schema } from "effect" import { FSUtil } from "./fs-util" import { Global } from "./global" import { Location } from "./location" -import { PermissionV2 } from "./permission" +import { PermissionSchema } from "./permission/schema" import { Policy } from "./policy" import { AbsolutePath } from "./schema" import { ConfigAgent } from "./config/agent" @@ -52,7 +52,7 @@ export class Info extends Schema.Class("Config.Info")({ username: Schema.String.pipe(Schema.optional).annotate({ description: "Username displayed in conversations and used for telemetry identity", }), - permissions: PermissionV2.Ruleset.pipe(Schema.optional).annotate({ + permissions: PermissionSchema.Ruleset.pipe(Schema.optional).annotate({ description: "Ordered tool permission rules applied to agent tool use", }), agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({ diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts index 80d4b08bfd..1dea6044bc 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/core/src/config/agent.ts @@ -1,7 +1,7 @@ export * as ConfigAgent from "./agent" import { Schema } from "effect" -import { PermissionV2 } from "../permission" +import { PermissionSchema } from "../permission/schema" import { ConfigProvider } from "./provider" import { PositiveInt } from "../schema" @@ -21,5 +21,5 @@ export class Info extends Schema.Class("ConfigV2.Agent")({ color: Color.pipe(Schema.optional), steps: PositiveInt.pipe(Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional), - permissions: PermissionV2.Ruleset.pipe(Schema.optional), + permissions: PermissionSchema.Ruleset.pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/plugin/skill.ts b/packages/core/src/config/plugin/skill.ts index a6c1bccdec..736fc675e6 100644 --- a/packages/core/src/config/plugin/skill.ts +++ b/packages/core/src/config/plugin/skill.ts @@ -18,7 +18,9 @@ export const Plugin = PluginV2.define({ const skill = yield* SkillV2.Service const transform = yield* skill.transform() const entries = yield* config.entries() - const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])) + const items = entries.flatMap((entry) => + entry.type === "document" ? (entry.info.skills ?? []) : [path.join(entry.path, "skill"), path.join(entry.path, "skills")], + ) yield* transform((editor) => { for (const item of items) { diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index 476f3a486a..9f2ba82eb3 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -27,5 +27,9 @@ export const migrations = ( import("./migration/20260601202201_amazing_prowler"), import("./migration/20260602002951_lowly_union_jack"), import("./migration/20260602182828_add_project_directories"), + import("./migration/20260603001617_session_message_projection_indexes"), + import("./migration/20260603040000_session_message_projection_order"), + import("./migration/20260603141458_session_input_inbox"), + import("./migration/20260603160727_jittery_ezekiel_stane"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration.ts b/packages/core/src/database/migration.ts index f0a0301442..c9a7dd02b9 100644 --- a/packages/core/src/database/migration.ts +++ b/packages/core/src/database/migration.ts @@ -1,12 +1,13 @@ export * as DatabaseMigration from "./migration" import { sql } from "drizzle-orm" -import { Effect } from "effect" +import { Effect, Semaphore } from "effect" import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" import { migrations } from "./migration.gen" type Database = EffectDrizzleSqlite.EffectSQLiteDatabase type Transaction = Parameters[0]>[0] +const lock = Semaphore.makeUnsafe(1) export type Migration = { id: string @@ -14,7 +15,7 @@ export type Migration = { } export function apply(db: Database) { - return applyOnly(db, migrations) + return lock.withPermit(applyOnly(db, migrations)) } export function applyOnly(db: Database, input: Migration[]) { diff --git a/packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts b/packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts new file mode 100644 index 0000000000..8dfd06fa77 --- /dev/null +++ b/packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603001617_session_message_projection_indexes", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_idx\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_idx\`;`) + yield* tx.run(`CREATE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_type_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`time_created\`,\`id\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts b/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts new file mode 100644 index 0000000000..77f44f88db --- /dev/null +++ b/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts @@ -0,0 +1,18 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603040000_session_message_projection_order", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer NOT NULL DEFAULT 0;`) + yield* tx.run(`UPDATE \`session_message\` SET \`seq\` = COALESCE((SELECT \`seq\` + 1 FROM \`event\` WHERE \`event\`.\`id\` = \`session_message\`.\`id\`), 0);`) + const unmatched = yield* tx.get<{ count: number }>(`SELECT COUNT(*) AS \`count\` FROM \`session_message\` WHERE \`seq\` = 0;`) + if ((unmatched?.count ?? 0) > 0) return yield* Effect.die("Cannot migrate session_message projections without matching durable events") + yield* tx.run(`UPDATE \`session_message\` SET \`seq\` = \`seq\` - 1;`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_time_created_id_idx\`;`) + yield* tx.run(`CREATE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603141458_session_input_inbox.ts b/packages/core/src/database/migration/20260603141458_session_input_inbox.ts new file mode 100644 index 0000000000..329ab15c08 --- /dev/null +++ b/packages/core/src/database/migration/20260603141458_session_input_inbox.ts @@ -0,0 +1,25 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603141458_session_input_inbox", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`session_input\` ( + \`seq\` integer PRIMARY KEY AUTOINCREMENT, + \`id\` text NOT NULL UNIQUE, + \`session_id\` text NOT NULL, + \`prompt\` text NOT NULL, + \`delivery\` text NOT NULL, + \`promoted_seq\` integer, + \`time_created\` integer NOT NULL, + CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `CREATE INDEX \`session_input_session_pending_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`seq\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts b/packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts new file mode 100644 index 0000000000..07230cc6a8 --- /dev/null +++ b/packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts @@ -0,0 +1,18 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603160727_jittery_ezekiel_stane", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`session_input_session_pending_seq_idx\`;`) + yield* tx.run(`CREATE INDEX IF NOT EXISTS \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`) + yield* tx.run( + `CREATE INDEX IF NOT EXISTS \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX IF NOT EXISTS \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/sqlite.bun.ts b/packages/core/src/database/sqlite.bun.ts index 5dda2cd2c6..e15f4c117e 100644 --- a/packages/core/src/database/sqlite.bun.ts +++ b/packages/core/src/database/sqlite.bun.ts @@ -175,8 +175,9 @@ const drizzleLayer = Layer.effect( }), ) -export const layer = (config: Config) => - Layer.merge( - nativeLayer(config), - Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(nativeLayer(config))), - ).pipe(Layer.provide(Reactivity.layer)) +export const layer = (config: Config) => { + const native = nativeLayer(config) + return Layer.merge(native, Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe( + Layer.provide(Reactivity.layer), + ) +} diff --git a/packages/core/src/database/sqlite.node.ts b/packages/core/src/database/sqlite.node.ts index d7471a4401..6eaecbee26 100644 --- a/packages/core/src/database/sqlite.node.ts +++ b/packages/core/src/database/sqlite.node.ts @@ -170,8 +170,9 @@ const drizzleLayer = Layer.effect( }), ) -export const layer = (config: Config) => - Layer.merge( - nativeLayer(config), - Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(nativeLayer(config))), - ).pipe(Layer.provide(Reactivity.layer)) +export const layer = (config: Config) => { + const native = nativeLayer(config) + return Layer.merge(native, Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe( + Layer.provide(Reactivity.layer), + ) +} diff --git a/packages/core/src/effect/keyed-mutex.ts b/packages/core/src/effect/keyed-mutex.ts new file mode 100644 index 0000000000..be4220516d --- /dev/null +++ b/packages/core/src/effect/keyed-mutex.ts @@ -0,0 +1,43 @@ +export * as KeyedMutex from "./keyed-mutex" + +import { Effect, Semaphore } from "effect" + +export interface KeyedMutex { + readonly size: Effect.Effect + readonly withLock: (key: Key) => (effect: Effect.Effect) => Effect.Effect +} + +/** + * Creates an in-memory mutex with one lock per key. Entries are removed when no + * holder or waiter remains. + * + * same key -> queue + * different key -> run independently + * + * `users` counts holders and waiters so an entry is not removed while a waiter + * will reuse it. + */ +export const makeUnsafe = (): KeyedMutex => { + const locks = new Map() + + const withLock = (key: Key) => (effect: Effect.Effect) => + Effect.suspend(() => { + const current = locks.get(key) + const entry = current ?? { semaphore: Semaphore.makeUnsafe(1), users: 0 } + if (!current) locks.set(key, entry) + entry.users++ + return entry.semaphore.withPermit(effect).pipe( + Effect.ensuring( + Effect.sync(() => { + entry.users-- + if (entry.users === 0) locks.delete(key) + }), + ), + ) + }) + + return { size: Effect.sync(() => locks.size), withLock } +} + +/** Creates an in-memory keyed mutex inside an Effect workflow. */ +export const make = (): Effect.Effect> => Effect.sync(makeUnsafe) diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 0be8c64ef6..27e682a2a5 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -1,19 +1,29 @@ export * as EventV2 from "./event" import { Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" -import { eq } from "drizzle-orm" +import { and, asc, eq, gt } from "drizzle-orm" import { Database } from "./database/database" import { EventSequenceTable, EventTable } from "./event/sql" import { Location } from "./location" -import { withStatics } from "./schema" +import { externalID, type ExternalID, NonNegativeInt, withStatics } from "./schema" import { Identifier } from "./util/identifier" export const ID = Schema.String.pipe( Schema.brand("Event.ID"), - withStatics((schema) => ({ create: () => schema.make("evt_" + Identifier.ascending()) })), + withStatics((schema) => ({ + create: () => schema.make("evt_" + Identifier.ascending()), + fromExternal: (input: ExternalID) => schema.make(externalID("evt", input)), + })), ) export type ID = typeof ID.Type +/** + * Durable aggregate continuation position for embedded replay streams. + * TODO: Decide whether a future HTTP / SDK surface should expose an opaque cursor instead. + */ +export const Cursor = NonNegativeInt.pipe(Schema.brand("EventV2.Cursor")) +export type Cursor = typeof Cursor.Type + export type Definition = { readonly type: Type readonly sync?: { @@ -29,6 +39,8 @@ export type Payload = { readonly id: ID readonly type: D["type"] readonly data: Data + /** Durable aggregate order, populated while synchronized events are projected. */ + readonly seq?: number readonly version?: number readonly location?: Location.Ref readonly metadata?: Record @@ -36,6 +48,7 @@ export type Payload = { export type Projector = (event: Payload) => Effect.Effect type AnyProjector = (event: Payload) => Effect.Effect +export type CommitGuard = (event: Payload) => Effect.Effect export type Listener = (event: Payload) => Effect.Effect export type Sync = (event: Payload) => Effect.Effect export type Unsubscribe = Effect.Effect @@ -48,6 +61,11 @@ export type SerializedEvent = { readonly data: Record } +export type CursorEvent = { + readonly cursor: Cursor + readonly event: E +} + export class InvalidSyncEventError extends Schema.TaggedErrorClass()( "EventV2.InvalidSyncEvent", { @@ -61,7 +79,15 @@ export function versionedType(type: string, version: number) { } export const registry = new Map() -const syncRegistry = new Map }>() +type SyncDefinition = Definition & { + readonly sync: NonNullable + readonly encode: (data: unknown) => unknown + readonly decode: (data: unknown) => unknown +} +const syncRegistry = new Map() + +// Synchronized events cross a JSON boundary, so their data schemas must encode and decode without services. +const syncCodec = (definition: Definition) => definition.data as Schema.Codec export function define(input: { readonly type: Type @@ -93,7 +119,10 @@ export function define }, + Object.assign(definition, { + encode: Schema.encodeUnknownSync(syncCodec(definition)), + decode: Schema.decodeUnknownSync(syncCodec(definition)), + }) as SyncDefinition, ) return definition as Schema.Schema>>> & Definition> @@ -117,16 +146,18 @@ export interface Interface { ) => Effect.Effect> readonly subscribe: (definition: D) => Stream.Stream> readonly all: () => Stream.Stream + readonly aggregateEvents: (input: { readonly aggregateID: string; readonly after?: Cursor }) => Stream.Stream readonly sync: (handler: Sync) => Effect.Effect readonly listen: (listener: Listener) => Effect.Effect + readonly beforeCommit: (guard: CommitGuard) => Effect.Effect readonly project: (definition: D, projector: Projector) => Effect.Effect readonly replay: ( event: SerializedEvent, - options?: { readonly publish?: boolean; readonly ownerID?: string }, + options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, ) => Effect.Effect readonly replayAll: ( events: SerializedEvent[], - options?: { readonly publish?: boolean; readonly ownerID?: string }, + options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, ) => Effect.Effect readonly remove: (aggregateID: string) => Effect.Effect readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect @@ -134,12 +165,18 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Event") {} -export const layer = Layer.effect( +export interface LayerOptions { + readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect +} + +export const layerWith = (options?: LayerOptions) => Layer.effect( Service, Effect.gen(function* () { const all = yield* PubSub.unbounded() + const synchronized = new Map>>() const typed = new Map>() const projectors = new Map() + const commitGuards = new Array() const listeners = new Array() const syncHandlers = new Array() const { db } = yield* Database.Service @@ -156,13 +193,16 @@ export const layer = Layer.effect( yield* Effect.addFinalizer(() => Effect.gen(function* () { yield* PubSub.shutdown(all) + yield* Effect.forEach(synchronized.values(), (pubsubs) => + Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }), + { discard: true }) yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true }) }), ) function commitSyncEvent( event: Payload, - input?: { readonly seq: number; readonly aggregateID: string; readonly ownerID?: string }, + input?: { readonly seq: number; readonly aggregateID: string; readonly ownerID?: string; readonly strictOwner?: boolean }, ) { return Effect.gen(function* () { const definition = registry.get(event.type) @@ -185,58 +225,93 @@ export const layer = Layer.effect( }), ) } else { - const list = projectors.get(event.type) ?? [] - yield* db - .transaction( - () => - Effect.gen(function* () { - const row = yield* db - .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id }) - .from(EventSequenceTable) - .where(eq(EventSequenceTable.aggregate_id, aggregateID)) - .get() - .pipe(Effect.orDie) - const latest = row?.seq ?? -1 - if (input && input.seq <= latest) return - if (input && row?.ownerID && row.ownerID !== input.ownerID) return - const seq = input?.seq ?? latest + 1 - if (input && seq !== latest + 1) { - yield* Effect.die( - new InvalidSyncEventError({ - type: event.type, - message: `Sequence mismatch for aggregate ${aggregateID}: expected ${latest + 1}, got ${seq}`, - }), - ) - } - for (const projector of list) { - yield* projector(event as Payload) - } - yield* db - .insert(EventSequenceTable) - .values([{ aggregate_id: aggregateID, seq, owner_id: input?.ownerID }]) - .onConflictDoUpdate({ - target: EventSequenceTable.aggregate_id, - set: { seq }, - }) - .run() - .pipe(Effect.orDie) - yield* db - .insert(EventTable) - .values([ - { - id: event.id, - aggregate_id: aggregateID, - seq, - type: versionedType(definition.type, sync.version), - data: event.data as Record, - }, - ]) - .run() - .pipe(Effect.orDie) - }), - { behavior: "immediate" }, + if (input && input.aggregateID !== aggregateID) { + yield* Effect.die( + new InvalidSyncEventError({ + type: event.type, + message: `Aggregate mismatch: expected ${input.aggregateID}, got ${aggregateID}`, + }), ) - .pipe(Effect.orDie) + } + const list = projectors.get(event.type) ?? [] + return yield* Effect.uninterruptible( + Effect.gen(function* () { + const committed = yield* db + .transaction( + () => + Effect.gen(function* () { + const row = yield* db + .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id }) + .from(EventSequenceTable) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .get() + .pipe(Effect.orDie) + const latest = row?.seq ?? -1 + if (input && input.seq <= latest) return + if (input && row?.ownerID && row.ownerID !== input.ownerID) { + if (input.strictOwner) { + yield* Effect.die( + new InvalidSyncEventError({ + type: event.type, + message: `Replay owner mismatch for aggregate ${aggregateID}: expected ${row.ownerID}, got ${input.ownerID ?? "none"}`, + }), + ) + } + return + } + const seq = input?.seq ?? latest + 1 + if (input && seq !== latest + 1) { + yield* Effect.die( + new InvalidSyncEventError({ + type: event.type, + message: `Sequence mismatch for aggregate ${aggregateID}: expected ${latest + 1}, got ${seq}`, + }), + ) + } + for (const guard of commitGuards) { + yield* guard(event) + } + for (const projector of list) { + yield* projector({ ...event, seq } as Payload) + } + const encoded = syncRegistry.get(versionedType(definition.type, sync.version))!.encode(event.data) + yield* db + .insert(EventSequenceTable) + .values([{ aggregate_id: aggregateID, seq, owner_id: input?.ownerID }]) + .onConflictDoUpdate({ + target: EventSequenceTable.aggregate_id, + set: { seq, ...(input?.ownerID && row?.ownerID == null ? { owner_id: input.ownerID } : {}) }, + }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(EventTable) + .values([ + { + id: event.id, + aggregate_id: aggregateID, + seq, + type: versionedType(definition.type, sync.version), + data: encoded as Record, + }, + ]) + .run() + .pipe(Effect.orDie) + return { aggregateID, seq } + }), + { behavior: "immediate" }, + ) + .pipe(Effect.orDie) + if (committed) { + yield* Effect.forEach( + synchronized.get(committed.aggregateID) ?? [], + (pubsub) => PubSub.publish(pubsub, undefined), + { discard: true }, + ) + } + return committed + }), + ) } } }) @@ -244,10 +319,14 @@ export const layer = Layer.effect( function publishEvent(event: Payload) { return Effect.gen(function* () { - for (const sync of syncHandlers) { - yield* sync(event as Payload) + const durable = registry.get(event.type)?.sync !== undefined + if (durable) { + for (const sync of syncHandlers) { + yield* sync(event as Payload) + } + const committed = yield* commitSyncEvent(event as Payload) + if (committed) event = { ...event, seq: committed.seq } } - yield* commitSyncEvent(event as Payload) for (const listener of listeners) { yield* listener(event as Payload) } @@ -277,7 +356,7 @@ export const layer = Layer.effect( }) } - function replay(event: SerializedEvent, options?: { readonly publish?: boolean; readonly ownerID?: string }) { + function replay(event: SerializedEvent, options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }) { return Effect.gen(function* () { const definition = syncRegistry.get(event.type) if (!definition) { @@ -289,22 +368,23 @@ export const layer = Layer.effect( id: event.id, type: definition.type, version: definition.sync.version, - data: event.data, + data: definition.decode(event.data), } as Payload - yield* commitSyncEvent(payload, { seq: event.seq, aggregateID: event.aggregateID, ownerID: options?.ownerID }) - if (options?.publish) { + const committed = yield* commitSyncEvent(payload, { seq: event.seq, aggregateID: event.aggregateID, ownerID: options?.ownerID, strictOwner: options?.strictOwner }) + if (committed && options?.publish) { + const published = { ...payload, seq: committed.seq } for (const listener of listeners) { - yield* listener(payload) + yield* listener(published) } const pubsub = typed.get(payload.type) - if (pubsub) yield* PubSub.publish(pubsub, payload) - yield* PubSub.publish(all, payload) + if (pubsub) yield* PubSub.publish(pubsub, published) + yield* PubSub.publish(all, published) } } }) } - function replayAll(events: SerializedEvent[], options?: { readonly publish?: boolean; readonly ownerID?: string }) { + function replayAll(events: SerializedEvent[], options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }) { return Effect.gen(function* () { const source = events[0]?.aggregateID if (!source) return undefined @@ -362,6 +442,88 @@ export const layer = Layer.effect( const streamAll = (): Stream.Stream => Stream.fromPubSub(all) + const decodeSerializedEvent = (event: SerializedEvent): CursorEvent => { + const definition = syncRegistry.get(event.type) + if (!definition) { + throw new InvalidSyncEventError({ type: event.type, message: `Unknown sync event type ${event.type}` }) + } + return { + cursor: Cursor.make(event.seq), + event: { + id: event.id, + type: definition.type, + version: definition.sync.version, + seq: event.seq, + data: definition.decode(event.data), + }, + } + } + + const readAfter = (aggregateID: string, after: number) => + (options?.beforeAggregateRead?.(aggregateID) ?? Effect.void).pipe( + Effect.andThen( + db + .select() + .from(EventTable) + .where(and(eq(EventTable.aggregate_id, aggregateID), gt(EventTable.seq, after))) + .orderBy(asc(EventTable.seq)) + .all(), + ), + Effect.orDie, + Effect.map((rows) => + rows.map((event) => + decodeSerializedEvent({ + id: event.id, + aggregateID: event.aggregate_id, + seq: event.seq, + type: event.type, + data: event.data, + }), + ), + ), + ) + + const subscribeSynchronized = (aggregateID: string) => + Effect.gen(function* () { + const pubsub = yield* PubSub.sliding(1) + const subscription = yield* PubSub.subscribe(pubsub) + yield* Effect.acquireRelease( + Effect.sync(() => { + const pubsubs = synchronized.get(aggregateID) ?? new Set() + pubsubs.add(pubsub) + synchronized.set(aggregateID, pubsubs) + }), + () => + Effect.sync(() => { + const pubsubs = synchronized.get(aggregateID) + pubsubs?.delete(pubsub) + if (pubsubs?.size === 0) synchronized.delete(aggregateID) + }).pipe(Effect.andThen(PubSub.shutdown(pubsub))), + ) + return subscription + }) + + const streamEvents = (input: { readonly aggregateID: string; readonly after?: Cursor }): Stream.Stream => + Stream.unwrap( + Effect.gen(function* () { + const synchronized = yield* subscribeSynchronized(input.aggregateID) + let cursor = input.after ?? -1 + const read = Effect.suspend(() => readAfter(input.aggregateID, cursor)).pipe( + Effect.tap((events) => + Effect.sync(() => { + cursor = events.at(-1)?.cursor ?? cursor + }), + ), + ) + const historical = yield* read + const live = Stream.fromSubscription(synchronized).pipe( + Stream.mapEffect(() => read), + Stream.flattenIterable, + ) + return Stream.concat(Stream.fromIterable(historical), live) + }), + ) + const listen = (listener: Listener): Effect.Effect => Effect.sync(() => { listeners.push(listener) @@ -380,6 +542,11 @@ export const layer = Layer.effect( }) }) + const beforeCommit = (guard: CommitGuard): Effect.Effect => + Effect.sync(() => { + commitGuards.push(guard) + }) + const project = (definition: D, projector: Projector): Effect.Effect => Effect.sync(() => { const list = projectors.get(definition.type) ?? [] @@ -387,8 +554,10 @@ export const layer = Layer.effect( projectors.set(definition.type, list) }) - return Service.of({ publish, subscribe, all: streamAll, sync, listen, project, replay, replayAll, remove, claim }) + return Service.of({ publish, subscribe, all: streamAll, aggregateEvents: streamEvents, sync, listen, beforeCommit, project, replay, replayAll, remove, claim }) }), ) +export const layer = layerWith() + export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer)) diff --git a/packages/core/src/event/sql.ts b/packages/core/src/event/sql.ts index 6bccc0fbb9..74f0ab4834 100644 --- a/packages/core/src/event/sql.ts +++ b/packages/core/src/event/sql.ts @@ -1,4 +1,4 @@ -import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" +import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core" import type { EventV2 } from "../event" export const EventSequenceTable = sqliteTable("event_sequence", { @@ -7,12 +7,19 @@ export const EventSequenceTable = sqliteTable("event_sequence", { owner_id: text(), }) -export const EventTable = sqliteTable("event", { - id: text().$type().primaryKey(), - aggregate_id: text() - .notNull() - .references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }), - seq: integer().notNull(), - type: text().notNull(), - data: text({ mode: "json" }).$type>().notNull(), -}) +export const EventTable = sqliteTable( + "event", + { + id: text().$type().primaryKey(), + aggregate_id: text() + .notNull() + .references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }), + seq: integer().notNull(), + type: text().notNull(), + data: text({ mode: "json" }).$type>().notNull(), + }, + (table) => [ + index("event_aggregate_seq_idx").on(table.aggregate_id, table.seq), + index("event_aggregate_type_seq_idx").on(table.aggregate_id, table.type, table.seq), + ], +) diff --git a/packages/core/src/file-mutation.ts b/packages/core/src/file-mutation.ts new file mode 100644 index 0000000000..ed5429dd58 --- /dev/null +++ b/packages/core/src/file-mutation.ts @@ -0,0 +1,202 @@ +export * as FileMutation from "./file-mutation" + +import { Context, Effect, Layer, Schema } from "effect" +import { dirname } from "path" +import { KeyedMutex } from "./effect/keyed-mutex" +import { FSUtil } from "./fs-util" +import { LocationMutation } from "./location-mutation" + +export interface WriteInput { + readonly plan: LocationMutation.Plan + readonly content: string | Uint8Array +} + +export interface TextWriteInput { + readonly plan: LocationMutation.Plan + readonly content: string +} + +export interface ConditionalWriteInput extends WriteInput { + readonly expected: Uint8Array +} + +export interface RemoveInput { + readonly plan: LocationMutation.Plan +} + +export class StaleContentError extends Schema.TaggedErrorClass()("FileMutation.StaleContentError", { + path: Schema.String, +}) {} + +export class TargetExistsError extends Schema.TaggedErrorClass()("FileMutation.TargetExistsError", { + path: Schema.String, +}) {} + +export interface WriteResult { + readonly operation: "write" + /** Canonical target actually passed to the filesystem mutation. */ + readonly target: string + /** Permission resource captured during planning. */ + readonly resource: string + readonly existed: boolean +} + +export interface RemoveResult { + readonly operation: "remove" + /** Canonical target actually passed to the filesystem mutation. */ + readonly target: string + /** Permission resource captured during planning. */ + readonly resource: string + readonly existed: boolean +} + +export interface Interface { + /** Create only while the planned target remains absent. */ + readonly create: (input: WriteInput) => Effect.Effect + /** Write after immediately revalidating the planned target. */ + readonly write: (input: WriteInput) => Effect.Effect + /** Write text while retaining an existing UTF-8 BOM and emitting at most one BOM. */ + readonly writeTextPreservingBom: (input: TextWriteInput) => Effect.Effect + /** Commit only if an existing target still has the expected bytes. */ + readonly writeIfUnchanged: ( + input: ConditionalWriteInput, + ) => Effect.Effect + /** Remove after immediately revalidating the planned target. */ + readonly remove: (input: RemoveInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/FileMutation") {} + +/** + * Commit planned file changes. + * + * resolve(path) -> approve -> lock target -> revalidate(plan) -> mutate + * + * The caller approves the plan first. This service locks the canonical target, + * revalidates the plan immediately before the filesystem operation, then mutates. + * + * `writeIfUnchanged` compares and writes while holding the same in-memory lock, + * so cooperating calls in this process cannot overwrite from the same stale + * content. Locks apply only within this service layer and only to identical + * canonical targets. + * + * Revalidation reduces the race window but is not atomic with the next + * path-based filesystem operation. A hostile local process can still race it. + * + * TODO: Use descriptor-relative no-follow operations where supported to close + * the final race. + */ +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const mutation = yield* LocationMutation.Service + const locks = KeyedMutex.makeUnsafe() + const withTargetLock = (target: string) => (effect: Effect.Effect) => + locks.withLock(target)(Effect.uninterruptible(effect)) + + const withValidatedTarget = (plan: LocationMutation.Plan) => ( + commit: (target: LocationMutation.Target) => Effect.Effect, + ) => withTargetLock(plan.target.canonical)(mutation.revalidate(plan).pipe(Effect.flatMap(commit))) + + const writeResult = (target: LocationMutation.Target, existed = target.exists): WriteResult => ({ + operation: "write", + target: target.canonical, + resource: target.resource, + existed, + }) + + const removeResult = (target: LocationMutation.Target): RemoveResult => ({ + operation: "remove", + target: target.canonical, + resource: target.resource, + existed: target.exists, + }) + + const write = Effect.fn("FileMutation.write")((input: WriteInput) => + withValidatedTarget(input.plan)((target) => + Effect.gen(function* () { + yield* fs.writeWithDirs(target.canonical, input.content) + return writeResult(target) + }), + ), + ) + + const writeTextPreservingBom = Effect.fn("FileMutation.writeTextPreservingBom")((input: TextWriteInput) => + withValidatedTarget(input.plan)((target) => + Effect.gen(function* () { + const next = splitBom(input.content) + const preserveBom = target.exists && hasUtf8Bom(yield* fs.readFile(target.canonical)) + yield* fs.writeWithDirs(target.canonical, joinBom(next.text, preserveBom || next.bom)) + return writeResult(target) + }), + ), + ) + + const create = Effect.fn("FileMutation.create")((input: WriteInput) => + withValidatedTarget(input.plan)((target) => + Effect.gen(function* () { + if (target.exists) return yield* new TargetExistsError({ path: target.canonical }) + yield* fs.ensureDir(dirname(target.canonical)) + if (typeof input.content === "string") yield* fs.writeFileString(target.canonical, input.content, { flag: "wx" }) + else yield* fs.writeFile(target.canonical, input.content, { flag: "wx" }) + return writeResult(target, false) + }), + ), + ) + + const writeIfUnchanged = Effect.fn("FileMutation.writeIfUnchanged")((input: ConditionalWriteInput) => + withValidatedTarget(input.plan)((target) => + Effect.gen(function* () { + const current = yield* fs.readFile(target.canonical) + if (!sameBytes(current, input.expected)) return yield* new StaleContentError({ path: target.canonical }) + yield* fs.writeWithDirs(target.canonical, input.content) + return writeResult(target) + }), + ), + ) + + const remove = Effect.fn("FileMutation.remove")((input: RemoveInput) => + withValidatedTarget(input.plan)((target) => + Effect.gen(function* () { + yield* fs.remove(target.canonical) + return removeResult(target) + }), + ), + ) + + return Service.of({ create, write, writeTextPreservingBom, writeIfUnchanged, remove }) + }), +) + +function splitBom(text: string) { + const stripped = text.replace(/^\uFEFF+/, "") + return { bom: stripped.length !== text.length, text: stripped } +} + +function joinBom(text: string, bom: boolean) { + const stripped = splitBom(text).text + return bom ? `\uFEFF${stripped}` : stripped +} + +function hasUtf8Bom(content: Uint8Array) { + return content[0] === 0xef && content[1] === 0xbb && content[2] === 0xbf +} + +function sameBytes(left: Uint8Array, right: Uint8Array) { + if (left.length !== right.length) return false + return left.every((byte, index) => byte === right[index]) +} + +export const locationLayer = layer + +/** + * Deferred until the corresponding V2 integrations exist. + */ +// TODO: Add formatter integration after V2 formatter runtime exists. +// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +// TODO: Add snapshots / undo after V2 snapshot design exists. +// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists. +// TODO: Design multi-file transactions / rollback if apply_patch needs atomic edits. +// Until then, edits are sequential and report partial application. +// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement. diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 3fbdc9b7b4..b2b113be83 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -4,7 +4,7 @@ import path from "path" import { pathToFileURL } from "url" import fuzzysort from "fuzzysort" import ignore from "ignore" -import { Context, Effect, Layer, Schema, Stream } from "effect" +import { Context, Effect, Layer, Option, Schema, Stream } from "effect" import { EventV2 } from "./event" import { FSUtil } from "./fs-util" import { Global } from "./global" @@ -16,17 +16,22 @@ import { Ripgrep } from "./filesystem/ripgrep" export const ReadInput = Schema.Struct({ path: RelativePath, - reference: Schema.String.pipe(Schema.optional), + reference: Schema.NonEmptyString.pipe(Schema.optional), }) export type ReadInput = typeof ReadInput.Type -export class TextContent extends Schema.Class("LocationFileSystem.TextContent")({ +export const MAX_READ_LINES = 2_000 +export const MAX_READ_BYTES = 50 * 1024 +const MAX_LINE_LENGTH = 2_000 +const MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)` + +export class TextContent extends Schema.Class("FileSystem.TextContent")({ type: Schema.Literal("text"), content: Schema.String, mime: Schema.String, }) {} -export class BinaryContent extends Schema.Class("LocationFileSystem.BinaryContent")({ +export class BinaryContent extends Schema.Class("FileSystem.BinaryContent")({ type: Schema.Literal("binary"), content: Schema.String, encoding: Schema.Literal("base64"), @@ -36,19 +41,80 @@ export class BinaryContent extends Schema.Class("LocationFileSyst export const Content = Schema.Union([TextContent, BinaryContent]).pipe(Schema.toTaggedUnion("type")) export type Content = typeof Content.Type +export const TextPageInput = Schema.Struct({ + offset: PositiveInt.pipe(Schema.optional), + limit: PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_READ_LINES)).pipe(Schema.optional), +}) +export type TextPageInput = typeof TextPageInput.Type + +export class TextPage extends Schema.Class("FileSystem.TextPage")({ + type: Schema.Literal("text-page"), + content: Schema.String, + mime: Schema.String, + offset: PositiveInt, + truncated: Schema.Boolean, + next: PositiveInt.pipe(Schema.optional), +}) {} + +export class ReadTarget extends Schema.Class("FileSystem.ReadTarget")({ + real: Schema.String, + resource: Schema.String, + size: NonNegativeInt, + dev: Schema.Number, + ino: Schema.Number.pipe(Schema.optional), +}) {} + export const ListInput = Schema.Struct({ path: RelativePath.pipe(Schema.optional), - reference: Schema.String.pipe(Schema.optional), + reference: Schema.NonEmptyString.pipe(Schema.optional), }) export type ListInput = typeof ListInput.Type -export class Entry extends Schema.Class("LocationFileSystem.Entry")({ +export const ListPageInput = Schema.Struct({ + ...ListInput.fields, + offset: PositiveInt.pipe(Schema.optional), + limit: PositiveInt.check(Schema.isLessThanOrEqualTo(2_000)).pipe(Schema.optional), +}) +export type ListPageInput = typeof ListPageInput.Type + +export class ListTarget extends Schema.Class("FileSystem.ListTarget")({ + absolute: Schema.String, + real: Schema.String, + directory: Schema.String, + root: Schema.String, + resource: Schema.String, +}) {} + +/** Canonical read authority for Location-scoped search and metadata leaves. */ +export class RootTarget extends Schema.Class("FileSystem.RootTarget")({ + absolute: Schema.String, + real: Schema.String, + directory: Schema.String, + root: Schema.String, + resource: Schema.String, + reference: Schema.NonEmptyString.pipe(Schema.optional), + type: Schema.Literals(["file", "directory"]), + dev: Schema.Number, + ino: Schema.Number.pipe(Schema.optional), +}) {} + +export type ReadPathTarget = + | { readonly type: "file"; readonly target: ReadTarget } + | { readonly type: "directory"; readonly target: ListTarget } + +export class Entry extends Schema.Class("FileSystem.Entry")({ path: RelativePath, uri: Schema.String, type: Schema.Literals(["file", "directory"]), mime: Schema.String, }) {} +export class ListPage extends Schema.Class("FileSystem.ListPage")({ + entries: Schema.Array(Entry), + truncated: Schema.Boolean, + next: PositiveInt.pipe(Schema.optional), +}) {} + export const FindInput = Schema.Struct({ query: Schema.String, type: Schema.Literals(["file", "directory"]).pipe(Schema.optional), @@ -63,7 +129,7 @@ export const GrepInput = Schema.Struct({ }) export type GrepInput = typeof GrepInput.Type -export class GrepMatch extends Schema.Class("LocationFileSystem.GrepMatch")({ +export class GrepMatch extends Schema.Class("FileSystem.GrepMatch")({ path: RelativePath, lines: Schema.String, line: PositiveInt, @@ -88,7 +154,21 @@ export const Event = { export interface Interface { readonly read: (input: ReadInput) => Effect.Effect + readonly resolveReadPath: (input: ReadInput) => Effect.Effect + readonly resolveRead: (input: ReadInput) => Effect.Effect + readonly readResolved: (target: ReadTarget, maximumBytes?: number) => Effect.Effect + readonly readTextPageResolved: (target: ReadTarget, page?: TextPageInput) => Effect.Effect readonly list: (input?: ListInput) => Effect.Effect + /** Select a contained canonical read root without asserting leaf policy. */ + readonly resolveRoot: (input?: ListInput) => Effect.Effect + readonly revalidateRoot: (target: RootTarget) => Effect.Effect + readonly resolveList: (input?: ListInput) => Effect.Effect + readonly listResolved: (target: ListTarget) => Effect.Effect + readonly listPage: (input?: ListPageInput) => Effect.Effect + readonly listPageResolved: ( + target: ListTarget, + page?: Pick, + ) => Effect.Effect readonly find: (input: FindInput) => Effect.Effect readonly grep: (input: GrepInput) => Effect.Effect readonly isIgnored: (path: RelativePath, type: "file" | "directory") => boolean @@ -183,13 +263,36 @@ export const layer = Layer.effect( return [...files, ...dirs] }) - return Service.of({ - read: Effect.fn("FileSystem.read")(function* (input) { - const file = yield* resolve(input.path, input.reference) - const info = yield* fs.stat(file.real).pipe(Effect.orDie) - if (info.type !== "File") return yield* Effect.die(new Error("Path is not a file")) - const bytes = yield* fs.readFile(file.real).pipe(Effect.orDie) - const mime = FSUtil.mimeType(file.real) + const resolveReadPath = Effect.fn("FileSystem.resolveReadPath")(function* (input: ReadInput) { + const file = yield* resolve(input.path, input.reference) + const info = yield* fs.stat(file.real).pipe(Effect.orDie) + const relative = path.relative(file.root, file.real).replaceAll("\\", "/") + const resource = input.reference === undefined ? relative || "." : `${input.reference}:${relative || "."}` + if (info.type === "File") { + return { + type: "file" as const, + target: new ReadTarget({ + real: file.real, + resource, + size: Number(info.size), + dev: info.dev, + ino: Option.getOrUndefined(info.ino), + }), + } + } + if (info.type === "Directory") { + return { type: "directory" as const, target: new ListTarget({ ...file, resource }) } + } + return yield* Effect.die(new Error("Path is not a file or directory")) + }) + const resolveRead = Effect.fn("FileSystem.resolveRead")(function* (input: ReadInput) { + const resolved = yield* resolveReadPath(input) + if (resolved.type !== "file") return yield* Effect.die(new Error("Path is not a file")) + return resolved.target + }) + const content = (target: ReadTarget, bytes: Uint8Array) => + Effect.gen(function* () { + const mime = FSUtil.mimeType(target.real) if (!bytes.includes(0)) { const content = yield* Effect.sync(() => new TextDecoder("utf-8", { fatal: true }).decode(bytes)).pipe( Effect.option, @@ -202,25 +305,222 @@ export const layer = Layer.effect( encoding: "base64", mime, }) + }) + const readResolved = Effect.fn("FileSystem.readResolved")(function* (target: ReadTarget, maximumBytes?: number) { + if (maximumBytes === undefined) return yield* content(target, yield* fs.readFile(target.real).pipe(Effect.orDie)) + return yield* Effect.scoped( + Effect.gen(function* () { + const file = yield* fs.open(target.real, { flag: "r" }).pipe(Effect.orDie) + const info = yield* file.stat.pipe(Effect.orDie) + if (info.type !== "File") return yield* Effect.die(new Error("Path is not a file")) + if (info.dev !== target.dev || Option.getOrUndefined(info.ino) !== target.ino) + return yield* Effect.die(new Error("File changed after permission approval")) + if (info.size > maximumBytes) + return yield* Effect.die(new Error(`File exceeds ${maximumBytes} byte read limit`)) + const bytes = yield* file.readAlloc(maximumBytes + 1).pipe(Effect.orDie) + if (bytes._tag === "Some" && bytes.value.length > maximumBytes) + return yield* Effect.die(new Error(`File exceeds ${maximumBytes} byte read limit`)) + return yield* content(target, bytes._tag === "Some" ? bytes.value : new Uint8Array()) + }), + ) + }) + const readTextPageResolved = Effect.fn("FileSystem.readTextPageResolved")(function* ( + target: ReadTarget, + page: TextPageInput = {}, + ) { + return yield* Effect.scoped( + Effect.gen(function* () { + const file = yield* fs.open(target.real, { flag: "r" }).pipe(Effect.orDie) + const info = yield* file.stat.pipe(Effect.orDie) + if (info.type !== "File") return yield* Effect.die(new Error("Path is not a file")) + if (info.dev !== target.dev || Option.getOrUndefined(info.ino) !== target.ino) + return yield* Effect.die(new Error("File changed after permission approval")) + + const offset = page.offset ?? 1 + const limit = Math.min(page.limit ?? MAX_READ_LINES, MAX_READ_LINES) + const lines: string[] = [] + const decoder = new TextDecoder("utf-8", { fatal: true }) + let pending = "" + let discard = false + let line = 1 + let bytes = 0 + let found = false + let truncated = false + let next: number | undefined + + const append = (input: string) => { + if (line < offset) { + line++ + return true + } + if (lines.length >= limit) { + truncated = true + next = line + return false + } + found = true + const text = input.length > MAX_LINE_LENGTH ? input.slice(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX : input + const size = Buffer.byteLength(text, "utf-8") + (lines.length > 0 ? 1 : 0) + if (bytes + size > MAX_READ_BYTES) { + truncated = true + next = line + return false + } + lines.push(text) + bytes += size + line++ + return true + } + + let done = false + while (!done) { + const chunk = yield* file.readAlloc(64 * 1024).pipe(Effect.orDie) + if (Option.isNone(chunk)) break + if (chunk.value.includes(0)) return yield* Effect.die(new Error("Cannot page binary file")) + let text = decoder.decode(chunk.value, { stream: true }) + while (true) { + const index = text.indexOf("\n") + if (index === -1) { + if (!discard) { + pending += text + if (pending.length > MAX_LINE_LENGTH) { + pending = pending.slice(0, MAX_LINE_LENGTH + 1) + discard = true + } + } + break + } + const current = pending + (discard ? "" : text.slice(0, index)) + pending = "" + discard = false + text = text.slice(index + 1) + if (!append(current.endsWith("\r") ? current.slice(0, -1) : current)) { + done = true + break + } + } + } + if (!done) { + const tail = decoder.decode() + if (!discard) pending += tail + if (pending && !append(pending.endsWith("\r") ? pending.slice(0, -1) : pending)) done = true + } + if (!done && !found && offset !== 1) return yield* Effect.die(new Error(`Offset ${offset} is out of range`)) + + return new TextPage({ + type: "text-page", + content: lines.join("\n"), + mime: FSUtil.mimeType(target.real), + offset, + truncated, + ...(next === undefined ? {} : { next }), + }) + }), + ) + }) + const resolveList = Effect.fn("FileSystem.resolveList")(function* (input: ListInput = {}) { + const directory = yield* resolve(input.path, input.reference) + const info = yield* fs.stat(directory.real).pipe(Effect.orDie) + if (info.type !== "Directory") return yield* Effect.die(new Error("Path is not a directory")) + const relative = path.relative(directory.root, directory.real).replaceAll("\\", "/") || "." + return new ListTarget({ + ...directory, + resource: input.reference === undefined ? relative : `${input.reference}:${relative}`, + }) + }) + const resolveRoot = Effect.fn("FileSystem.resolveRoot")(function* (input: ListInput = {}) { + const target = yield* resolve(input.path, input.reference) + const info = yield* fs.stat(target.real).pipe(Effect.orDie) + const type = info.type === "File" ? "file" : info.type === "Directory" ? "directory" : undefined + if (!type) return yield* Effect.die(new Error("Path is not a file or directory")) + const relative = path.relative(target.root, target.real).replaceAll("\\", "/") || "." + return new RootTarget({ + ...target, + resource: input.reference === undefined ? relative : `${input.reference}:${relative}`, + reference: input.reference, + type, + dev: info.dev, + ino: Option.getOrUndefined(info.ino), + }) + }) + const revalidateRoot = Effect.fn("FileSystem.revalidateRoot")(function* (target: RootTarget) { + const canonical = yield* fs.realPath(target.absolute).pipe(Effect.orDie) + if (canonical !== target.real) return yield* Effect.die(new Error("Search root changed after approval")) + const info = yield* fs.stat(canonical).pipe(Effect.orDie) + if (info.type !== (target.type === "file" ? "File" : "Directory") || info.dev !== target.dev || Option.getOrUndefined(info.ino) !== target.ino) + return yield* Effect.die(new Error("Search root identity changed after approval")) + return target + }) + const listResolved = Effect.fn("FileSystem.listResolved")(function* (directory: ListTarget) { + return yield* fs.readDirectoryEntries(directory.real).pipe( + Effect.orDie, + Effect.flatMap((items) => + Effect.forEach(items, (item) => entry(path.join(directory.absolute, item.name), directory), { + concurrency: "unbounded", + }), + ), + Effect.map((items) => + items + .filter((item): item is Entry => item !== undefined) + .sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === "directory" ? -1 : 1)), + ), + ) + }) + const listPageResolved = Effect.fn("FileSystem.listPageResolved")(function* ( + target: ListTarget, + page: Pick = {}, + ) { + type Candidate = Entry | { readonly name: string; readonly type: "file" | "directory" } + const offset = page.offset ?? 1 + const limit = Math.min(page.limit ?? 2_000, 2_000) + const items = yield* fs.readDirectoryEntries(target.real).pipe(Effect.orDie) + const candidates = yield* Effect.forEach( + items, + (item): Effect.Effect => { + if (item.type === "other") return Effect.succeed(undefined) + if (item.type === "symlink") return entry(path.join(target.absolute, item.name), target) + return Effect.succeed({ name: item.name, type: item.type } as const) + }, + { concurrency: 16 }, + ).pipe(Effect.map((items) => items.filter((item): item is Candidate => item !== undefined))) + candidates.sort((a, b) => { + return a.type === b.type + ? (a instanceof Entry ? a.path : a.name).localeCompare(b instanceof Entry ? b.path : b.name) + : a.type === "directory" + ? -1 + : 1 + }) + const selected = candidates.slice(offset - 1, offset - 1 + limit) + const entries = yield* Effect.forEach( + selected, + (item) => (item instanceof Entry ? Effect.succeed(item) : entry(path.join(target.absolute, item.name), target)), + { + concurrency: 16, + }, + ).pipe(Effect.map((items) => items.filter((item): item is Entry => item !== undefined))) + const truncated = offset - 1 + selected.length < candidates.length + return new ListPage({ entries, truncated, ...(truncated ? { next: offset + selected.length } : {}) }) + }) + + return Service.of({ + read: Effect.fn("FileSystem.read")(function* (input) { + return yield* readResolved(yield* resolveRead(input)) }), - list: Effect.fn("FileSystem.list")(function* (input = {}) { - const directory = yield* resolve(input.path, input.reference) - const info = yield* fs.stat(directory.real).pipe(Effect.orDie) - if (info.type !== "Directory") return yield* Effect.die(new Error("Path is not a directory")) - return yield* fs.readDirectoryEntries(directory.real).pipe( - Effect.orDie, - Effect.flatMap((items) => - Effect.forEach(items, (item) => entry(path.join(directory.absolute, item.name), directory), { - concurrency: "unbounded", - }), - ), - Effect.map((items) => - items - .filter((item): item is Entry => item !== undefined) - .sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === "directory" ? -1 : 1)), - ), - ) + resolveReadPath, + resolveRead, + readResolved, + readTextPageResolved, + list: Effect.fn("FileSystem.list")(function* (input) { + return yield* listResolved(yield* resolveList(input)) }), + resolveRoot, + revalidateRoot, + resolveList, + listResolved, + listPage: Effect.fn("FileSystem.listPage")(function* (input) { + return yield* listPageResolved(yield* resolveList(input), input) + }), + listPageResolved, find: Effect.fn("FileSystem.find")(function* (input) { const items = (yield* scan()).filter((item) => input.type !== "file" || !item.endsWith("/")) const filtered = items.filter((item) => input.type !== "directory" || item.endsWith("/")) diff --git a/packages/core/src/filesystem/ripgrep.ts b/packages/core/src/filesystem/ripgrep.ts index d047e21c03..b8a171b9e3 100644 --- a/packages/core/src/filesystem/ripgrep.ts +++ b/packages/core/src/filesystem/ripgrep.ts @@ -135,6 +135,7 @@ export interface TreeInput { } export interface Interface { + readonly filepath: Effect.Effect readonly files: (input: FilesInput) => Stream.Stream readonly tree: (input: TreeInput) => Effect.Effect readonly search: (input: SearchInput) => Effect.Effect @@ -471,7 +472,7 @@ export const layer: Layer.Layer()("@opencode/example/LocationServiceMap", { lookup: (ref: Location.Ref) => { const location = Location.layer(ref) - return Layer.mergeAll( + const permissionsAndTools = ToolRegistry.layer.pipe(Layer.provideMerge(PermissionV2.locationLayer)) + const services = Layer.mergeAll( location, Policy.locationLayer, Config.locationLayer, @@ -36,12 +53,41 @@ export class LocationServiceMap extends LayerMap.Service()(" Catalog.locationLayer, AgentV2.locationLayer, PluginBoot.locationLayer, - PermissionV2.locationLayer, FileSystem.locationLayer, Watcher.locationLayer, Pty.locationLayer, SkillV2.locationLayer, - ).pipe(Layer.provideMerge(location), Layer.fresh) + permissionsAndTools, + LocationMutation.locationLayer.pipe(Layer.orDie), + ).pipe(Layer.provideMerge(location)) + const commits = FileMutation.locationLayer.pipe(Layer.provide(services)) + const searches = LocationSearch.layer.pipe(Layer.provide(Ripgrep.layer), Layer.provide(services)) + const resources = ToolOutputStore.layer.pipe(Layer.provide(services)) + const todos = SessionTodo.layer.pipe(Layer.provide(services)) + const questions = QuestionV2.locationLayer.pipe(Layer.provide(services)) + const builtInTools = BuiltInTools.locationLayer.pipe( + Layer.provide(services), + Layer.provide(commits), + Layer.provide(searches), + Layer.provide(resources), + Layer.provide(todos), + Layer.provide(questions), + ) + const model = SessionRunnerModel.locationLayer.pipe(Layer.provide(services)) + const runner = SessionRunnerLLM.defaultLayer.pipe(Layer.provide(services), Layer.provide(model)) + const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner)) + return Layer.mergeAll( + services, + commits, + searches, + resources, + todos, + questions, + model, + runner, + coordinator, + builtInTools, + ).pipe(Layer.fresh) }, idleTimeToLive: "60 minutes", dependencies: [ @@ -51,10 +97,14 @@ export class LocationServiceMap extends LayerMap.Service()(" Npm.defaultLayer, ModelsDev.defaultLayer, FSUtil.defaultLayer, + AppProcess.defaultLayer, Global.defaultLayer, Database.defaultLayer, - SessionV2.defaultLayer, + SessionStore.layer.pipe(Layer.provide(Database.defaultLayer)), PermissionSaved.defaultLayer, RepositoryCache.defaultLayer, + LLMClient.layer.pipe(Layer.provide(RequestExecutor.defaultLayer)), + FetchHttpClient.layer, + ToolOutputStore.defaultCleanupLayer, ], }) {} diff --git a/packages/core/src/location-mutation.ts b/packages/core/src/location-mutation.ts new file mode 100644 index 0000000000..b000be8a38 --- /dev/null +++ b/packages/core/src/location-mutation.ts @@ -0,0 +1,305 @@ +export * as LocationMutation from "./location-mutation" + +import path from "path" +import { Context, Effect, Layer, Option, Schema } from "effect" +import { FSUtil } from "./fs-util" +import { Location } from "./location" + +export const Kind = Schema.Literals(["file", "directory"]) +export type Kind = typeof Kind.Type + +/** + * Mutation paths do not accept project references. Relative paths must stay + * inside the active Location. Absolute paths outside it require separate + * `external_directory` approval. + */ +export const ResolveInput = Schema.Struct({ + path: Schema.String, + /** Selects the external approval boundary; it does not validate the target type. */ + kind: Kind.pipe(Schema.optional), +}) +export type ResolveInput = typeof ResolveInput.Type + +export class PathError extends Schema.TaggedErrorClass()("LocationMutation.PathError", { + path: Schema.String, + reason: Schema.Literals([ + "relative_escape", + "location_escape", + "non_directory_ancestor", + "unresolved_symlink", + "location_identity_changed", + ]), +}) {} + +export class RevalidationError extends Schema.TaggedErrorClass()( + "LocationMutation.RevalidationError", + { + path: Schema.String, + reason: Schema.String, + }, +) {} + +export interface Identity { + /** Canonical path for this saved filesystem identity. */ + readonly canonical: string + readonly dev: number + readonly ino?: number +} + +export interface ExternalDirectoryAuthorization { + readonly action: "external_directory" + /** Canonical existing directory used as the external approval boundary. */ + readonly directory: string + /** `external_directory` permission resource. */ + readonly resource: string + readonly save: string + /** Saved identity checked again after approval to detect swaps. */ + readonly authority: Identity +} + +/** Build the `external_directory` permission request. */ +export const externalDirectoryPermission = (input: ExternalDirectoryAuthorization) => ({ + action: input.action, + resources: [input.resource], + save: [input.save], +}) + +export interface Target { + /** Canonical existing path, or missing path below a canonical directory. */ + readonly canonical: string + readonly exists: boolean + readonly type?: + | "File" + | "Directory" + | "SymbolicLink" + | "BlockDevice" + | "CharacterDevice" + | "FIFO" + | "Socket" + | "Unknown" + /** Permission resource: Location-relative for internal paths, canonical for external paths. */ + readonly resource: string + readonly externalDirectory?: ExternalDirectoryAuthorization +} + +/** + * A path checked before permission approval. + * + * resolve(path) -> Plan -> approve -> revalidate(plan) -> mutate immediately + * + * Tools must approve `target.externalDirectory`, when present, and their normal + * mutation action before calling `revalidate`. Revalidation rejects escapes, + * symlinks in missing suffixes, and changes made while approval is pending. It + * cannot be atomic with the next filesystem call, so mutate immediately afterward. + */ +export interface Plan { + readonly input: ResolveInput + readonly target: Target + /** Saved identity of the existing target or nearest existing ancestor. */ + readonly authority: Identity +} + +export interface Interface { + /** + * Check a path before approval and derive its permission resources. Relative + * paths must stay inside the Location. Absolute paths outside it require + * separate `external_directory` approval. This does not approve the tool's + * mutation action. + */ + readonly resolve: (input: ResolveInput) => Effect.Effect + /** + * Check the plan again immediately before mutation. Reject changes to the + * target, its saved identity, or approval resources. Mutate the returned + * target immediately. + */ + readonly revalidate: (plan: Plan) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/LocationMutation") {} + +interface ResolvedPath { + readonly canonical: string + readonly exists: boolean + readonly type?: Target["type"] + readonly authority: Identity +} + +const slash = (value: string) => value.replaceAll("\\", "/") + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const location = yield* Location.Service + const locationRoot = yield* fs.realPath(location.directory) + const locationAuthority = yield* identity(locationRoot) + + function identityFrom(canonical: string, info: Effect.Success>): Identity { + return { + canonical, + dev: info.dev, + ino: Option.getOrUndefined(info.ino), + } + } + + function identity(canonical: string) { + return fs.stat(canonical).pipe(Effect.map((info) => identityFrom(canonical, info))) + } + + function notFound(effect: Effect.Effect) { + return effect.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined))) + } + + function sameIdentity(left: Identity, right: Identity) { + return left.canonical === right.canonical && left.dev === right.dev && left.ino === right.ino + } + + /** Check whether a saved path still points to the same filesystem object. */ + const assertIdentity = Effect.fnUntraced(function* (expected: Identity) { + const canonical = yield* notFound(fs.realPath(expected.canonical)) + if (canonical === undefined) return false + const actual = yield* notFound(identity(canonical)) + if (actual === undefined) return false + return canonical === expected.canonical && sameIdentity(expected, actual) + }) + + const assertLocationIdentity = Effect.fnUntraced(function* (requested: string) { + if (yield* assertIdentity(locationAuthority)) return + return yield* new PathError({ path: requested, reason: "location_identity_changed" }) + }) + + const hasUnresolvedSymlink = Effect.fnUntraced(function* (anchor: string, suffix: string) { + let current = anchor + for (const part of suffix.split(path.sep)) { + if (!part) continue + current = path.join(current, part) + if ( + yield* fs.readLink(current).pipe( + Effect.as(true), + Effect.catch(() => Effect.succeed(false)), + ) + ) + return true + } + return false + }) + + /** + * Resolve a path to a canonical target and save an existing filesystem + * identity for later revalidation. + * + * existing path -> save target identity + * missing path -> save nearest existing directory identity + * + * Missing suffixes must not contain symlinks. + */ + const resolvePath = Effect.fnUntraced(function* (absolute: string) { + const existing = yield* notFound(fs.realPath(absolute)) + if (existing !== undefined) { + const info = yield* fs.stat(existing) + return { + canonical: existing, + exists: true, + type: info.type, + authority: identityFrom(existing, info), + } satisfies ResolvedPath + } + + let anchor = path.dirname(absolute) + while (true) { + const canonical = yield* notFound(fs.realPath(anchor)) + if (canonical !== undefined) { + const info = yield* fs.stat(canonical) + if (info.type !== "Directory") + return yield* new PathError({ path: absolute, reason: "non_directory_ancestor" }) + const suffix = path.relative(anchor, absolute) + if (yield* hasUnresolvedSymlink(anchor, suffix)) { + return yield* new PathError({ path: absolute, reason: "unresolved_symlink" }) + } + return { + canonical: path.resolve(canonical, suffix), + exists: false, + authority: identityFrom(canonical, info), + } satisfies ResolvedPath + } + const parent = path.dirname(anchor) + if (parent === anchor) return yield* new PathError({ path: absolute, reason: "non_directory_ancestor" }) + anchor = parent + } + }) + + /** + * Choose the existing directory used for separate external approval. + * + * existing directory target -> "/*" + * file or missing target -> "/*" + */ + const externalDirectory = Effect.fnUntraced(function* (resolved: ResolvedPath, kind: Kind) { + const candidate = + kind === "directory" && resolved.type === "Directory" ? resolved.canonical : path.dirname(resolved.canonical) + const boundary = yield* resolvePath(candidate) + const directory = + boundary.exists && boundary.type === "Directory" ? boundary.canonical : boundary.authority.canonical + const resource = slash(path.join(directory, "*")) + return { action: "external_directory" as const, directory, resource, save: resource, authority: boundary.authority } + }) + + const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) { + yield* assertLocationIdentity(input.path) + const relative = !path.isAbsolute(input.path) + const absolute = path.resolve(location.directory, input.path) + const lexicallyInternal = FSUtil.contains(location.directory, absolute) + if (relative && !lexicallyInternal) return yield* new PathError({ path: input.path, reason: "relative_escape" }) + + const resolved = yield* resolvePath(absolute) + if (lexicallyInternal && !FSUtil.contains(locationRoot, resolved.canonical)) { + return yield* new PathError({ path: input.path, reason: "location_escape" }) + } + + const external = !lexicallyInternal + const resource = external + ? slash(resolved.canonical) + : slash(path.relative(locationRoot, resolved.canonical) || ".") + const target: Target = { + canonical: resolved.canonical, + exists: resolved.exists, + type: resolved.type, + resource, + externalDirectory: external ? yield* externalDirectory(resolved, input.kind ?? "file") : undefined, + } + return { input, target, authority: resolved.authority } satisfies Plan + }) + + /** + * Re-resolve a plan immediately before mutation and reject any changed + * identity, target, or approval resource. This reduces the race window but + * cannot make the next filesystem call atomic. + */ + const revalidate = Effect.fn("LocationMutation.revalidate")(function* (plan: Plan) { + const invalid = (reason: string) => new RevalidationError({ path: plan.input.path, reason }) + const fresh = yield* resolve(plan.input).pipe( + Effect.mapError((error) => (error instanceof PathError ? invalid(error.reason) : error)), + ) + if (!sameIdentity(fresh.authority, plan.authority)) return yield* invalid("mutation authority changed") + if (fresh.target.canonical !== plan.target.canonical) return yield* invalid("canonical mutation target changed") + if (fresh.target.resource !== plan.target.resource) return yield* invalid("mutation resource changed") + if (Boolean(fresh.target.externalDirectory) !== Boolean(plan.target.externalDirectory)) { + return yield* invalid("external directory authority changed") + } + if ( + fresh.target.externalDirectory && + plan.target.externalDirectory && + (fresh.target.externalDirectory.directory !== plan.target.externalDirectory.directory || + fresh.target.externalDirectory.resource !== plan.target.externalDirectory.resource || + !sameIdentity(fresh.target.externalDirectory.authority, plan.target.externalDirectory.authority)) + ) { + return yield* invalid("external directory authority changed") + } + return fresh.target + }) + + return Service.of({ resolve, revalidate }) + }), +) + +export const locationLayer = layer diff --git a/packages/core/src/location-search.ts b/packages/core/src/location-search.ts new file mode 100644 index 0000000000..d162ea2073 --- /dev/null +++ b/packages/core/src/location-search.ts @@ -0,0 +1,195 @@ +export * as LocationSearch from "./location-search" + +import path from "path" +import { Context, Effect, Layer, Option, Schema } from "effect" +import { FileSystem } from "./filesystem" +import { FSUtil } from "./fs-util" +import { Ripgrep } from "./ripgrep" +import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" + +/** + * Location-scoped raw search substrate. Search authority is selected only by + * FileSystem, preserving Location-relative paths and named read + * references. Model formatting, leaf-tool permissions, and HTTP transport stay + * outside this service so future GlobTool, GrepTool, and HTTP consumers can + * share the same bounded filesystem behavior. + * + * TODO: Expose this substrate through HTTP fs.search/fs.grep endpoints. + * TODO: Reuse this substrate for instruction and skill discovery where suitable. + */ + +export const DEFAULT_RESULT_LIMIT = 100 +export const MAX_RESULT_LIMIT = 100 +export const MAX_LINE_PREVIEW_LENGTH = 2_000 + +export const ResultLimit = PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_RESULT_LIMIT)) + +const RootInput = { + path: RelativePath.pipe(Schema.optional), + reference: Schema.NonEmptyString.pipe(Schema.optional), +} + +export const FilesInput = Schema.Struct({ + pattern: Schema.String, + ...RootInput, + limit: ResultLimit.pipe(Schema.optional), +}) +export type FilesInput = typeof FilesInput.Type & { readonly signal?: AbortSignal } + +export const GrepInput = Schema.Struct({ + pattern: Schema.String, + include: Schema.String.pipe(Schema.optional), + ...RootInput, + limit: ResultLimit.pipe(Schema.optional), +}) +export type GrepInput = typeof GrepInput.Type & { readonly signal?: AbortSignal } + +export class File extends Schema.Class("LocationSearch.File")({ + path: RelativePath, + canonical: Schema.String, + resource: Schema.String, + mtime: Schema.Number, +}) {} + +export class Submatch extends Schema.Class("LocationSearch.Submatch")({ + text: Schema.String, + start: NonNegativeInt, + end: NonNegativeInt, +}) {} + +export class Match extends Schema.Class("LocationSearch.Match")({ + path: RelativePath, + canonical: Schema.String, + resource: Schema.String, + lines: Schema.String, + linePreviewTruncated: Schema.Boolean, + line: PositiveInt, + offset: NonNegativeInt, + submatches: Schema.Array(Submatch), + mtime: Schema.Number, +}) {} + +export class FilesResult extends Schema.Class("LocationSearch.FilesResult")({ + items: Schema.Array(File), + truncated: Schema.Boolean, + partial: Schema.Boolean, +}) {} + +export class GrepResult extends Schema.Class("LocationSearch.GrepResult")({ + items: Schema.Array(Match), + truncated: Schema.Boolean, + partial: Schema.Boolean, +}) {} + +export interface Interface { + readonly files: (input: FilesInput, root?: FileSystem.RootTarget) => Effect.Effect + readonly grep: (input: GrepInput, root?: FileSystem.RootTarget) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/LocationSearch") {} + +const slash = (value: string) => value.replaceAll("\\", "/") +const cap = (limit?: number) => Math.min(limit ?? DEFAULT_RESULT_LIMIT, MAX_RESULT_LIMIT) + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const filesystem = yield* FileSystem.Service + const ripgrep = yield* Ripgrep.Service + + const candidate = Effect.fnUntraced(function* (root: FileSystem.RootTarget, cwd: string, value: string) { + const absolute = path.resolve(cwd, value) + const lexicallyContained = + root.type === "directory" ? FSUtil.contains(root.real, absolute) : absolute === root.real + if (!lexicallyContained) return + const canonical = yield* fs.realPath(absolute).pipe(Effect.catch(() => Effect.void)) + if (!canonical || !FSUtil.contains(root.root, canonical)) return + const info = yield* fs.stat(canonical).pipe(Effect.catch(() => Effect.void)) + if (!info || info.type !== "File") return + const relative = slash(path.relative(root.root, canonical)) + return { + path: RelativePath.make(relative), + canonical, + resource: root.reference === undefined ? relative : `${root.reference}:${relative}`, + mtime: info.mtime.pipe( + Option.map((date) => date.getTime()), + Option.getOrElse(() => 0), + ), + } + }) + + return Service.of({ + files: Effect.fn("LocationSearch.files")(function* (input, approvedRoot) { + const root = yield* filesystem.revalidateRoot(approvedRoot ?? (yield* filesystem.resolveRoot(input))) + if (root.type !== "directory") + return yield* Effect.die(new globalThis.Error("Files search path must be a directory")) + const result = yield* ripgrep.files({ + cwd: root.real, + pattern: input.pattern, + limit: cap(input.limit), + signal: input.signal, + }) + const mapped = yield* Effect.forEach(result.items, (item) => candidate(root, root.real, item), { + concurrency: 16, + }) + const items = mapped.filter((item): item is File => item !== undefined).map((item) => new File(item)) + // TODO: Decide result ordering policy: V1 mtime sorting versus stable path ordering. + // TODO: Report inaccessible paths discovered after bounded ripgrep termination when practical. + return new FilesResult({ + items, + truncated: result.truncated, + partial: result.partial || items.length !== result.items.length, + }) + }), + grep: Effect.fn("LocationSearch.grep")(function* (input, approvedRoot) { + const root = yield* filesystem.revalidateRoot(approvedRoot ?? (yield* filesystem.resolveRoot(input))) + const cwd = root.type === "directory" ? root.real : path.dirname(root.real) + const result = yield* ripgrep.grep({ + cwd, + pattern: input.pattern, + include: input.include, + file: root.type === "file" ? path.basename(root.real) : undefined, + limit: cap(input.limit), + signal: input.signal, + }) + const candidates = new Map>() + for (const item of result.items) { + if (!candidates.has(item.path.text)) { + candidates.set(item.path.text, yield* Effect.cached(candidate(root, cwd, item.path.text))) + } + } + const mapped = yield* Effect.forEach( + result.items, + (item) => + candidates.get(item.path.text)!.pipe( + Effect.map( + (file) => + file && + new Match({ + ...file, + lines: item.lines.text.slice(0, MAX_LINE_PREVIEW_LENGTH), + linePreviewTruncated: item.lines.text.length > MAX_LINE_PREVIEW_LENGTH, + line: item.line_number, + offset: item.absolute_offset, + submatches: item.submatches.map( + (submatch) => + new Submatch({ text: submatch.match.text, start: submatch.start, end: submatch.end }), + ), + }), + ), + ), + { concurrency: 16 }, + ) + const items = mapped.filter((item): item is Match => item !== undefined) + // TODO: Decide result ordering policy: V1 mtime sorting versus stable path ordering. + // TODO: Report inaccessible paths discovered after bounded ripgrep termination when practical. + return new GrepResult({ + items, + truncated: result.truncated, + partial: result.partial || items.length !== result.items.length, + }) + }), + }) + }), +) diff --git a/packages/core/src/location.ts b/packages/core/src/location.ts index 9613885c97..f3d4ff10a1 100644 --- a/packages/core/src/location.ts +++ b/packages/core/src/location.ts @@ -1,18 +1,19 @@ import { Context, Effect, Layer, Schema } from "effect" import { Project } from "./project" import { AbsolutePath } from "./schema" +import { WorkspaceV2 } from "./workspace" export * as Location from "./location" export const Ref = Schema.Struct({ directory: AbsolutePath, - workspaceID: Schema.optional(Schema.String), + workspaceID: Schema.optional(WorkspaceV2.ID), }).annotate({ identifier: "Location.Ref" }) export type Ref = typeof Ref.Type export interface Interface { readonly directory: AbsolutePath - readonly workspaceID?: string + readonly workspaceID?: WorkspaceV2.ID readonly project: { readonly id: Project.ID readonly directory: AbsolutePath diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index a57647c4da..a98775bee8 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -52,6 +52,18 @@ export const Api = Schema.Union([ ]).pipe(Schema.toTaggedUnion("type")) export type Api = typeof Api.Type +export const PublicApi = Schema.Union([ + Schema.Struct({ + id: ID, + ...ProviderV2.PublicAISDK.fields, + }), + Schema.Struct({ + id: ID, + ...ProviderV2.PublicNative.fields, + }), +]).pipe(Schema.toTaggedUnion("type")) +export type PublicApi = typeof PublicApi.Type + export class Info extends Schema.Class("ModelV2.Info")({ id: ID, providerID: ProviderV2.ID, @@ -113,6 +125,64 @@ export class Info extends Schema.Class("ModelV2.Info")({ } } +export class PublicInfo extends Schema.Class("ModelV2.PublicInfo")({ + id: ID, + providerID: ProviderV2.ID, + family: Family.pipe(Schema.optional), + name: Schema.String, + api: PublicApi, + capabilities: Capabilities, + variants: Schema.Struct({ + id: VariantID, + }).pipe(Schema.Array), + time: Schema.Struct({ + released: DateTimeUtcFromMillis, + }), + cost: Cost.pipe(Schema.Array), + status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), + enabled: Schema.Boolean, + limit: Schema.Struct({ + context: Schema.Int, + input: Schema.Int.pipe(Schema.optional), + output: Schema.Int, + }), +}) {} + +export function toPublic(info: Info): PublicInfo { + const api = + info.api.type === "aisdk" + ? { + id: info.api.id, + type: info.api.type, + package: info.api.package, + url: ProviderV2.sanitizePublicUrl(info.api.url), + } + : { id: info.api.id, type: info.api.type, url: ProviderV2.sanitizePublicUrl(info.api.url) } + return new PublicInfo({ + id: info.id, + providerID: info.providerID, + family: info.family, + name: info.name, + api, + capabilities: { + tools: info.capabilities.tools, + input: [...info.capabilities.input], + output: [...info.capabilities.output], + }, + variants: info.variants.map((variant) => ({ id: variant.id })), + time: { released: info.time.released }, + cost: info.cost.map((cost) => ({ + tier: cost.tier && { ...cost.tier }, + input: cost.input, + output: cost.output, + cache: { ...cost.cache }, + })), + status: info.status, + enabled: info.enabled, + limit: { ...info.limit }, + }) +} + export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } { const [providerID, ...modelID] = input.split("/") return { diff --git a/packages/core/src/opencode.ts b/packages/core/src/opencode.ts new file mode 100644 index 0000000000..052536e364 --- /dev/null +++ b/packages/core/src/opencode.ts @@ -0,0 +1,39 @@ +export * as OpenCode from "./opencode" + +import { Context, Effect, Layer } from "effect" +import { Database } from "./database/database" +import { EventV2 } from "./event" +import { LocationServiceMap } from "./location-layer" +import { ProjectV2 } from "./project" +import { SessionV2 } from "./session" +import { SessionProjector } from "./session/projector" +import * as SessionExecutionLocal from "./session/execution/local" +import { SessionStore } from "./session/store" + +export interface Interface { + readonly sessions: SessionV2.Interface +} + +/** Public embedded OpenCode API for Effect-native applications. */ +export class Service extends Context.Service()("@opencode/OpenCode") {} + +const DefaultSessions = SessionV2.layer.pipe( + Layer.provide(SessionProjector.layer), + Layer.provide(SessionExecutionLocal.layer), + Layer.provide(LocationServiceMap.layer), + Layer.provide(SessionStore.layer), + Layer.provide(EventV2.layer), + Layer.provide(Database.defaultLayer), + Layer.provide(ProjectV2.defaultLayer), + Layer.orDie, +) + +// TODO: Accept explicit storage so tests and embeddings can select disposable or application-owned persistence. +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + return Service.of({ sessions: yield* SessionV2.Service }) + }), + ).pipe(Layer.provide(DefaultSessions)) + +// TODO: Add OpenCode.create(...) as the Promise facade over the same embedded API semantics. diff --git a/packages/core/src/patch.ts b/packages/core/src/patch.ts new file mode 100644 index 0000000000..2a0f83e747 --- /dev/null +++ b/packages/core/src/patch.ts @@ -0,0 +1,179 @@ +export * as Patch from "./patch" + +export type Hunk = + | { readonly type: "add"; readonly path: string; readonly contents: string } + | { readonly type: "delete"; readonly path: string } + | { readonly type: "update"; readonly path: string; readonly movePath?: string; readonly chunks: ReadonlyArray } + +export interface UpdateFileChunk { + readonly oldLines: ReadonlyArray + readonly newLines: ReadonlyArray + readonly changeContext?: string + readonly endOfFile?: boolean +} + +export interface FileUpdate { + readonly content: string + readonly bom: boolean +} + +export function parse(patchText: string): ReadonlyArray { + const lines = stripHeredoc(patchText.trim()).split("\n") + const begin = lines.findIndex((line) => line.trim() === "*** Begin Patch") + const end = lines.findIndex((line) => line.trim() === "*** End Patch") + if (begin === -1 || end === -1 || begin >= end) throw new Error("Invalid patch format: missing Begin/End markers") + + const hunks: Hunk[] = [] + let index = begin + 1 + while (index < end) { + const line = lines[index]! + if (line.startsWith("*** Add File:")) { + const path = line.slice("*** Add File:".length).trim() + if (!path) throw new Error("Invalid add file path") + const parsed = parseAdd(lines, index + 1) + hunks.push({ type: "add", path, contents: parsed.content }) + index = parsed.next + continue + } + if (line.startsWith("*** Delete File:")) { + const path = line.slice("*** Delete File:".length).trim() + if (!path) throw new Error("Invalid delete file path") + hunks.push({ type: "delete", path }) + index++ + continue + } + if (line.startsWith("*** Update File:")) { + const path = line.slice("*** Update File:".length).trim() + if (!path) throw new Error("Invalid update file path") + let next = index + 1 + let movePath: string | undefined + if (lines[next]?.startsWith("*** Move to:")) { + movePath = lines[next]!.slice("*** Move to:".length).trim() + if (!movePath) throw new Error("Invalid move file path") + next++ + } + const parsed = parseUpdate(lines, next) + if (parsed.chunks.length === 0) throw new Error(`Invalid update hunk for ${path}: expected at least one @@ chunk`) + hunks.push({ type: "update", path, movePath, chunks: parsed.chunks }) + index = parsed.next + continue + } + throw new Error(`Invalid patch line: ${line}`) + } + return hunks +} + +export function derive(path: string, chunks: ReadonlyArray, original: string): FileUpdate { + const source = splitBom(original) + const lines = source.text.split("\n") + if (lines.at(-1) === "") lines.pop() + const replacements = computeReplacements(lines, path, chunks) + const updated = [...lines] + for (const [start, remove, insert] of replacements.toReversed()) updated.splice(start, remove, ...insert) + if (updated.at(-1) !== "") updated.push("") + const next = splitBom(updated.join("\n")) + return { content: next.text, bom: source.bom || next.bom } +} + +export function joinBom(text: string, bom: boolean) { + const stripped = splitBom(text).text + return bom ? `\uFEFF${stripped}` : stripped +} + +function parseAdd(lines: ReadonlyArray, start: number) { + const content: string[] = [] + let index = start + while (index < lines.length && !lines[index]!.startsWith("***")) { + if (!lines[index]!.startsWith("+")) throw new Error(`Invalid add file line: ${lines[index]}`) + content.push(lines[index]!.slice(1)) + index++ + } + return { content: content.join("\n"), next: index } +} + +function parseUpdate(lines: ReadonlyArray, start: number) { + const chunks: UpdateFileChunk[] = [] + let index = start + while (index < lines.length && !lines[index]!.startsWith("***")) { + if (!lines[index]!.startsWith("@@")) { + throw new Error(`Invalid update file line: ${lines[index]}`) + } + const changeContext = lines[index]!.slice(2).trim() || undefined + const oldLines: string[] = [] + const newLines: string[] = [] + let endOfFile = false + index++ + while (index < lines.length && !lines[index]!.startsWith("@@")) { + const line = lines[index]! + if (line === "*** End of File") { + endOfFile = true + index++ + break + } + if (line.startsWith("***")) break + if (line.startsWith(" ")) { + oldLines.push(line.slice(1)) + newLines.push(line.slice(1)) + } else if (line.startsWith("-")) oldLines.push(line.slice(1)) + else if (line.startsWith("+")) newLines.push(line.slice(1)) + else throw new Error(`Invalid update chunk line: ${line}`) + index++ + } + chunks.push({ oldLines, newLines, changeContext, endOfFile: endOfFile || undefined }) + } + return { chunks, next: index } +} + +function computeReplacements(lines: ReadonlyArray, path: string, chunks: ReadonlyArray) { + const replacements: Array]> = [] + let lineIndex = 0 + for (const chunk of chunks) { + if (chunk.changeContext) { + const context = seek(lines, [chunk.changeContext], lineIndex) + if (context === -1) throw new Error(`Failed to find context '${chunk.changeContext}' in ${path}`) + lineIndex = context + 1 + } + if (chunk.oldLines.length === 0) { + replacements.push([lines.length, 0, chunk.newLines]) + continue + } + let oldLines = chunk.oldLines + let newLines = chunk.newLines + let found = seek(lines, oldLines, lineIndex, chunk.endOfFile) + if (found === -1 && oldLines.at(-1) === "") { + oldLines = oldLines.slice(0, -1) + if (newLines.at(-1) === "") newLines = newLines.slice(0, -1) + found = seek(lines, oldLines, lineIndex, chunk.endOfFile) + } + if (found === -1) throw new Error(`Failed to find expected lines in ${path}:\n${chunk.oldLines.join("\n")}`) + replacements.push([found, oldLines.length, newLines]) + lineIndex = found + oldLines.length + } + return replacements.toSorted((left, right) => left[0] - right[0]) +} + +function seek(lines: ReadonlyArray, pattern: ReadonlyArray, start: number, eof = false) { + if (pattern.length === 0) return -1 + for (const compare of [exact, rstrip, trim, normalized]) { + if (eof) { + const offset = lines.length - pattern.length + if (offset >= start && matches(lines, pattern, offset, compare)) return offset + } + for (let offset = start; offset <= lines.length - pattern.length; offset++) { + if (matches(lines, pattern, offset, compare)) return offset + } + } + return -1 +} + +function matches(lines: ReadonlyArray, pattern: ReadonlyArray, offset: number, compare: (left: string, right: string) => boolean) { + return pattern.every((line, index) => compare(lines[offset + index]!, line)) +} + +const exact = (left: string, right: string) => left === right +const rstrip = (left: string, right: string) => left.trimEnd() === right.trimEnd() +const trim = (left: string, right: string) => left.trim() === right.trim() +const normalized = (left: string, right: string) => normalize(left.trim()) === normalize(right.trim()) +const normalize = (value: string) => value.replace(/[‘’‚‛]/g, "'").replace(/[“”„‟]/g, '"').replace(/[‐‑‒–—―]/g, "-").replace(/…/g, "...").replace(/ /g, " ") +const splitBom = (text: string) => text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text } +const stripHeredoc = (input: string) => input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/)?.[2] ?? input diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index a7c3247202..4cc52b96aa 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -5,6 +5,7 @@ import { EventV2 } from "./event" import { Location } from "./location" import { AgentV2 } from "./agent" import { SessionV2 } from "./session" +import { SessionStore } from "./session/store" import { withStatics } from "./schema" import { Identifier } from "./util/identifier" import { Wildcard } from "./util/wildcard" @@ -135,7 +136,7 @@ export const layer = Layer.effect( const events = yield* EventV2.Service const location = yield* Location.Service const agents = yield* AgentV2.Service - const sessions = yield* SessionV2.Service + const sessions = yield* SessionStore.Service const saved = yield* PermissionSaved.Service const pending = new Map() @@ -159,8 +160,8 @@ export const layer = Layer.effect( const configured = EffectRuntime.fn("PermissionV2.configured")(function* (sessionID: SessionV2.ID) { const session = yield* sessions.get(sessionID) - if (!session.agent) return [] - return (yield* agents.get(AgentV2.ID.make(session.agent)))?.permissions ?? [] + if (!session) return yield* new SessionV2.NotFoundError({ sessionID }) + return (yield* agents.get(AgentV2.ID.make(session.agent ?? "build")))?.permissions ?? [] }) function denied(input: AssertInput, rules: Ruleset) { @@ -192,13 +193,19 @@ export const layer = Layer.effect( } } - const create = EffectRuntime.fnUntraced(function* (request: Request) { - const deferred = yield* Deferred.make() - const item = { request, deferred } - pending.set(request.id, item) - yield* events.publish(Event.Asked, request) - return item - }) + const create = (request: Request) => + EffectRuntime.uninterruptible( + EffectRuntime.gen(function* () { + const deferred = yield* Deferred.make() + const item = { request, deferred } + if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`) + pending.set(request.id, item) + yield* events.publish(Event.Asked, request).pipe( + EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id))), + ) + return item + }), + ) const ask = EffectRuntime.fn("PermissionV2.ask")(function* (input: AssertInput) { const result = yield* evaluateInput(input) @@ -207,28 +214,31 @@ export const layer = Layer.effect( return { id: value.id, effect: result.effect } }) - const assert = EffectRuntime.fn("PermissionV2.assert")(function* (input: AssertInput) { - const result = yield* evaluateInput(input) - if (result.effect === "deny") { - return yield* new DeniedError({ - rules: relevant(input, result.rules), - }) - } - if (result.effect === "allow") return - const item = yield* create(request(input)) - return yield* Deferred.await(item.deferred).pipe( - EffectRuntime.ensuring( - EffectRuntime.sync(() => { - pending.delete(item.request.id) - }), - ), - ) - }) + const assert = EffectRuntime.fn("PermissionV2.assert")((input: AssertInput) => + EffectRuntime.uninterruptibleMask((restore) => + EffectRuntime.gen(function* () { + const result = yield* evaluateInput(input) + if (result.effect === "deny") { + return yield* new DeniedError({ + rules: relevant(input, result.rules), + }) + } + if (result.effect === "allow") return + const item = yield* create(request(input)) + return yield* restore(Deferred.await(item.deferred)).pipe( + EffectRuntime.ensuring( + EffectRuntime.sync(() => { + pending.delete(item.request.id) + }), + ), + ) + }), + ), + ) - const reply = EffectRuntime.fn("PermissionV2.reply")(function* (input: ReplyInput) { + const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) => EffectRuntime.uninterruptible(EffectRuntime.gen(function* () { const existing = pending.get(input.requestID) if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) - pending.delete(input.requestID) yield* events.publish(Event.Replied, { sessionID: existing.request.sessionID, requestID: existing.request.id, @@ -240,15 +250,16 @@ export const layer = Layer.effect( existing.deferred, input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(), ) + pending.delete(input.requestID) for (const [id, item] of pending) { if (item.request.sessionID !== existing.request.sessionID) continue - pending.delete(id) yield* events.publish(Event.Replied, { sessionID: item.request.sessionID, requestID: item.request.id, reply: "reject", }) yield* Deferred.fail(item.deferred, new RejectedError()) + pending.delete(id) } return } @@ -261,6 +272,7 @@ export const layer = Layer.effect( }) } yield* Deferred.succeed(existing.deferred, undefined) + pending.delete(input.requestID) if (input.reply !== "always" || !existing.request.save?.length) return const rememberedRules = yield* savedRules() @@ -278,15 +290,15 @@ export const layer = Layer.effect( ) ) continue - pending.delete(id) yield* events.publish(Event.Replied, { sessionID: item.request.sessionID, requestID: item.request.id, reply: "always", }) yield* Deferred.succeed(item.deferred, undefined) + pending.delete(id) } - }) + }))) const list = EffectRuntime.fn("PermissionV2.list")(function* () { return Array.from(pending.values(), (item) => item.request) diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index 7297ef0f3e..9cd9d6820a 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -6,6 +6,7 @@ import { Context, Effect, Exit, Layer, Schema, Scope } from "effect" import type { ModelV2 } from "./model" import type { Catalog } from "./catalog" import { EventV2 } from "./event" +import { KeyedMutex } from "./effect/keyed-mutex" export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) export type ID = typeof ID.Type @@ -105,29 +106,36 @@ export const layer = Layer.effect( scope: Scope.Closeable }[] = [] const events = yield* EventV2.Service + const scope = yield* Scope.Scope + const locks = KeyedMutex.makeUnsafe() const svc = Service.of({ add: Effect.fn("Plugin.add")(function* (input) { - const existing = hooks.find((item) => item.id === input.id) - if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) - const scope = yield* Scope.make() - const result = yield* input.effect.pipe( - Scope.provide(scope), - Effect.withSpan("Plugin.load", { - attributes: { - "plugin.id": input.id, - }, + yield* locks.withLock(input.id)( + Effect.gen(function* () { + const existing = hooks.find((item) => item.id === input.id) + if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) + const childScope = yield* Scope.fork(scope) + const result = yield* input.effect.pipe( + Scope.provide(childScope), + Effect.withSpan("Plugin.load", { + attributes: { + "plugin.id": input.id, + }, + }), + Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)), + ) + hooks = [ + ...hooks.filter((item) => item.id !== input.id), + { + id: input.id, + hooks: result ?? {}, + scope: childScope, + }, + ] + yield* events.publish(Event.Added, { id: input.id }) }), ) - hooks = [ - ...hooks.filter((item) => item.id !== input.id), - { - id: input.id, - hooks: result ?? {}, - scope, - }, - ] - yield* events.publish(Event.Added, { id: input.id }) }), trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) { return yield* svc.triggerFor(ID.make("*"), name, input, output) @@ -167,9 +175,13 @@ export const layer = Layer.effect( return event as any }), remove: Effect.fn("Plugin.remove")(function* (id) { - const existing = hooks.find((item) => item.id === id) - hooks = hooks.filter((item) => item.id !== id) - if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) + yield* locks.withLock(id)( + Effect.gen(function* () { + const existing = hooks.find((item) => item.id === id) + hooks = hooks.filter((item) => item.id !== id) + if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) + }), + ) }), }) return svc diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts index 8e17e462bd..637ae40423 100644 --- a/packages/core/src/plugin/agent.ts +++ b/packages/core/src/plugin/agent.ts @@ -21,7 +21,6 @@ Guidelines: - Use Glob for broad file pattern matching - Use Grep for searching file contents with regex - Use Read when you know the specific file path you need to read -- Use Bash for file operations like copying, moving, or listing directory contents - Adapt your search approach based on the thoroughness level specified by the caller - Return file paths as absolute paths in your final response - For clear communication, avoid using emojis @@ -171,8 +170,6 @@ export const Plugin = PluginV2.define({ { action: "*", resource: "*", effect: "deny" }, { action: "grep", resource: "*", effect: "allow" }, { action: "glob", resource: "*", effect: "allow" }, - { action: "list", resource: "*", effect: "allow" }, - { action: "bash", resource: "*", effect: "allow" }, { action: "webfetch", resource: "*", effect: "allow" }, { action: "websearch", resource: "*", effect: "allow" }, { action: "read", resource: "*", effect: "allow" }, diff --git a/packages/core/src/process.ts b/packages/core/src/process.ts index f076ea4e42..4555b28017 100644 --- a/packages/core/src/process.ts +++ b/packages/core/src/process.ts @@ -79,7 +79,7 @@ const describeCommand = (command: ChildProcess.Command): string => { const wrapError = (description: string, cause: unknown): AppProcessError => cause instanceof AppProcessError ? cause : new AppProcessError({ command: description, cause }) -const abortError = (signal: AbortSignal): Error => { +export const abortError = (signal: AbortSignal): Error => { const reason = signal.reason if (reason instanceof Error) return reason const err = new Error("Aborted") @@ -87,7 +87,7 @@ const abortError = (signal: AbortSignal): Error => { return err } -const waitForAbort = (signal: AbortSignal) => +export const waitForAbort = (signal: AbortSignal) => Effect.callback((resume) => { if (signal.aborted) { resume(Effect.fail(abortError(signal))) @@ -107,7 +107,7 @@ const normalizeStdin = ( ? Stream.make(input) : input -const collectStream = (stream: Stream.Stream, maxOutputBytes: number | undefined) => +export const collectStream = (stream: Stream.Stream, maxOutputBytes: number | undefined) => Stream.runFold( stream, () => ({ chunks: [] as Uint8Array[], bytes: 0, truncated: false }), diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index 3f99cedc1c..7b122ad416 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -41,6 +41,20 @@ export const Native = Schema.Struct({ export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type")) export type Api = typeof Api.Type +export const PublicAISDK = Schema.Struct({ + type: Schema.Literal("aisdk"), + package: Schema.String, + url: Schema.String.pipe(Schema.optional), +}) + +export const PublicNative = Schema.Struct({ + type: Schema.Literal("native"), + url: Schema.String.pipe(Schema.optional), +}) + +export const PublicApi = Schema.Union([PublicAISDK, PublicNative]).pipe(Schema.toTaggedUnion("type")) +export type PublicApi = typeof PublicApi.Type + export const Request = Schema.Struct({ headers: Schema.Record(Schema.String, Schema.String), body: Schema.Record(Schema.String, Schema.Any), @@ -86,3 +100,49 @@ export class Info extends Schema.Class("ProviderV2.Info")({ }) } } + +export class PublicInfo extends Schema.Class("ProviderV2.PublicInfo")({ + id: ID, + name: Schema.String, + enabled: Schema.Union([ + Schema.Literal(false), + Schema.Struct({ + via: Schema.Literal("env"), + name: Schema.String, + }), + Schema.Struct({ + via: Schema.Literal("account"), + service: Schema.String, + }), + Schema.Struct({ + via: Schema.Literal("custom"), + }), + ]), + env: Schema.String.pipe(Schema.Array), + api: PublicApi, +}) {} + +export function sanitizePublicUrl(value: string | undefined): string | undefined { + if (!value) return undefined + try { + const url = new URL(value) + return url.protocol === "http:" || url.protocol === "https:" ? url.origin : undefined + } catch { + return undefined + } +} + +export function toPublic(info: Info): PublicInfo { + const enabled = info.enabled === false || info.enabled.via !== "custom" ? info.enabled : { via: "custom" as const } + const api = + info.api.type === "aisdk" + ? { type: info.api.type, package: info.api.package, url: sanitizePublicUrl(info.api.url) } + : { type: info.api.type, url: sanitizePublicUrl(info.api.url) } + return new PublicInfo({ + id: info.id, + name: info.name, + enabled, + env: [...info.env], + api, + }) +} diff --git a/packages/core/src/question.ts b/packages/core/src/question.ts new file mode 100644 index 0000000000..a489fb9aac --- /dev/null +++ b/packages/core/src/question.ts @@ -0,0 +1,198 @@ +export * as QuestionV2 from "./question" + +import { Context, Deferred, Effect, Layer, Schema } from "effect" +import { EventV2 } from "./event" +import { Identifier } from "./id/id" +import { withStatics } from "./schema" +import { SessionSchema } from "./session/schema" + +export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( + Schema.brand("QuestionV2.ID"), + withStatics((schema) => ({ ascending: (id?: string) => schema.make(Identifier.ascending("question", id)) })), +) +export type ID = typeof ID.Type + +export const Option = Schema.Struct({ + label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }), + description: Schema.String.annotate({ description: "Explanation of choice" }), +}).annotate({ identifier: "QuestionV2.Option" }) +export type Option = typeof Option.Type + +const base = { + question: Schema.String.annotate({ description: "Complete question" }), + header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }), + options: Schema.Array(Option).annotate({ description: "Available choices" }), + multiple: Schema.Boolean.pipe(Schema.optional).annotate({ description: "Allow selecting multiple choices" }), +} + +export const Info = Schema.Struct({ + ...base, + custom: Schema.Boolean.pipe(Schema.optional).annotate({ + description: "Allow typing a custom answer (default: true)", + }), +}).annotate({ identifier: "QuestionV2.Info" }) +export type Info = typeof Info.Type + +export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" }) +export type Prompt = typeof Prompt.Type + +export const Tool = Schema.Struct({ + messageID: Schema.String, + callID: Schema.String, +}).annotate({ identifier: "QuestionV2.Tool" }) +export type Tool = typeof Tool.Type + +export const Request = Schema.Struct({ + id: ID, + sessionID: SessionSchema.ID, + questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), + tool: Tool.pipe(Schema.optional), +}).annotate({ identifier: "QuestionV2.Request" }) +export type Request = typeof Request.Type + +export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" }) +export type Answer = typeof Answer.Type + +export const Reply = Schema.Struct({ + answers: Schema.Array(Answer).annotate({ + description: "User answers in order of questions (each answer is an array of selected labels)", + }), +}).annotate({ identifier: "QuestionV2.Reply" }) +export type Reply = typeof Reply.Type + +export const Event = { + Asked: EventV2.define({ type: "question.v2.asked", schema: Request.fields }), + Replied: EventV2.define({ + type: "question.v2.replied", + schema: { + sessionID: SessionSchema.ID, + requestID: ID, + answers: Schema.Array(Answer), + }, + }), + Rejected: EventV2.define({ + type: "question.v2.rejected", + schema: { + sessionID: SessionSchema.ID, + requestID: ID, + }, + }), +} + +export class RejectedError extends Schema.TaggedErrorClass()("QuestionV2.RejectedError", {}) { + override get message() { + return "The user dismissed this question" + } +} + +export class NotFoundError extends Schema.TaggedErrorClass()("QuestionV2.NotFoundError", { + requestID: ID, +}) {} + +export interface AskInput { + readonly sessionID: SessionSchema.ID + readonly questions: ReadonlyArray + readonly tool?: Tool +} + +export interface ReplyInput { + readonly requestID: ID + readonly answers: ReadonlyArray +} + +export interface Interface { + readonly ask: (input: AskInput) => Effect.Effect, RejectedError> + readonly reply: (input: ReplyInput) => Effect.Effect + readonly reject: (requestID: ID) => Effect.Effect + readonly list: () => Effect.Effect> +} + +export class Service extends Context.Service()("@opencode/v2/Question") {} + +interface Pending { + readonly request: Request + readonly deferred: Deferred.Deferred, RejectedError> +} + +/** + * Location-owned pending prompts. The Location layer map must materialize this + * layer once per embedded Location so replies cannot settle another Location's + * deferred request. + */ +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const events = yield* EventV2.Service + const pending = new Map() + + yield* Effect.addFinalizer(() => + Effect.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), { + discard: true, + }).pipe( + Effect.ensuring( + Effect.sync(() => { + pending.clear() + }), + ), + ), + ) + + const ask = Effect.fn("QuestionV2.ask")((input: AskInput) => + Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const id = ID.ascending() + const deferred = yield* Deferred.make, RejectedError>() + const request: Request = { id, ...input } + pending.set(id, { request, deferred }) + return yield* events.publish(Event.Asked, request).pipe( + Effect.andThen(restore(Deferred.await(deferred))), + Effect.ensuring( + Effect.sync(() => { + pending.delete(id) + }), + ), + ) + }), + ), + ) + + const reply = Effect.fn("QuestionV2.reply")((input: ReplyInput) => + Effect.uninterruptible( + Effect.gen(function* () { + const existing = pending.get(input.requestID) + if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) + yield* events.publish(Event.Replied, { + sessionID: existing.request.sessionID, + requestID: existing.request.id, + answers: input.answers.map((answer) => [...answer]), + }) + yield* Deferred.succeed(existing.deferred, input.answers) + pending.delete(input.requestID) + }), + ), + ) + + const reject = Effect.fn("QuestionV2.reject")((requestID: ID) => + Effect.uninterruptible( + Effect.gen(function* () { + const existing = pending.get(requestID) + if (!existing) return yield* new NotFoundError({ requestID }) + yield* events.publish(Event.Rejected, { + sessionID: existing.request.sessionID, + requestID: existing.request.id, + }) + yield* Deferred.fail(existing.deferred, new RejectedError()) + pending.delete(requestID) + }), + ), + ) + + const list = Effect.fn("QuestionV2.list")(function* () { + return Array.from(pending.values(), (item) => item.request) + }) + + return Service.of({ ask, reply, reject, list }) + }), +) + +export const locationLayer = layer diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts new file mode 100644 index 0000000000..8247dd77ba --- /dev/null +++ b/packages/core/src/ripgrep.ts @@ -0,0 +1,182 @@ +export * as Ripgrep from "./ripgrep" + +import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect" +import { ChildProcess } from "effect/unstable/process" +import { Ripgrep as FileSystemRipgrep } from "./filesystem/ripgrep" +import { AppProcess, collectStream, waitForAbort } from "./process" +import { NonNegativeInt, PositiveInt } from "./schema" + +/** + * Small core-owned ripgrep execution adapter. It deliberately exposes raw + * process-oriented rows, not model text or permission behavior. LocationSearch + * supplies read authority and bounded substrate results; future leaf tools own + * presentation and permission prompts. + */ + +const ERROR_BYTES = 8 * 1024 +export const MAX_RECORD_BYTES = 64 * 1024 +export const MAX_SUBMATCHES = 100 + +const RawMatch = Schema.Struct({ + type: Schema.Literal("match"), + data: Schema.Struct({ + path: Schema.Struct({ text: Schema.String }), + lines: Schema.Struct({ text: Schema.String }), + line_number: PositiveInt, + absolute_offset: NonNegativeInt, + submatches: Schema.Array( + Schema.Struct({ + match: Schema.Struct({ text: Schema.String }), + start: NonNegativeInt, + end: NonNegativeInt, + }), + ), + }), +}) + +export type Match = typeof RawMatch.Type["data"] + +export class Error extends Schema.TaggedErrorClass()("Ripgrep.Error", { + message: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +export class InvalidPatternError extends Schema.TaggedErrorClass()("Ripgrep.InvalidPatternError", { + pattern: Schema.String, + message: Schema.String, +}) {} + +export interface Result { + readonly items: A[] + readonly truncated: boolean + readonly partial: boolean +} + +export interface FilesInput { + readonly cwd: string + readonly pattern: string + readonly limit: number + readonly signal?: AbortSignal +} + +export interface GrepInput { + readonly cwd: string + readonly pattern: string + readonly file?: string + readonly include?: string + readonly limit: number + readonly signal?: AbortSignal +} + +export interface Interface { + readonly files: (input: FilesInput) => Effect.Effect, Error> + readonly grep: (input: GrepInput) => Effect.Effect, Error | InvalidPatternError> +} + +export class Service extends Context.Service()("@opencode/v2/Ripgrep") {} + +const failure = (message: string, cause?: unknown) => new Error({ message, cause }) + +const isInvalidPattern = (stderr: string) => stderr.includes("regex parse error") || stderr.includes("error parsing regex") + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const process = yield* AppProcess.Service + const binary = yield* FileSystemRipgrep.Service + + const run = (input: { + readonly cwd: string + readonly args: string[] + readonly limit: number + readonly signal?: AbortSignal + readonly parse: (line: string) => Effect.Effect + readonly pattern?: string + }) => { + const program = Effect.scoped( + Effect.gen(function* () { + const handle = yield* process.spawn( + ChildProcess.make(yield* binary.filepath, input.args, { cwd: input.cwd, extendEnv: true, stdin: "ignore" }), + ) + const stderrFiber = yield* collectStream(handle.stderr, ERROR_BYTES).pipe( + Effect.map((output) => output.buffer.toString("utf8")), + Effect.forkScoped, + ) + const rows = yield* Stream.decodeText(handle.stdout).pipe( + Stream.splitLines, + Stream.filter((line) => line.length > 0), + Stream.mapEffect(input.parse), + Stream.filter((row): row is A => row !== undefined), + Stream.take(input.limit + 1), + Stream.runCollect, + Effect.map((chunk) => [...chunk]), + ) + const truncated = rows.length > input.limit + if (truncated) return { items: rows.slice(0, input.limit), truncated, partial: false } + + const code = yield* handle.exitCode + const stderr = yield* Fiber.join(stderrFiber) + if (input.pattern && code === 2 && isInvalidPattern(stderr)) { + return yield* new InvalidPatternError({ pattern: input.pattern, message: stderr.trim() }) + } + if (code !== 0 && code !== 1 && code !== 2) { + return yield* failure(stderr.trim() || `ripgrep failed with code ${code}`) + } + return { items: code === 1 ? [] : rows, truncated: false, partial: code === 2 } + }), + ) + const abortable = input.signal ? program.pipe(Effect.raceFirst(waitForAbort(input.signal))) : program + return abortable.pipe(Effect.mapError((cause) => cause instanceof Error || cause instanceof InvalidPatternError ? cause : failure("ripgrep execution failed", cause))) + } + + return Service.of({ + files: (input) => + run({ + ...input, + args: [ + "--no-config", + "--files", + "--glob=!.git/*", // TODO: Review .git exclusion policy before leaf tool exposure. + `--glob=${input.pattern}`, + "--glob=!.*", + "--glob=!**/.*", + ".", + ], + parse: (line) => Effect.succeed(line.replace(/^\.\//, "")), + }).pipe( + Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))), + ), + grep: (input) => + run({ + ...input, + args: [ + "--no-config", + "--json", + "--glob=!.git/*", // TODO: Review .git exclusion policy before leaf tool exposure. + "--no-messages", + ...(input.include ? [`--glob=${input.include}`] : []), + "--glob=!.*", + "--glob=!**/.*", + "--", + input.pattern, + input.file ?? ".", + ], + parse: (line) => + (Buffer.byteLength(line, "utf8") > MAX_RECORD_BYTES + ? Effect.fail(failure(`Ripgrep JSON record exceeded ${MAX_RECORD_BYTES} bytes`)) + : Effect.try({ + try: () => JSON.parse(line) as unknown, + catch: (cause) => failure("Invalid ripgrep JSON output", cause), + })).pipe( + Effect.flatMap((json) => { + if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match") return Effect.succeed(undefined) + return Schema.decodeUnknownEffect(RawMatch)(json).pipe( + Effect.map((match) => ({ ...match.data, submatches: match.data.submatches.slice(0, MAX_SUBMATCHES) })), + Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)), + ) + }), + ), + }), + }) + }), +).pipe(Layer.provide(FileSystemRipgrep.defaultLayer)) diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index b5cee90a57..19928c9069 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -1,4 +1,12 @@ import { Option, Schema, SchemaGetter } from "effect" +import { Hash } from "./util/hash" + +export type ExternalID = { + readonly namespace: string + readonly key: string +} + +export const externalID = (prefix: string, input: ExternalID) => `${prefix}_${Hash.sha256(JSON.stringify([input.namespace, input.key]))}` /** * Integer greater than zero. diff --git a/packages/core/src/session-system-context.ts b/packages/core/src/session-system-context.ts new file mode 100644 index 0000000000..0ae80c66e3 --- /dev/null +++ b/packages/core/src/session-system-context.ts @@ -0,0 +1,54 @@ +export * as SessionSystemContext from "./session-system-context" + +import { Context, DateTime, Effect, Layer } from "effect" +import { Location } from "./location" +import { SystemContext } from "./system-context" + +export interface Interface { + readonly load: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/SessionSystemContext") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const location = yield* Location.Service + const environment = [ + "", + ` Working directory: ${location.directory}`, + ` Workspace root folder: ${location.project.directory}`, + ` Is directory a git repo: ${location.vcs?.type === "git" ? "yes" : "no"}`, + ` Platform: ${process.platform}`, + "", + ].join("\n") + const context = SystemContext.struct({ + environment: SystemContext.value({ + key: SystemContext.Key.make("core/environment"), + load: Effect.succeed({ + baseline: ["Here is some useful information about the environment you are running in:", environment].join( + "\n", + ), + update: ["The environment you are running in is now:", environment].join("\n"), + }), + }), + date: SystemContext.value({ + key: SystemContext.Key.make("core/date"), + load: DateTime.nowAsDate.pipe( + Effect.map((date) => ({ + baseline: `Today's date: ${date.toDateString()}`, + update: `Today's date is now: ${date.toDateString()}`, + })), + ), + }), + }) + + return Service.of({ + load: Effect.fn("SessionSystemContext.load")(function* () { + return yield* SystemContext.load(context) + }), + }) + }), +) + +export const locationLayer = layer diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index f0af455466..bdf463ac6a 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,14 +1,14 @@ export * as SessionV2 from "./session" export * from "./session/schema" -import { DateTime, Effect, Layer, Schema, Context } from "effect" -import { and, asc, desc, eq, gt, gte, like, lt, or, type SQL } from "drizzle-orm" +import { Cause, DateTime, Effect, Layer, Schema, Context, Stream } from "effect" +import { and, asc, desc, eq, gt, like, lt, or, type SQL } from "drizzle-orm" import { ProjectV2 } from "./project" import { WorkspaceV2 } from "./workspace" import { ModelV2 } from "./model" import { Location } from "./location" import { SessionMessage } from "./session/message" -import type { Prompt } from "./session/prompt" +import { Prompt } from "./session/prompt" import { EventV2 } from "./event" import { ProviderV2 } from "./provider" import { Database } from "./database/database" @@ -17,6 +17,18 @@ import { SessionMessageTable, SessionTable } from "./session/sql" import { SessionSchema } from "./session/schema" import { AbsolutePath, PositiveInt, RelativePath } from "./schema" import { AgentV2 } from "./agent" +import { SessionV1 } from "./v1/session" +import { InstallationVersion } from "./installation/version" +import { Slug } from "./util/slug" +import { ProjectTable } from "./project/sql" +import path from "path" +import { fromRow } from "./session/info" +import { SessionRunner } from "./session/runner/index" +import { SessionStore } from "./session/store" +import { SessionExecution } from "./session/execution" +import { MessageDecodeError } from "./session/error" +import { SessionEvent } from "./session/event" +import { SessionInput } from "./session/input" // get project -> project.locations // @@ -60,7 +72,7 @@ export type ListInput = typeof ListInput.Type type CreateInput = { id?: SessionSchema.ID - agent?: string + agent?: AgentV2.ID model?: ModelV2.Ref location: Location.Ref } @@ -82,21 +94,23 @@ export class NotFoundError extends Schema.TaggedErrorClass()("Ses export class OperationUnavailableError extends Schema.TaggedErrorClass()( "Session.OperationUnavailableError", { - operation: Schema.Literals(["prompt", "compact", "wait"]), + operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "switchModel", "compact", "wait"]), }, ) {} -export class MessageDecodeError extends Schema.TaggedErrorClass()("Session.MessageDecodeError", { +export { MessageDecodeError } from "./session/error" + +export class PromptConflictError extends Schema.TaggedErrorClass()("Session.PromptConflictError", { sessionID: SessionSchema.ID, messageID: SessionMessage.ID, }) {} -export type Error = NotFoundError | MessageDecodeError | OperationUnavailableError +export type Error = NotFoundError | MessageDecodeError | OperationUnavailableError | PromptConflictError export interface Interface { readonly list: (input?: ListInput) => Effect.Effect - readonly create: (input?: CreateInput) => Effect.Effect - readonly move: (input: MoveInput) => Effect.Effect + readonly create: (input: CreateInput) => Effect.Effect + readonly move: (input: MoveInput) => Effect.Effect readonly get: (sessionID: SessionSchema.ID) => Effect.Effect readonly messages: (input: { sessionID: SessionSchema.ID @@ -104,85 +118,74 @@ export interface Interface { order?: "asc" | "desc" cursor?: { id: SessionMessage.ID - time: number direction: "previous" | "next" } }) => Effect.Effect + readonly message: (input: { + sessionID: SessionSchema.ID + messageID: SessionMessage.ID + }) => Effect.Effect readonly context: ( sessionID: SessionSchema.ID, ) => Effect.Effect - readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect - readonly switchModel: (input: { sessionID: SessionSchema.ID; model: ModelV2.Ref }) => Effect.Effect + readonly events: (input: { + sessionID: SessionSchema.ID + after?: EventV2.Cursor + }) => Stream.Stream, NotFoundError> + readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect + readonly switchModel: (input: { sessionID: SessionSchema.ID; model: ModelV2.Ref }) => Effect.Effect readonly prompt: (input: { - id?: EventV2.ID + id?: SessionMessage.ID sessionID: SessionSchema.ID prompt: Prompt - delivery?: SessionSchema.Delivery + delivery?: SessionInput.Delivery resume?: boolean - }) => Effect.Effect + }) => Effect.Effect readonly shell: (input: { id?: EventV2.ID sessionID: SessionSchema.ID command: string - delivery?: SessionSchema.Delivery resume?: boolean - }) => Effect.Effect + }) => Effect.Effect readonly skill: (input: { id?: EventV2.ID sessionID: SessionSchema.ID skill: string - delivery?: SessionSchema.Delivery resume?: boolean - }) => Effect.Effect + }) => Effect.Effect readonly compact: (input: CompactInput) => Effect.Effect readonly wait: (id: SessionSchema.ID) => Effect.Effect - readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect + readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/Session") {} -function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info { - return SessionSchema.Info.make({ - id: SessionSchema.ID.make(row.id), - projectID: ProjectV2.ID.make(row.project_id), - title: row.title, - parentID: row.parent_id ? SessionSchema.ID.make(row.parent_id) : undefined, - agent: row.agent ? AgentV2.ID.make(row.agent) : undefined, - model: row.model - ? { - id: ModelV2.ID.make(row.model.id), - providerID: ProviderV2.ID.make(row.model.providerID), - variant: ModelV2.VariantID.make(row.model.variant ?? "default"), - } - : undefined, - cost: row.cost, - tokens: { - input: row.tokens_input, - output: row.tokens_output, - reasoning: row.tokens_reasoning, - cache: { - read: row.tokens_cache_read, - write: row.tokens_cache_write, - }, - }, - location: Location.Ref.make({ - directory: AbsolutePath.make(row.directory), - workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined, - }), - subpath: row.path ? RelativePath.make(row.path) : undefined, - time: { - created: DateTime.makeUnsafe(row.time_created), - updated: DateTime.makeUnsafe(row.time_updated), - archived: row.time_archived ? DateTime.makeUnsafe(row.time_archived) : undefined, - }, - }) -} - export const layer = Layer.effect( Service, Effect.gen(function* () { const db = (yield* Database.Service).db + const events = yield* EventV2.Service + const projects = yield* ProjectV2.Service + const execution = yield* SessionExecution.Service + const store = yield* SessionStore.Service const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) + const isDurableSessionEvent = Schema.is(SessionEvent.Durable) + const scope = yield* Effect.scope + + const enqueueWake = (sessionID: SessionSchema.ID) => + execution.wake(sessionID).pipe( + Effect.tapCause((cause) => + Cause.hasInterruptsOnly(cause) + ? Effect.void + : Effect.logError("Failed to wake Session").pipe( + Effect.annotateLogs("sessionID", sessionID), + Effect.annotateLogs("cause", cause), + ), + ), + Effect.ignore, + Effect.forkIn(scope, { startImmediately: true }), + Effect.asVoid, + ) const decode = (row: typeof SessionMessageTable.$inferSelect) => decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe( @@ -195,14 +198,80 @@ export const layer = Layer.effect( ), ) + const findExistingPrompt = Effect.fnUntraced(function* (input: { + sessionID: SessionSchema.ID + messageID: SessionMessage.ID + prompt: Prompt + delivery: SessionInput.Delivery + }) { + const stored = yield* SessionInput.find(db, input.messageID) + if (!stored) return yield* SessionInput.reconcileProjected(db, { id: input.messageID, ...input }) + if (!SessionInput.equivalent(stored, input)) { + return yield* new PromptConflictError({ sessionID: input.sessionID, messageID: input.messageID }) + } + return stored + }) + const result = Service.of({ - create: Effect.fn("V2Session.create")(function* () { - return {} as SessionSchema.Info + create: Effect.fn("V2Session.create")(function* (input) { + const sessionID = input.id ?? SessionSchema.ID.create() + const recorded = yield* store.get(sessionID) + if (recorded) return recorded + const project = yield* projects.resolve(input.location.directory) + yield* db + .insert(ProjectTable) + .values({ id: project.id, worktree: project.directory, vcs: project.vcs?.type, sandboxes: [] }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + const now = Date.now() + const info = SessionV1.SessionInfo.make({ + id: sessionID, + slug: Slug.create(), + version: InstallationVersion, + projectID: project.id, + directory: input.location.directory, + path: path.relative(project.directory, input.location.directory).replaceAll("\\", "/"), + workspaceID: input.location.workspaceID ? WorkspaceV2.ID.make(input.location.workspaceID) : undefined, + title: `New session - ${new Date(now).toISOString()}`, + agent: input.agent, + model: input.model + ? { + id: ProviderV2.ModelID.make(input.model.id), + providerID: input.model.providerID, + variant: input.model.variant, + } + : undefined, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: now, updated: now }, + }) + const projected = yield* events + .publish(SessionV1.Event.Created, { sessionID, info }, { location: input.location }) + .pipe( + Effect.as({ type: "created" } as const), + Effect.catchDefect((defect) => { + if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) { + return Effect.die(defect) + } + // Concurrent creation lost the projection race. The existing Session identity wins. + return store + .get(sessionID) + .pipe( + Effect.flatMap((session) => + session ? Effect.succeed({ type: "existing", session } as const) : Effect.die(defect), + ), + ) + }), + ) + if (projected.type === "existing") return projected.session + // TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice. + return yield* result.get(sessionID).pipe(Effect.orDie) }), get: Effect.fn("V2Session.get")(function* (sessionID) { - const row = yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie) - if (!row) return yield* new NotFoundError({ sessionID }) - return fromRow(row) + const session = yield* store.get(sessionID) + if (!session) return yield* new NotFoundError({ sessionID }) + return session }), list: Effect.fn("V2Session.list")(function* (input = {}) { const direction = input.anchor?.direction ?? "next" @@ -245,22 +314,19 @@ export const layer = Layer.effect( const direction = input.cursor?.direction ?? "next" const requestedOrder = input.order ?? "desc" const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder - const boundary = input.cursor + const anchor = input.cursor + ? yield* db + .select({ seq: SessionMessageTable.seq }) + .from(SessionMessageTable) + .where(and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id))) + .get() + .pipe(Effect.orDie) + : undefined + if (input.cursor && !anchor) return [] + const boundary = anchor ? order === "asc" - ? or( - gt(SessionMessageTable.time_created, input.cursor.time), - and( - eq(SessionMessageTable.time_created, input.cursor.time), - gt(SessionMessageTable.id, input.cursor.id), - ), - ) - : or( - lt(SessionMessageTable.time_created, input.cursor.time), - and( - eq(SessionMessageTable.time_created, input.cursor.time), - lt(SessionMessageTable.id, input.cursor.id), - ), - ) + ? gt(SessionMessageTable.seq, anchor.seq) + : lt(SessionMessageTable.seq, anchor.seq) : undefined const where = boundary ? and(eq(SessionMessageTable.session_id, input.sessionID), boundary) @@ -269,55 +335,68 @@ export const layer = Layer.effect( .select() .from(SessionMessageTable) .where(where) - .orderBy( - order === "asc" ? asc(SessionMessageTable.time_created) : desc(SessionMessageTable.time_created), - order === "asc" ? asc(SessionMessageTable.id) : desc(SessionMessageTable.id), - ) + .orderBy(order === "asc" ? asc(SessionMessageTable.seq) : desc(SessionMessageTable.seq)) const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe( Effect.orDie, ) return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode) }), + message: Effect.fn("V2Session.message")(function* (input) { + const stored = yield* store.message(input.messageID) + return stored?.sessionID === input.sessionID ? stored.message : undefined + }), context: Effect.fn("V2Session.context")(function* (sessionID) { yield* result.get(sessionID) - const compaction = yield* db - .select() - .from(SessionMessageTable) - .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction"))) - .orderBy(desc(SessionMessageTable.time_created), desc(SessionMessageTable.id)) - .limit(1) - .get() - .pipe(Effect.orDie) - const rows = yield* db - .select() - .from(SessionMessageTable) - .where( - and( - eq(SessionMessageTable.session_id, sessionID), - compaction - ? or( - gt(SessionMessageTable.time_created, compaction.time_created), - and( - eq(SessionMessageTable.time_created, compaction.time_created), - gte(SessionMessageTable.id, compaction.id), - ), - ) - : undefined, - ), - ) - .orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)) - .all() - .pipe(Effect.orDie) - return yield* Effect.forEach(rows, decode) + return yield* store.context(sessionID) }), - prompt: Effect.fn("V2Session.prompt")(function* (input) { - yield* result.get(input.sessionID) - return yield* Effect.fail(new OperationUnavailableError({ operation: "prompt" })) + events: (input) => + Stream.unwrap( + result + .get(input.sessionID) + .pipe(Effect.as(events.aggregateEvents({ aggregateID: input.sessionID, after: input.after }))), + ).pipe( + Stream.filter((event): event is EventV2.CursorEvent => + isDurableSessionEvent(event.event), + ), + ), + prompt: Effect.fn("V2Session.prompt")((input) => + Effect.uninterruptible( + Effect.gen(function* () { + yield* result.get(input.sessionID) + const returnPrompt = Effect.fnUntraced(function* (admitted: SessionInput.Admitted) { + if (input.resume !== false) yield* enqueueWake(input.sessionID) + return SessionInput.toMessage(admitted) + }, Effect.uninterruptible) + const messageID = input.id ?? SessionMessage.ID.create() + const delivery = input.delivery ?? "steer" + const expected = { sessionID: input.sessionID, messageID, prompt: input.prompt, delivery } + const existing = yield* findExistingPrompt(expected) + if (existing) return yield* returnPrompt(existing) + const admitted = yield* SessionInput.admit(db, { + id: messageID, + sessionID: input.sessionID, + prompt: input.prompt, + delivery, + }) + if (!admitted) return yield* new PromptConflictError({ sessionID: input.sessionID, messageID }) + if (!SessionInput.equivalent(admitted, expected)) + return yield* new PromptConflictError({ sessionID: input.sessionID, messageID }) + return yield* returnPrompt(admitted) + }), + ), + ), + shell: Effect.fn("V2Session.shell")(function* () { + return yield* new OperationUnavailableError({ operation: "shell" }) + }), + skill: Effect.fn("V2Session.skill")(function* () { + return yield* new OperationUnavailableError({ operation: "skill" }) + }), + switchAgent: Effect.fn("V2Session.switchAgent")(function* () { + return yield* new OperationUnavailableError({ operation: "switchAgent" }) + }), + switchModel: Effect.fn("V2Session.switchModel")(function* () { + return yield* new OperationUnavailableError({ operation: "switchModel" }) }), - shell: Effect.fn("V2Session.shell")(function* () {}), - skill: Effect.fn("V2Session.skill")(function* () {}), - switchAgent: Effect.fn("V2Session.switchAgent")(function* () {}), - switchModel: Effect.fn("V2Session.switchModel")(function* () {}), compact: Effect.fn("V2Session.compact")(function* (input) { yield* result.get(input.sessionID) return yield* new OperationUnavailableError({ operation: "compact" }) @@ -326,16 +405,33 @@ export const layer = Layer.effect( yield* result.get(sessionID) return yield* new OperationUnavailableError({ operation: "wait" }) }), - resume: Effect.fn("V2Session.resume")(function* () {}), - move: Effect.fn("V2Session.move")(function* () {}), + resume: Effect.fn("V2Session.resume")(function* (sessionID) { + yield* result.get(sessionID) + yield* execution.resume(sessionID) + }), + move: Effect.fn("V2Session.move")(function* () { + return yield* new OperationUnavailableError({ operation: "move" }) + }), }) return result }), ) +const DefaultDatabase = Database.defaultLayer +const DefaultEvents = EventV2.layer.pipe(Layer.provide(DefaultDatabase)) +const DefaultProjector = SessionProjector.layer.pipe(Layer.provide(DefaultEvents), Layer.provide(DefaultDatabase)) +const DefaultStore = SessionStore.layer.pipe(Layer.provide(DefaultDatabase)) export const defaultLayer = layer.pipe( - Layer.provide(SessionProjector.defaultLayer), - Layer.provide(Database.defaultLayer), + Layer.provide( + Layer.mergeAll( + DefaultDatabase, + DefaultEvents, + DefaultProjector, + DefaultStore, + SessionExecution.noopLayer, + ProjectV2.defaultLayer, + ), + ), Layer.orDie, ) diff --git a/packages/core/src/session/context.ts b/packages/core/src/session/context.ts new file mode 100644 index 0000000000..458f768032 --- /dev/null +++ b/packages/core/src/session/context.ts @@ -0,0 +1,47 @@ +import { and, asc, desc, eq, gt, gte, or } from "drizzle-orm" +import { Effect, Schema } from "effect" +import { Database } from "../database/database" +import { MessageDecodeError } from "./error" +import { SessionMessage } from "./message" +import { SessionSchema } from "./schema" +import { SessionMessageTable } from "./sql" + +type DatabaseService = Database.Interface["db"] + +const decode = Schema.decodeUnknownEffect(SessionMessage.Message) + +export const load = Effect.fn("SessionContext.load")(function* (db: DatabaseService, sessionID: SessionSchema.ID) { + const compaction = yield* db + .select() + .from(SessionMessageTable) + .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction"))) + .orderBy(desc(SessionMessageTable.seq)) + .limit(1) + .get() + .pipe(Effect.orDie) + const rows = yield* db + .select() + .from(SessionMessageTable) + .where( + and( + eq(SessionMessageTable.session_id, sessionID), + compaction ? or(gte(SessionMessageTable.seq, compaction.seq)) : undefined, + ), + ) + .orderBy(asc(SessionMessageTable.seq)) + .all() + .pipe(Effect.orDie) + return yield* Effect.forEach(rows, (row) => + decode({ ...row.data, id: row.id, type: row.type }).pipe( + Effect.mapError( + () => + new MessageDecodeError({ + sessionID: SessionSchema.ID.make(row.session_id), + messageID: SessionMessage.ID.make(row.id), + }), + ), + ), + ) +}) + +export * as SessionContext from "./context" diff --git a/packages/core/src/session/error.ts b/packages/core/src/session/error.ts new file mode 100644 index 0000000000..610016e8e7 --- /dev/null +++ b/packages/core/src/session/error.ts @@ -0,0 +1,8 @@ +import { Schema } from "effect" +import { SessionMessage } from "./message" +import { SessionSchema } from "./schema" + +export class MessageDecodeError extends Schema.TaggedErrorClass()("Session.MessageDecodeError", { + sessionID: SessionSchema.ID, + messageID: SessionMessage.ID, +}) {} diff --git a/packages/core/src/session/event.ts b/packages/core/src/session/event.ts index c8b4aac503..0e82c0bac0 100644 --- a/packages/core/src/session/event.ts +++ b/packages/core/src/session/event.ts @@ -1,4 +1,5 @@ import { Schema } from "effect" +import { ProviderMetadata } from "@opencode-ai/llm" import { EventV2 } from "../event" import { ModelV2 } from "../model" import { NonNegativeInt } from "../schema" @@ -29,6 +30,12 @@ const options = { version: 1, }, } as const +const stepSettlementOptions = { + sync: { + aggregate: "sessionID", + version: 2, + }, +} as const export const UnknownError = Schema.Struct({ type: Schema.Literal("unknown"), @@ -64,6 +71,7 @@ export const Prompted = EventV2.define({ schema: { ...Base, prompt: Prompt, + delivery: Schema.Literals(["steer", "queue"]), }, }) export type Prompted = typeof Prompted.Type @@ -117,9 +125,10 @@ export namespace Step { export const Ended = EventV2.define({ type: "session.next.step.ended", - ...options, + ...stepSettlementOptions, schema: { ...Base, + assistantMessageID: EventV2.ID, finish: Schema.String, cost: Schema.Finite, tokens: Schema.Struct({ @@ -138,9 +147,10 @@ export namespace Step { export const Failed = EventV2.define({ type: "session.next.step.failed", - ...options, + ...stepSettlementOptions, schema: { ...Base, + assistantMessageID: EventV2.ID, error: UnknownError, }, }) @@ -153,15 +163,17 @@ export namespace Text { ...options, schema: { ...Base, + textID: Schema.String, }, }) export type Started = typeof Started.Type + // Stream fragments are live-only; Text.Ended is the replayable full-value boundary. export const Delta = EventV2.define({ type: "session.next.text.delta", - ...options, schema: { ...Base, + textID: Schema.String, delta: Schema.String, }, }) @@ -172,6 +184,7 @@ export namespace Text { ...options, schema: { ...Base, + textID: Schema.String, text: Schema.String, }, }) @@ -185,13 +198,14 @@ export namespace Reasoning { schema: { ...Base, reasoningID: Schema.String, + providerMetadata: ProviderMetadata.pipe(Schema.optional), }, }) export type Started = typeof Started.Type + // Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary. export const Delta = EventV2.define({ type: "session.next.reasoning.delta", - ...options, schema: { ...Base, reasoningID: Schema.String, @@ -207,30 +221,35 @@ export namespace Reasoning { ...Base, reasoningID: Schema.String, text: Schema.String, + providerMetadata: ProviderMetadata.pipe(Schema.optional), }, }) export type Ended = typeof Ended.Type } export namespace Tool { + const ToolBase = { + ...Base, + assistantMessageID: EventV2.ID, + callID: Schema.String, + } + export namespace Input { export const Started = EventV2.define({ type: "session.next.tool.input.started", ...options, schema: { - ...Base, - callID: Schema.String, + ...ToolBase, name: Schema.String, }, }) export type Started = typeof Started.Type + // Stream fragments are live-only; Input.Ended is the replayable raw-input boundary. export const Delta = EventV2.define({ type: "session.next.tool.input.delta", - ...options, schema: { - ...Base, - callID: Schema.String, + ...ToolBase, delta: Schema.String, }, }) @@ -240,8 +259,7 @@ export namespace Tool { type: "session.next.tool.input.ended", ...options, schema: { - ...Base, - callID: Schema.String, + ...ToolBase, text: Schema.String, }, }) @@ -252,24 +270,26 @@ export namespace Tool { type: "session.next.tool.called", ...options, schema: { - ...Base, - callID: Schema.String, + ...ToolBase, tool: Schema.String, input: Schema.Record(Schema.String, Schema.Unknown), provider: Schema.Struct({ executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + metadata: ProviderMetadata.pipe(Schema.optional), }), }, }) export type Called = typeof Called.Type + /** + * Replayable bounded running-tool state. Tools should checkpoint semantic + * transitions or at a bounded cadence, not persist every stdout/stderr chunk. + */ export const Progress = EventV2.define({ type: "session.next.tool.progress", ...options, schema: { - ...Base, - callID: Schema.String, + ...ToolBase, structured: ToolOutput.Structured, content: Schema.Array(ToolOutput.Content), }, @@ -280,13 +300,13 @@ export namespace Tool { type: "session.next.tool.success", ...options, schema: { - ...Base, - callID: Schema.String, + ...ToolBase, structured: ToolOutput.Structured, content: Schema.Array(ToolOutput.Content), + result: Schema.Unknown.pipe(Schema.optional), provider: Schema.Struct({ executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + metadata: ProviderMetadata.pipe(Schema.optional), }), }, }) @@ -296,12 +316,12 @@ export namespace Tool { type: "session.next.tool.failed", ...options, schema: { - ...Base, - callID: Schema.String, + ...ToolBase, error: UnknownError, + result: Schema.Unknown.pipe(Schema.optional), provider: Schema.Struct({ executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + metadata: ProviderMetadata.pipe(Schema.optional), }), }, }) @@ -364,39 +384,39 @@ export namespace Compaction { export type Ended = typeof Ended.Type } -export const All = Schema.Union( - [ - AgentSwitched, - ModelSwitched, - Prompted, - Synthetic, - Shell.Started, - Shell.Ended, - Step.Started, - Step.Ended, - Step.Failed, - Text.Started, - Text.Delta, - Text.Ended, - Tool.Input.Started, - Tool.Input.Delta, - Tool.Input.Ended, - Tool.Called, - Tool.Progress, - Tool.Success, - Tool.Failed, - Reasoning.Started, - Reasoning.Delta, - Reasoning.Ended, - Retried, - Compaction.Started, - Compaction.Delta, - Compaction.Ended, - ], - { - mode: "oneOf", - }, -).pipe(Schema.toTaggedUnion("type")) +const DurableDefinitions = [ + AgentSwitched, + ModelSwitched, + Prompted, + Synthetic, + Shell.Started, + Shell.Ended, + Step.Started, + Step.Ended, + Step.Failed, + Text.Started, + Text.Ended, + Tool.Input.Started, + Tool.Input.Ended, + Tool.Called, + Tool.Progress, + Tool.Success, + Tool.Failed, + Reasoning.Started, + Reasoning.Ended, + Retried, + Compaction.Started, + Compaction.Delta, + Compaction.Ended, +] as const +const EphemeralDefinitions = [Text.Delta, Tool.Input.Delta, Reasoning.Delta] as const + +export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type")) +export type DurableEvent = typeof Durable.Type + +export const All = Schema.Union([...DurableDefinitions, ...EphemeralDefinitions], { mode: "oneOf" }).pipe( + Schema.toTaggedUnion("type"), +) export type Event = typeof All.Type export type Type = Event["type"] diff --git a/packages/core/src/session/execution.ts b/packages/core/src/session/execution.ts new file mode 100644 index 0000000000..9c5f9f4b4f --- /dev/null +++ b/packages/core/src/session/execution.ts @@ -0,0 +1,18 @@ +export * as SessionExecution from "./execution" + +import { Context, Effect, Layer } from "effect" +import { SessionRunner } from "./runner/index" +import { SessionSchema } from "./schema" + +export interface Interface { + /** Explicitly drain one Session, making at least one provider attempt. */ + readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect + /** Schedule a drain after durable work is recorded. Repeated wakeups may coalesce. */ + readonly wake: (sessionID: SessionSchema.ID) => Effect.Effect +} + +/** Routes execution from a Session ID to the runner owned by that Session's Location. */ +export class Service extends Context.Service()("@opencode/v2/SessionExecution") {} + +/** Low-level compatibility layer for callers that only need durable Session recording. */ +export const noopLayer = Layer.succeed(Service, Service.of({ resume: () => Effect.void, wake: () => Effect.void })) diff --git a/packages/core/src/session/execution/local.ts b/packages/core/src/session/execution/local.ts new file mode 100644 index 0000000000..478cecfc2c --- /dev/null +++ b/packages/core/src/session/execution/local.ts @@ -0,0 +1,35 @@ +import { Effect, Layer } from "effect" +import { LocationServiceMap } from "../../location-layer" +import { SessionRunCoordinator } from "../run-coordinator" +import { SessionSchema } from "../schema" +import { SessionStore } from "../store" +import { SessionExecution } from "../execution" + +/** Current-process routing for implicit-local Locations. Future remote placement belongs here. */ +export const layer = Layer.effect( + SessionExecution.Service, + Effect.gen(function* () { + const store = yield* SessionStore.Service + const locations = yield* LocationServiceMap + const scope = yield* Effect.scope + const withCoordinator = Effect.fnUntraced(function* ( + sessionID: SessionSchema.ID, + use: (coordinator: SessionRunCoordinator.Interface) => Effect.Effect, + ) { + const session = yield* store.get(sessionID) + if (!session) return yield* Effect.die(`Session not found: ${sessionID}`) + return yield* SessionRunCoordinator.Service.use(use).pipe(Effect.provide(locations.get(session.location))) + }) + + return SessionExecution.Service.of({ + resume: Effect.fn("SessionExecution.resume")(function* (sessionID) { + return yield* withCoordinator(sessionID, (coordinator) => coordinator.run(sessionID)) + }), + wake: Effect.fn("SessionExecution.wake")(function* (sessionID) { + yield* withCoordinator(sessionID, (coordinator) => + coordinator.wake(sessionID).pipe(Effect.andThen(coordinator.awaitIdle(sessionID))), + ).pipe(Effect.forkIn(scope), Effect.asVoid) + }), + }) + }), +) diff --git a/packages/core/src/session/info.ts b/packages/core/src/session/info.ts new file mode 100644 index 0000000000..2308d06460 --- /dev/null +++ b/packages/core/src/session/info.ts @@ -0,0 +1,47 @@ +import { DateTime } from "effect" +import { AgentV2 } from "../agent" +import { Location } from "../location" +import { ModelV2 } from "../model" +import { ProjectV2 } from "../project" +import { ProviderV2 } from "../provider" +import { AbsolutePath, RelativePath } from "../schema" +import { WorkspaceV2 } from "../workspace" +import { SessionSchema } from "./schema" +import { SessionTable } from "./sql" + +export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info { + return SessionSchema.Info.make({ + id: SessionSchema.ID.make(row.id), + projectID: ProjectV2.ID.make(row.project_id), + title: row.title, + parentID: row.parent_id ? SessionSchema.ID.make(row.parent_id) : undefined, + agent: row.agent ? AgentV2.ID.make(row.agent) : undefined, + model: row.model + ? { + id: ModelV2.ID.make(row.model.id), + providerID: ProviderV2.ID.make(row.model.providerID), + variant: ModelV2.VariantID.make(row.model.variant ?? "default"), + } + : undefined, + cost: row.cost, + tokens: { + input: row.tokens_input, + output: row.tokens_output, + reasoning: row.tokens_reasoning, + cache: { + read: row.tokens_cache_read, + write: row.tokens_cache_write, + }, + }, + location: Location.Ref.make({ + directory: AbsolutePath.make(row.directory), + workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined, + }), + subpath: row.path ? RelativePath.make(row.path) : undefined, + time: { + created: DateTime.makeUnsafe(row.time_created), + updated: DateTime.makeUnsafe(row.time_updated), + archived: row.time_archived ? DateTime.makeUnsafe(row.time_archived) : undefined, + }, + }) +} diff --git a/packages/core/src/session/input.ts b/packages/core/src/session/input.ts new file mode 100644 index 0000000000..fd937e53fe --- /dev/null +++ b/packages/core/src/session/input.ts @@ -0,0 +1,285 @@ +export * as SessionInput from "./input" + +import { and, asc, eq, inArray, isNull } from "drizzle-orm" +import { DateTime, Effect, Schema } from "effect" +import type { Database } from "../database/database" +import type { EventV2 } from "../event" +import { EventTable } from "../event/sql" +import { NonNegativeInt, PositiveInt } from "../schema" +import { V2Schema } from "../v2-schema" +import { SessionEvent } from "./event" +import { SessionMessage } from "./message" +import { Prompt } from "./prompt" +import { SessionSchema } from "./schema" +import { SessionInputTable, SessionMessageTable } from "./sql" + +type DatabaseService = Database.Interface["db"] + +export const Delivery = Schema.Literals(["steer", "queue"]) +export type Delivery = typeof Delivery.Type + +export class Admitted extends Schema.Class("SessionInput.Admitted")({ + seq: PositiveInt, + id: SessionMessage.ID, + sessionID: SessionSchema.ID, + prompt: Prompt, + delivery: Delivery, + timeCreated: V2Schema.DateTimeUtcFromMillis, + promotedSeq: NonNegativeInt.pipe(Schema.optional), +}) {} + +const decodePrompt = Schema.decodeUnknownSync(Prompt) +const encodePrompt = Schema.encodeSync(Prompt) +const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message) + +const fromRow = (row: typeof SessionInputTable.$inferSelect): Admitted => + new Admitted({ + seq: row.seq, + id: SessionMessage.ID.make(row.id), + sessionID: SessionSchema.ID.make(row.session_id), + prompt: decodePrompt(row.prompt), + delivery: row.delivery, + timeCreated: DateTime.makeUnsafe(row.time_created), + ...(row.promoted_seq === null ? {} : { promotedSeq: row.promoted_seq }), + }) + +export const find = Effect.fn("SessionInput.find")(function* (db: DatabaseService, id: SessionMessage.ID) { + const row = yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie) + return row === undefined ? undefined : fromRow(row) +}) + +export const admit = Effect.fn("SessionInput.admit")(function* ( + db: DatabaseService, + input: { + readonly id: SessionMessage.ID + readonly sessionID: SessionSchema.ID + readonly prompt: Prompt + readonly delivery: Delivery + }, +) { + return yield* db + .transaction( + () => + Effect.gen(function* () { + const existing = yield* find(db, input.id) + if (existing !== undefined) return existing + const event = yield* db + .select({ id: EventTable.id }) + .from(EventTable) + .where(eq(EventTable.id, input.id)) + .get() + .pipe(Effect.orDie) + const message = yield* db + .select({ id: SessionMessageTable.id }) + .from(SessionMessageTable) + .where(eq(SessionMessageTable.id, input.id)) + .get() + .pipe(Effect.orDie) + if (event !== undefined || message !== undefined) return undefined + const row = yield* db + .insert(SessionInputTable) + .values({ + id: input.id, + session_id: input.sessionID, + prompt: encodePrompt(input.prompt), + delivery: input.delivery, + }) + .returning() + .get() + .pipe(Effect.orDie) + return fromRow(row) + }), + { behavior: "immediate" }, + ) + .pipe(Effect.orDie) +}) + +export const hasPending = Effect.fn("SessionInput.hasPending")(function* ( + db: DatabaseService, + sessionID: SessionSchema.ID, + deliveries: ReadonlyArray = ["steer", "queue"], +) { + if (deliveries.length === 0) return false + const row = yield* db + .select({ id: SessionInputTable.id }) + .from(SessionInputTable) + .where( + and( + eq(SessionInputTable.session_id, sessionID), + isNull(SessionInputTable.promoted_seq), + inArray(SessionInputTable.delivery, deliveries), + ), + ) + .limit(1) + .get() + .pipe(Effect.orDie) + return row !== undefined +}) + +export const equivalent = ( + input: Admitted, + expected: { + readonly sessionID: SessionSchema.ID + readonly prompt: Prompt + readonly delivery: Delivery + }, +) => input.delivery === expected.delivery && matchesPrompt(input, expected) + +const matchesPrompt = (input: Admitted, expected: { readonly sessionID: SessionSchema.ID; readonly prompt: Prompt }) => + input.sessionID === expected.sessionID && JSON.stringify(encodePrompt(input.prompt)) === JSON.stringify(encodePrompt(expected.prompt)) + +export const guardReservedID = Effect.fn("SessionInput.guardReservedID")(function* ( + db: DatabaseService, + event: EventV2.Payload, +) { + const admitted = yield* find(db, event.id) + if (admitted === undefined) return + if (!Schema.is(SessionEvent.Prompted)(event)) + return yield* Effect.die("Durable event conflicts with admitted prompt input") + if (!equivalent(admitted, event.data)) return yield* Effect.die("Prompt projection conflicts with admitted input") +}) + +export const project = Effect.fn("SessionInput.project")(function* ( + db: DatabaseService, + input: { + readonly id: SessionMessage.ID + readonly sessionID: SessionSchema.ID + readonly prompt: Prompt + readonly delivery: Delivery + readonly timeCreated: DateTime.Utc + readonly promotedSeq: number + }, +) { + yield* db + .insert(SessionInputTable) + .values({ + id: input.id, + session_id: input.sessionID, + prompt: encodePrompt(input.prompt), + delivery: input.delivery, + promoted_seq: input.promotedSeq, + time_created: DateTime.toEpochMillis(input.timeCreated), + }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + const admitted = yield* find(db, input.id) + if (admitted === undefined || admitted.delivery !== input.delivery || !matchesPrompt(admitted, input)) + return yield* Effect.die("Prompt projection conflicts with admitted input") + yield* db + .update(SessionInputTable) + .set({ promoted_seq: input.promotedSeq }) + .where( + and( + eq(SessionInputTable.id, input.id), + eq(SessionInputTable.session_id, input.sessionID), + isNull(SessionInputTable.promoted_seq), + ), + ) + .run() + .pipe(Effect.orDie) + return yield* find(db, input.id) +}) + +export const reconcileProjected = Effect.fn("SessionInput.reconcileProjected")(function* ( + db: DatabaseService, + expected: { + readonly id: SessionMessage.ID + readonly sessionID: SessionSchema.ID + readonly prompt: Prompt + readonly delivery: Delivery + }, +) { + if (expected.delivery !== "steer") return undefined + const row = yield* db + .select() + .from(SessionMessageTable) + .where(eq(SessionMessageTable.id, expected.id)) + .get() + .pipe(Effect.orDie) + if (row === undefined || row.session_id !== expected.sessionID || row.type !== "user") return undefined + const message = decodeMessage({ ...row.data, id: row.id, type: row.type }) + if (message.type !== "user" || !Prompt.equivalence(Prompt.fromUserMessage(message), expected.prompt)) return undefined + return yield* project(db, { + id: expected.id, + sessionID: expected.sessionID, + prompt: expected.prompt, + delivery: expected.delivery, + timeCreated: message.time.created, + promotedSeq: row.seq, + }) +}) + +const publish = Effect.fn("SessionInput.publish")(function* ( + events: EventV2.Interface, + sessionID: SessionSchema.ID, + rows: ReadonlyArray, +) { + for (const row of rows) { + yield* events.publish( + SessionEvent.Prompted, + { + sessionID, + timestamp: DateTime.makeUnsafe(row.time_created), + prompt: decodePrompt(row.prompt), + delivery: row.delivery, + }, + { id: SessionMessage.ID.make(row.id) }, + ) + } + return rows.length +}) + +export const promoteSteers = Effect.fn("SessionInput.promoteSteers")(function* ( + db: DatabaseService, + events: EventV2.Interface, + sessionID: SessionSchema.ID, +) { + const rows = yield* db + .select() + .from(SessionInputTable) + .where( + and( + eq(SessionInputTable.session_id, sessionID), + isNull(SessionInputTable.promoted_seq), + eq(SessionInputTable.delivery, "steer"), + ), + ) + .orderBy(asc(SessionInputTable.seq)) + .all() + .pipe(Effect.orDie) + return yield* publish(events, sessionID, rows) +}) + +export const promoteNextQueued = Effect.fn("SessionInput.promoteNextQueued")(function* ( + db: DatabaseService, + events: EventV2.Interface, + sessionID: SessionSchema.ID, +) { + const row = yield* db + .select() + .from(SessionInputTable) + .where( + and( + eq(SessionInputTable.session_id, sessionID), + isNull(SessionInputTable.promoted_seq), + eq(SessionInputTable.delivery, "queue"), + ), + ) + .orderBy(asc(SessionInputTable.seq)) + .limit(1) + .get() + .pipe(Effect.orDie) + return row === undefined ? false : yield* publish(events, sessionID, [row]).pipe(Effect.as(true)) +}) + +export const toMessage = (input: Admitted) => + new SessionMessage.User({ + id: input.id, + type: "user", + text: input.prompt.text, + files: input.prompt.files, + agents: input.prompt.agents, + references: input.prompt.references, + time: { created: input.timeCreated }, + }) diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index 1c1e218805..0f8e8088f9 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -1,4 +1,4 @@ -import { produce, type WritableDraft } from "immer" +import { castDraft, produce, type WritableDraft } from "immer" import { Effect } from "effect" import { SessionEvent } from "./event" import { SessionMessage } from "./message" @@ -9,6 +9,7 @@ export type MemoryState = { export interface Adapter { readonly getCurrentAssistant: () => Effect.Effect + readonly getAssistant: (messageID: SessionMessage.ID) => Effect.Effect readonly getCurrentCompaction: () => Effect.Effect readonly getCurrentShell: (callID: string) => Effect.Effect readonly updateAssistant: (assistant: SessionMessage.Assistant) => Effect.Effect @@ -18,8 +19,10 @@ export interface Adapter { } export function memory(state: MemoryState): Adapter { - const activeAssistantIndex = () => - state.messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed) + const assistantIndex = (messageID: SessionMessage.ID) => + state.messages.findLastIndex((message) => message.id === messageID) + // A newer turn supersedes stale incomplete rows; never resume an older assistant projection. + const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant") const activeCompactionIndex = () => state.messages.findLastIndex((message) => message.type === "compaction") const activeShellIndex = (callID: string) => state.messages.findLastIndex((message) => message.type === "shell" && message.callID === callID) @@ -27,7 +30,15 @@ export function memory(state: MemoryState): Adapter { return { getCurrentAssistant() { return Effect.sync(() => { - const index = activeAssistantIndex() + const index = latestAssistantIndex() + if (index < 0) return + const assistant = state.messages[index] + return assistant?.type === "assistant" && !assistant.time.completed ? assistant : undefined + }) + }, + getAssistant(messageID) { + return Effect.sync(() => { + const index = assistantIndex(messageID) if (index < 0) return const assistant = state.messages[index] return assistant?.type === "assistant" ? assistant : undefined @@ -51,7 +62,7 @@ export function memory(state: MemoryState): Adapter { }, updateAssistant(assistant) { return Effect.sync(() => { - const index = activeAssistantIndex() + const index = assistantIndex(assistant.id) if (index < 0) return const current = state.messages[index] if (current?.type !== "assistant") return @@ -95,12 +106,18 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { (item): item is DraftTool => item.type === "tool" && (callID === undefined || item.id === callID), ) - const latestText = (assistant: DraftAssistant | undefined) => - assistant?.content.findLast((item): item is DraftText => item.type === "text") + const latestText = (assistant: DraftAssistant | undefined, textID: string) => + assistant?.content.findLast((item): item is DraftText => item.type === "text" && item.id === textID) const latestReasoning = (assistant: DraftAssistant | undefined, reasoningID: string) => assistant?.content.findLast((item): item is DraftReasoning => item.type === "reasoning" && item.id === reasoningID) + const updateOwnedAssistant = (messageID: SessionMessage.ID, recipe: (draft: DraftAssistant) => void) => + Effect.gen(function* () { + const assistant = yield* adapter.getAssistant(messageID) + if (assistant) yield* adapter.updateAssistant(produce(assistant, recipe)) + }) + return Effect.gen(function* () { yield* SessionEvent.All.match(event, { "session.next.agent.switched": (event) => { @@ -200,42 +217,30 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { }) }, "session.next.step.ended": (event) => { - return Effect.gen(function* () { - const currentAssistant = yield* adapter.getCurrentAssistant() - if (currentAssistant) { - yield* adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.time.completed = event.data.timestamp - draft.finish = event.data.finish - draft.cost = event.data.cost - draft.tokens = event.data.tokens - if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, end: event.data.snapshot } - }), - ) - } + return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { + draft.time.completed = event.data.timestamp + draft.finish = event.data.finish + draft.cost = event.data.cost + draft.tokens = event.data.tokens + if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, end: event.data.snapshot } }) }, "session.next.step.failed": (event) => { - return Effect.gen(function* () { - const currentAssistant = yield* adapter.getCurrentAssistant() - if (currentAssistant) { - yield* adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.time.completed = event.data.timestamp - draft.finish = "error" - draft.error = event.data.error - }), - ) - } + return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { + draft.time.completed = event.data.timestamp + draft.finish = "error" + draft.error = event.data.error }) }, - "session.next.text.started": () => { + "session.next.text.started": (event) => { return Effect.gen(function* () { const currentAssistant = yield* adapter.getCurrentAssistant() if (currentAssistant) { yield* adapter.updateAssistant( produce(currentAssistant, (draft) => { - draft.content.push(new SessionMessage.AssistantText({ type: "text", text: "" }) as DraftText) + draft.content.push( + castDraft(new SessionMessage.AssistantText({ type: "text", id: event.data.textID, text: "" })), + ) }), ) } @@ -247,7 +252,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { if (currentAssistant) { yield* adapter.updateAssistant( produce(currentAssistant, (draft) => { - const match = latestText(draft) + const match = latestText(draft, event.data.textID) if (match) match.text += event.data.delta }), ) @@ -260,7 +265,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { if (currentAssistant) { yield* adapter.updateAssistant( produce(currentAssistant, (draft) => { - const match = latestText(draft) + const match = latestText(draft, event.data.textID) if (match) match.text = event.data.text }), ) @@ -268,118 +273,93 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { }) }, "session.next.tool.input.started": (event) => { - return Effect.gen(function* () { - const currentAssistant = yield* adapter.getCurrentAssistant() - if (currentAssistant) { - yield* adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.content.push( - new SessionMessage.AssistantTool({ - type: "tool", - id: event.data.callID, - name: event.data.name, - time: { created: event.data.timestamp }, - state: new SessionMessage.ToolStatePending({ status: "pending", input: "" }), - }) as DraftTool, - ) + return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { + draft.content.push( + castDraft( + new SessionMessage.AssistantTool({ + type: "tool", + id: event.data.callID, + name: event.data.name, + time: { created: event.data.timestamp }, + state: new SessionMessage.ToolStatePending({ status: "pending", input: "" }), }), - ) - } + ), + ) }) }, - "session.next.tool.input.delta": (event) => { - return Effect.gen(function* () { - const currentAssistant = yield* adapter.getCurrentAssistant() - if (currentAssistant) { - yield* adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.data.callID) - // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string) - if (match && match.state.status === "pending") match.state.input += event.data.delta - }), - ) - } + "session.next.tool.input.delta": () => Effect.void, + "session.next.tool.input.ended": (event) => { + return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match && match.state.status === "pending") match.state.input = event.data.text }) }, - "session.next.tool.input.ended": () => Effect.void, "session.next.tool.called": (event) => { - return Effect.gen(function* () { - const currentAssistant = yield* adapter.getCurrentAssistant() - if (currentAssistant) { - yield* adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.data.callID) - if (match) { - match.provider = event.data.provider - match.time.ran = event.data.timestamp - match.state = { - status: "running", - input: event.data.input, - structured: {}, - content: [], - } - } + return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match) { + match.provider = event.data.provider + match.time.ran = event.data.timestamp + match.state = castDraft( + new SessionMessage.ToolStateRunning({ + status: "running", + input: event.data.input, + structured: {}, + content: [], }), ) } }) }, "session.next.tool.progress": (event) => { - return Effect.gen(function* () { - const currentAssistant = yield* adapter.getCurrentAssistant() - if (currentAssistant) { - yield* adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.data.callID) - if (match && match.state.status === "running") { - match.state.structured = event.data.structured - match.state.content = [...event.data.content] - } - }), - ) + return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match && match.state.status === "running") { + match.state.structured = event.data.structured + match.state.content = [...event.data.content] } }) }, "session.next.tool.success": (event) => { - return Effect.gen(function* () { - const currentAssistant = yield* adapter.getCurrentAssistant() - if (currentAssistant) { - yield* adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.data.callID) - if (match && match.state.status === "running") { - match.provider = event.data.provider - match.time.completed = event.data.timestamp - match.state = { - status: "completed", - input: match.state.input, - structured: event.data.structured, - content: [...event.data.content], - } - } + return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match && match.state.status === "running") { + match.provider = { + executed: event.data.provider.executed || match.provider?.executed === true, + metadata: match.provider?.metadata, + resultMetadata: event.data.provider.metadata, + } + match.time.completed = event.data.timestamp + match.state = castDraft( + new SessionMessage.ToolStateCompleted({ + status: "completed", + input: match.state.input, + structured: event.data.structured, + content: [...event.data.content], + result: event.data.result, }), ) } }) }, "session.next.tool.failed": (event) => { - return Effect.gen(function* () { - const currentAssistant = yield* adapter.getCurrentAssistant() - if (currentAssistant) { - yield* adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.data.callID) - if (match && match.state.status === "running") { - match.provider = event.data.provider - match.time.completed = event.data.timestamp - match.state = { - status: "error", - error: event.data.error, - input: match.state.input, - structured: match.state.structured, - content: match.state.content, - } - } + return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match && (match.state.status === "pending" || match.state.status === "running")) { + match.provider = { + executed: event.data.provider.executed || match.provider?.executed === true, + metadata: match.provider?.metadata, + resultMetadata: event.data.provider.metadata, + } + match.time.completed = event.data.timestamp + match.state = castDraft( + new SessionMessage.ToolStateError({ + status: "error", + error: event.data.error, + input: typeof match.state.input === "string" ? {} : match.state.input, + structured: match.state.status === "running" ? match.state.structured : {}, + content: match.state.status === "running" ? match.state.content : [], + result: event.data.result, }), ) } @@ -392,11 +372,14 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { yield* adapter.updateAssistant( produce(currentAssistant, (draft) => { draft.content.push( - new SessionMessage.AssistantReasoning({ - type: "reasoning", - id: event.data.reasoningID, - text: "", - }) as DraftReasoning, + castDraft( + new SessionMessage.AssistantReasoning({ + type: "reasoning", + id: event.data.reasoningID, + text: "", + providerMetadata: event.data.providerMetadata, + }), + ), ) }), ) @@ -423,7 +406,10 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { yield* adapter.updateAssistant( produce(currentAssistant, (draft) => { const match = latestReasoning(draft, event.data.reasoningID) - if (match) match.text = event.data.text + if (match) { + match.text = event.data.text + if (event.data.providerMetadata !== undefined) match.providerMetadata = event.data.providerMetadata + } }), ) } diff --git a/packages/core/src/session/message.ts b/packages/core/src/session/message.ts index 9de73a17bb..f394df5a57 100644 --- a/packages/core/src/session/message.ts +++ b/packages/core/src/session/message.ts @@ -1,6 +1,7 @@ export * as SessionMessage from "./message" import { Schema } from "effect" +import { ProviderMetadata } from "@opencode-ai/llm" import { EventV2 } from "../event" import { ModelV2 } from "../model" import { ToolOutput } from "../tool-output" @@ -80,6 +81,7 @@ export class ToolStateCompleted extends Schema.Class("Sessio attachments: SessionEvent.FileAttachment.pipe(Schema.Array, Schema.optional), content: ToolOutput.Content.pipe(Schema.Array), structured: ToolOutput.Structured, + result: SessionEvent.Tool.Success.data.fields.result, }) {} export class ToolStateError extends Schema.Class("Session.Message.ToolState.Error")({ @@ -88,6 +90,7 @@ export class ToolStateError extends Schema.Class("Session.Messag content: ToolOutput.Content.pipe(Schema.Array), structured: ToolOutput.Structured, error: SessionEvent.UnknownError, + result: SessionEvent.Tool.Failed.data.fields.result, }) {} export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( @@ -101,7 +104,8 @@ export class AssistantTool extends Schema.Class("Session.Message. name: Schema.String, provider: Schema.Struct({ executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + metadata: ProviderMetadata.pipe(Schema.optional), + resultMetadata: ProviderMetadata.pipe(Schema.optional), }).pipe(Schema.optional), state: ToolState, time: Schema.Struct({ @@ -114,6 +118,7 @@ export class AssistantTool extends Schema.Class("Session.Message. export class AssistantText extends Schema.Class("Session.Message.Assistant.Text")({ type: Schema.Literal("text"), + id: Schema.String, text: Schema.String, }) {} @@ -121,6 +126,7 @@ export class AssistantReasoning extends Schema.Class("Sessio type: Schema.Literal("reasoning"), id: Schema.String, text: Schema.String, + providerMetadata: ProviderMetadata.pipe(Schema.optional), }) {} export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe( diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index 413332f833..f54490533a 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -1,6 +1,6 @@ export * as SessionProjector from "./projector" -import { and, eq, sql } from "drizzle-orm" +import { and, desc, eq, sql } from "drizzle-orm" import { DateTime, Effect, Layer, Schema } from "effect" import { Database } from "../database/database" import { EventV2 } from "../event" @@ -9,6 +9,7 @@ import { SessionV1 } from "../v1/session" import { WorkspaceTable } from "../control-plane/workspace.sql" import { SessionMessage } from "./message" import { SessionMessageUpdater } from "./message-updater" +import { SessionInput } from "./input" import { MessageTable, PartTable, SessionMessageTable, SessionTable } from "./sql" import type { DeepMutable } from "../schema" @@ -17,6 +18,9 @@ type DatabaseService = Database.Interface["db"] const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message) const encodeMessage = Schema.encodeSync(SessionMessage.Message) +export class PromptAlreadyProjected extends Error {} +export class SessionAlreadyProjected extends Error {} + type Usage = { cost: number tokens: { @@ -105,37 +109,84 @@ function applyUsage( function run(db: DatabaseService, event: SessionEvent.Event) { return Effect.gen(function* () { + const decodeRow = (row: typeof SessionMessageTable.$inferSelect) => + decodeMessage({ ...row.data, id: row.id, type: row.type }) + const writeMessage = (message: SessionMessage.Message) => { + if (event.seq === undefined) return Effect.die("Synchronized Session event is missing aggregate sequence") + const encoded = encodeMessage(message) + const { id, type, ...data } = encoded + return db + .insert(SessionMessageTable) + .values([ + { + id: SessionMessage.ID.make(id), + session_id: event.data.sessionID, + type, + seq: event.seq, + time_created: DateTime.toEpochMillis(message.time.created), + data, + }, + ]) + .onConflictDoUpdate({ + target: SessionMessageTable.id, + set: { type, time_created: DateTime.toEpochMillis(message.time.created), data }, + }) + .run() + .pipe(Effect.orDie) + } const adapter: SessionMessageUpdater.Adapter = { getCurrentAssistant() { return Effect.gen(function* () { - const rows = yield* db + // A newer turn supersedes stale incomplete rows; never resume an older assistant projection. + const row = yield* db .select() .from(SessionMessageTable) .where( and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "assistant")), ) - .all() + .orderBy(desc(SessionMessageTable.seq)) + .limit(1) + .get() .pipe(Effect.orDie) - return rows - .map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type })) - .find( - (message): message is SessionMessage.Assistant => message.type === "assistant" && !message.time.completed, + if (!row) return + const message = decodeRow(row) + return message.type === "assistant" && !message.time.completed ? message : undefined + }) + }, + getAssistant(messageID) { + return Effect.gen(function* () { + const row = yield* db + .select() + .from(SessionMessageTable) + .where( + and( + eq(SessionMessageTable.id, messageID), + eq(SessionMessageTable.session_id, event.data.sessionID), + eq(SessionMessageTable.type, "assistant"), + ), ) + .get() + .pipe(Effect.orDie) + if (!row) return + const message = decodeRow(row) + return message.type === "assistant" ? message : undefined }) }, getCurrentCompaction() { return Effect.gen(function* () { - const rows = yield* db + const row = yield* db .select() .from(SessionMessageTable) .where( and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "compaction")), ) - .all() + .orderBy(desc(SessionMessageTable.seq)) + .limit(1) + .get() .pipe(Effect.orDie) - return rows - .map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type })) - .find((message): message is SessionMessage.Compaction => message.type === "compaction") + if (!row) return + const message = decodeRow(row) + return message.type === "compaction" ? message : undefined }) }, getCurrentShell(callID) { @@ -144,121 +195,18 @@ function run(db: DatabaseService, event: SessionEvent.Event) { .select() .from(SessionMessageTable) .where(and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "shell"))) + .orderBy(desc(SessionMessageTable.seq)) .all() .pipe(Effect.orDie) return rows - .map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type })) + .map(decodeRow) .find((message): message is SessionMessage.Shell => message.type === "shell" && message.callID === callID) }) }, - updateAssistant(message) { - return Effect.gen(function* () { - const encoded = encodeMessage(message) - const { id, type, ...data } = encoded - yield* db - .insert(SessionMessageTable) - .values([ - { - id: SessionMessage.ID.make(id), - session_id: event.data.sessionID, - type, - time_created: DateTime.toEpochMillis(message.time.created), - data, - }, - ]) - .onConflictDoUpdate({ - target: SessionMessageTable.id, - set: { - type, - time_created: DateTime.toEpochMillis(message.time.created), - data, - }, - }) - .run() - .pipe(Effect.orDie) - }) - }, - updateCompaction(message) { - return Effect.gen(function* () { - const encoded = encodeMessage(message) - const { id, type, ...data } = encoded - yield* db - .insert(SessionMessageTable) - .values([ - { - id: SessionMessage.ID.make(id), - session_id: event.data.sessionID, - type, - time_created: DateTime.toEpochMillis(message.time.created), - data, - }, - ]) - .onConflictDoUpdate({ - target: SessionMessageTable.id, - set: { - type, - time_created: DateTime.toEpochMillis(message.time.created), - data, - }, - }) - .run() - .pipe(Effect.orDie) - }) - }, - updateShell(message) { - return Effect.gen(function* () { - const encoded = encodeMessage(message) - const { id, type, ...data } = encoded - yield* db - .insert(SessionMessageTable) - .values([ - { - id: SessionMessage.ID.make(id), - session_id: event.data.sessionID, - type, - time_created: DateTime.toEpochMillis(message.time.created), - data, - }, - ]) - .onConflictDoUpdate({ - target: SessionMessageTable.id, - set: { - type, - time_created: DateTime.toEpochMillis(message.time.created), - data, - }, - }) - .run() - .pipe(Effect.orDie) - }) - }, - appendMessage(message) { - return Effect.gen(function* () { - const encoded = encodeMessage(message) - const { id, type, ...data } = encoded - yield* db - .insert(SessionMessageTable) - .values([ - { - id: SessionMessage.ID.make(id), - session_id: event.data.sessionID, - type, - time_created: DateTime.toEpochMillis(message.time.created), - data, - }, - ]) - .onConflictDoUpdate({ - target: SessionMessageTable.id, - set: { - type, - time_created: DateTime.toEpochMillis(message.time.created), - data, - }, - }) - .run() - .pipe(Effect.orDie) - }) - }, + updateAssistant: writeMessage, + updateCompaction: writeMessage, + updateShell: writeMessage, + appendMessage: writeMessage, } yield* SessionMessageUpdater.update(adapter, event) }) @@ -268,9 +216,17 @@ export const layer = Layer.effectDiscard( Effect.gen(function* () { const events = yield* EventV2.Service const { db } = yield* Database.Service + yield* events.beforeCommit((event) => SessionInput.guardReservedID(db, event)) yield* events.project(SessionV1.Event.Created, (event) => Effect.gen(function* () { - yield* db.insert(SessionTable).values(sessionRow(event.data.info)).run().pipe(Effect.orDie) + const stored = yield* db + .insert(SessionTable) + .values(sessionRow(event.data.info)) + .onConflictDoNothing() + .returning({ sessionID: SessionTable.id }) + .get() + .pipe(Effect.orDie) + if (!stored) return yield* Effect.die(new SessionAlreadyProjected()) if (event.data.info.workspaceID) { yield* db .update(WorkspaceTable) @@ -361,93 +317,71 @@ export const layer = Layer.effectDiscard( if (next) yield* applyUsage(db, sessionID, next) }), ) - // session.next.* projectors are disabled while the v2 message projection is stabilized. - // The events still publish through EventV2 and fan out through the opencode bridge. - // yield* events.project(SessionEvent.AgentSwitched, (event) => - // Effect.gen(function* () { - // const message = Schema.encodeSync(SessionMessage.AgentSwitched)( - // new SessionMessage.AgentSwitched({ - // id: event.id, - // type: "agent-switched", - // metadata: event.metadata, - // agent: event.data.agent, - // time: { created: event.data.timestamp }, - // }), - // ) - // const data = { metadata: message.metadata, agent: message.agent, time: message.time } - // yield* db - // .update(SessionTable) - // .set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.data.timestamp) }) - // .where(eq(SessionTable.id, event.data.sessionID)) - // .run() - // .pipe(Effect.orDie) - // yield* db - // .insert(SessionMessageTable) - // .values([ - // { - // id: SessionMessage.ID.make(event.id), - // session_id: event.data.sessionID, - // type: "agent-switched", - // time_created: DateTime.toEpochMillis(event.data.timestamp), - // data, - // }, - // ]) - // .run() - // .pipe(Effect.orDie) - // }), - // ) - // yield* events.project(SessionEvent.ModelSwitched, (event) => - // Effect.gen(function* () { - // const message = Schema.encodeSync(SessionMessage.ModelSwitched)( - // new SessionMessage.ModelSwitched({ - // id: event.id, - // type: "model-switched", - // metadata: event.metadata, - // model: event.data.model, - // time: { created: event.data.timestamp }, - // }), - // ) - // const data = { metadata: message.metadata, model: message.model, time: message.time } - // yield* db - // .update(SessionTable) - // .set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.data.timestamp) }) - // .where(eq(SessionTable.id, event.data.sessionID)) - // .run() - // .pipe(Effect.orDie) - // yield* db - // .insert(SessionMessageTable) - // .values([ - // { - // id: SessionMessage.ID.make(event.id), - // session_id: event.data.sessionID, - // type: "model-switched", - // time_created: DateTime.toEpochMillis(event.data.timestamp), - // data, - // }, - // ]) - // .run() - // .pipe(Effect.orDie) - // }), - // ) - // yield* events.project(SessionEvent.Prompted, (event) => run(db, event)) - // yield* events.project(SessionEvent.Synthetic, (event) => run(db, event)) - // yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event)) - // yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event)) - // yield* events.project(SessionEvent.Step.Started, (event) => run(db, event)) - // yield* events.project(SessionEvent.Step.Ended, (event) => run(db, event)) - // yield* events.project(SessionEvent.Step.Failed, (event) => run(db, event)) - // yield* events.project(SessionEvent.Text.Started, (event) => run(db, event)) - // yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event)) - // yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event)) - // yield* events.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event)) - // yield* events.project(SessionEvent.Tool.Called, (event) => run(db, event)) - // yield* events.project(SessionEvent.Tool.Success, (event) => run(db, event)) - // yield* events.project(SessionEvent.Tool.Failed, (event) => run(db, event)) - // yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event)) - // yield* events.project(SessionEvent.Reasoning.Ended, (event) => run(db, event)) + yield* events.project(SessionEvent.AgentSwitched, (event) => + db.update(SessionTable) + .set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.data.timestamp) }) + .where(eq(SessionTable.id, event.data.sessionID)) + .run() + .pipe(Effect.orDie, Effect.andThen(run(db, event))), + ) + yield* events.project(SessionEvent.ModelSwitched, (event) => + db.update(SessionTable) + .set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.data.timestamp) }) + .where(eq(SessionTable.id, event.data.sessionID)) + .run() + .pipe(Effect.orDie, Effect.andThen(run(db, event))), + ) + yield* events.project(SessionEvent.Prompted, (event) => + Effect.gen(function* () { + const existing = yield* db + .select({ id: SessionMessageTable.id }) + .from(SessionMessageTable) + .where(eq(SessionMessageTable.id, event.id)) + .get() + .pipe(Effect.orDie) + if (existing) return yield* Effect.die(new PromptAlreadyProjected()) + yield* run(db, event) + const row = yield* db + .select() + .from(SessionMessageTable) + .where(eq(SessionMessageTable.id, event.id)) + .get() + .pipe(Effect.orDie) + if (!row) return yield* Effect.die("Prompt projection was not stored") + const message = decodeMessage({ ...row.data, id: row.id, type: row.type }) + if (message.type !== "user") return yield* Effect.die("Prompt projection did not produce a user message") + if (event.seq === undefined) + return yield* Effect.die("Synchronized Session event is missing aggregate sequence") + yield* SessionInput.project(db, { + id: SessionMessage.ID.make(event.id), + sessionID: event.data.sessionID, + prompt: event.data.prompt, + delivery: event.data.delivery, + timeCreated: event.data.timestamp, + promotedSeq: event.seq, + }) + }), + ) + yield* events.project(SessionEvent.Synthetic, (event) => run(db, event)) + yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event)) + yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event)) + yield* events.project(SessionEvent.Step.Started, (event) => run(db, event)) + yield* events.project(SessionEvent.Step.Ended, (event) => run(db, event)) + yield* events.project(SessionEvent.Step.Failed, (event) => run(db, event)) + yield* events.project(SessionEvent.Text.Started, (event) => run(db, event)) + yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event)) + yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event)) + yield* events.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event)) + yield* events.project(SessionEvent.Tool.Called, (event) => run(db, event)) + yield* events.project(SessionEvent.Tool.Progress, (event) => run(db, event)) + yield* events.project(SessionEvent.Tool.Success, (event) => run(db, event)) + yield* events.project(SessionEvent.Tool.Failed, (event) => run(db, event)) + yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event)) + yield* events.project(SessionEvent.Reasoning.Ended, (event) => run(db, event)) // yield* events.project(SessionEvent.Retried, (event) => run(db, event)) - // yield* events.project(SessionEvent.Compaction.Started, (event) => run(db, event)) - // yield* events.project(SessionEvent.Compaction.Ended, (event) => run(db, event)) + yield* events.project(SessionEvent.Compaction.Started, (event) => run(db, event)) + yield* events.project(SessionEvent.Compaction.Delta, (event) => run(db, event)) + yield* events.project(SessionEvent.Compaction.Ended, (event) => run(db, event)) }), ) diff --git a/packages/core/src/session/prompt.ts b/packages/core/src/session/prompt.ts index 14167fc288..f1822bcc17 100644 --- a/packages/core/src/session/prompt.ts +++ b/packages/core/src/session/prompt.ts @@ -46,4 +46,15 @@ export class Prompt extends Schema.Class("Prompt")({ files: Schema.Array(FileAttachment).pipe(Schema.optional), agents: Schema.Array(AgentAttachment).pipe(Schema.optional), references: Schema.Array(ReferenceAttachment).pipe(Schema.optional), -}) {} +}) { + static readonly equivalence = Schema.toEquivalence(Prompt) + + static fromUserMessage(input: Pick) { + return new Prompt({ + text: input.text, + ...(input.files === undefined ? {} : { files: input.files }), + ...(input.agents === undefined ? {} : { agents: input.agents }), + ...(input.references === undefined ? {} : { references: input.references }), + }) + } +} diff --git a/packages/core/src/session/run-coordinator.ts b/packages/core/src/session/run-coordinator.ts new file mode 100644 index 0000000000..c5bed8a42b --- /dev/null +++ b/packages/core/src/session/run-coordinator.ts @@ -0,0 +1,183 @@ +export * as SessionRunCoordinator from "./run-coordinator" + +import { Cause, Context, Deferred, Effect, Exit, FiberSet, Layer, Scope } from "effect" +import { SessionRunner } from "./runner" +import { SessionSchema } from "./schema" + +export type Mode = "run" | "wake" + +/** + * Runs at most one drain chain per key while allowing different keys to drain concurrently. + * + * For each key: + * + * idle --run/wake--> draining --run/wake--> draining + one coalesced rerun --> idle + * + * `run` is an explicit drain request. It starts a chain or joins the current chain and + * upgrades a pending follow-up so the caller receives explicit-run semantics. + * + * `wake` reports that durable work may now be available. It starts a chain while idle or + * requests one coalesced follow-up while draining. Repeated wakes collapse together. + */ +export interface Coordinator { + /** Starts or joins one explicit drain generation. */ + readonly run: (key: Key) => Effect.Effect + /** Coalesces one wake-up after durable work is recorded. */ + readonly wake: (key: Key) => Effect.Effect + /** Waits until the current ownership chain settles. */ + readonly awaitIdle: (key: Key) => Effect.Effect +} + +type Entry = { + readonly done: Deferred.Deferred + mode: Mode + rerun?: Mode + explicit?: Deferred.Deferred +} + +const strongest = (left: Mode | undefined, right: Mode): Mode => (left === "run" || right === "run" ? "run" : "wake") + +/** Constructs a scoped coordinator. Every in-memory transition is synchronous. */ +export const make = (options: { + readonly drain: (key: Key, mode: Mode) => Effect.Effect + readonly onFailure?: (key: Key, cause: Cause.Cause) => Effect.Effect +}): Effect.Effect, never, Scope.Scope> => + Effect.gen(function* () { + const active = new Map>() + const scope = yield* Effect.scope + const fork = yield* FiberSet.makeRuntime() + const shutdown = Deferred.makeUnsafe() + let closed = false + yield* Effect.addFinalizer(() => + Effect.sync(() => { + closed = true + Deferred.doneUnsafe(shutdown, Effect.void) + active.clear() + }), + ) + + const makeEntry = (mode: Mode, explicit?: Deferred.Deferred): Entry => ({ + done: Deferred.makeUnsafe(), + mode, + explicit, + }) + + const start = (key: Key, entry: Entry, mode: Mode) => { + fork(own(key, entry, mode)) + } + + const own = (key: Key, entry: Entry, mode: Mode): Effect.Effect => + Effect.suspend(() => options.drain(key, mode)).pipe( + Effect.exit, + Effect.flatMap((exit) => { + if (closed) return Deferred.done(entry.done, exit).pipe(Effect.asVoid) + if (mode === "run" && entry.explicit !== undefined) { + Deferred.doneUnsafe(entry.explicit, exit) + entry.explicit = undefined + } + if (exit._tag === "Success") { + if (active.get(key) !== entry) return Deferred.done(entry.done, exit).pipe(Effect.asVoid) + if (entry.rerun !== undefined) { + const mode = entry.rerun + entry.rerun = undefined + entry.mode = mode + return own(key, entry, mode) + } + active.delete(key) + return Deferred.done(entry.done, exit).pipe(Effect.asVoid) + } + + const successor = + active.get(key) === entry && entry.rerun !== undefined ? makeEntry(entry.rerun, entry.explicit) : undefined + if (successor === undefined) active.delete(key) + else { + active.set(key, successor) + } + if (successor !== undefined) start(key, successor, successor.mode) + const report = + mode === "wake" && options.onFailure !== undefined + ? options.onFailure(key, exit.cause).pipe(Effect.forkIn(scope), Effect.asVoid) + : Effect.void + return Deferred.done(entry.done, exit).pipe(Effect.andThen(report), Effect.asVoid) + }), + ) + + const wake = (key: Key) => + Effect.sync(() => { + if (closed) return + const entry = active.get(key) + if (entry !== undefined) { + entry.rerun = strongest(entry.rerun, "wake") + return + } + + const next = makeEntry("wake") + active.set(key, next) + start(key, next, "wake") + }) + + const awaitIdle = (key: Key): Effect.Effect => + Effect.gen(function* () { + let firstFailure: Cause.Cause | undefined + while (!closed) { + const entry = active.get(key) + if (entry === undefined) break + const exit = yield* Effect.raceFirst( + Deferred.await(entry.done).pipe(Effect.exit), + Deferred.await(shutdown).pipe(Effect.as(Exit.void)), + ) + if (closed) break + if (exit._tag === "Failure" && firstFailure === undefined) firstFailure = exit.cause + } + if (firstFailure !== undefined) return yield* Effect.failCause(firstFailure) + }) + + return { run, wake, awaitIdle } + + function run(key: Key): Effect.Effect { + return Effect.uninterruptibleMask((restore) => { + if (closed) return Effect.interrupt + const entry = active.get(key) + if (entry !== undefined) { + if (entry.mode === "wake") { + entry.rerun = "run" + entry.explicit ??= Deferred.makeUnsafe() + return restore(awaitRun(entry.explicit)) + } + return restore(awaitRun(entry.done)) + } + + const next = makeEntry("run") + active.set(key, next) + start(key, next, "run") + return restore(awaitRun(next.done)) + }) + } + + function awaitRun(done: Deferred.Deferred): Effect.Effect { + return Effect.raceFirst(Deferred.await(done), Deferred.await(shutdown).pipe(Effect.andThen(Effect.interrupt))) + } + }) + +export interface Interface extends Coordinator {} + +export class Service extends Context.Service()("@opencode/v2/SessionRunCoordinator") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const runner = yield* SessionRunner.Service + return Service.of( + yield* make({ + drain: (sessionID, mode) => runner.run({ sessionID, force: mode === "run" }), + onFailure: (sessionID, cause) => + Cause.hasInterruptsOnly(cause) + ? Effect.void + : Effect.logError("Failed to drain Session").pipe( + Effect.annotateLogs("sessionID", sessionID), + Effect.annotateLogs("cause", cause), + ), + }), + ) + }), +) diff --git a/packages/core/src/session/runner/index.ts b/packages/core/src/session/runner/index.ts new file mode 100644 index 0000000000..fe17284b8a --- /dev/null +++ b/packages/core/src/session/runner/index.ts @@ -0,0 +1,28 @@ +export * as SessionRunner from "./index" + +import type { LLMError } from "@opencode-ai/llm" +import { Context, Effect, Schema } from "effect" +import { SessionSchema } from "../schema" +import type { MessageDecodeError } from "../error" +import { SessionRunnerModel } from "./model" + +export class StepLimitExceededError extends Schema.TaggedErrorClass()( + "SessionRunner.StepLimitExceededError", + { + sessionID: SessionSchema.ID, + limit: Schema.Int, + }, +) {} + +export type RunError = LLMError | SessionRunnerModel.Error | MessageDecodeError | StepLimitExceededError + +/** Runs one local continuation from already-recorded Session history. */ +export interface Interface { + /** Drains eligible durable work. Explicit runs perform one provider attempt even when no work is eligible. */ + readonly run: (input: { + readonly sessionID: SessionSchema.ID + readonly force?: boolean + }) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/SessionRunner") {} diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts new file mode 100644 index 0000000000..6b51e3900c --- /dev/null +++ b/packages/core/src/session/runner/llm.ts @@ -0,0 +1,262 @@ +import { LLM, LLMClient, LLMError, LLMEvent } from "@opencode-ai/llm" +import { Cause, DateTime, Effect, FiberSet, Layer, Semaphore, Stream } from "effect" +import { EventV2 } from "../../event" +import { ModelV2 } from "../../model" +import { ProviderV2 } from "../../provider" +import { SessionSchema } from "../schema" +import { SessionEvent } from "../event" +import { SessionStore } from "../store" +import { Service, StepLimitExceededError } from "./index" +import { createLLMEventPublisher } from "./publish-llm-event" +import { toLLMMessages } from "./to-llm-message" +import { ToolRegistry } from "../../tool-registry" +import { SessionRunnerModel } from "./model" +import { Database } from "../../database/database" +import { SessionInput } from "../input" +import { QuestionV2 } from "../../question" + +/** + * Runs one durable coding-agent Session until it settles. + * + * Keep this as orchestration over smaller collaborators rather than rebuilding the legacy + * `SessionPrompt` monolith. Implement the unchecked items in small reviewed slices: + * + * - Session ownership and controls + * - [x] Coordinate one local active drain per Session; explicit resumes join and prompt wakeups coalesce. + * - [ ] Replace local ownership with durable multi-node ownership when clustered. + * - [ ] Mark busy, retrying, idle, interrupted, or terminal-failure status durably. + * - [ ] Honor interruption and reject stale work after runtime attachment replacement. + * - [x] Bound model steps. + * - [ ] Bound provider retries and repeated identical tool calls. + * + * - Runtime context assembly + * - [x] Load Session placement and chronological projected V2 history. + * - [x] Resolve the selected model through the location-scoped runner environment. + * - [ ] Load the selected agent and effective permissions. + * - [ ] Build provider/model-specific base instructions and environment facts. + * - [ ] Load configured project instructions such as `AGENTS.md`, remote instructions, and + * nearby nested instructions discovered while files are read. + * - [ ] List available skills in the system prompt and expose a tool for loading skill bodies. + * - [ ] Resolve referenced files, directories, agents, repositories, MCP resources, and media. + * - [ ] Apply steering reminders, plugin transforms, and structured-output policy. + * - [ ] Compact or summarize history when context pressure requires it. + * + * - One provider turn + * - [x] Translate every projected V2 Session message variant into canonical + * `@opencode-ai/llm` messages. + * - [ ] Resolve policy-filtered built-in, MCP, plugin, and structured-output tool definitions. + * - [x] Stream exactly one `llm.stream(request)` provider turn. + * - [x] Persist assistant text and usage events incrementally as they arrive. + * - [ ] Persist snapshots, patches, and retry notices incrementally as they arrive. + * - [x] Persist reasoning, provider errors, and tool-call events incrementally as they arrive. + * + * - Tool settlement and continuation + * - [x] Durably record each tool call before side effects begin. + * - [x] Authorize and execute recorded local calls through a core-owned registry hook. + * - [x] Persist typed success, failure, and provider-executed tool outcomes. + * - [x] Start each recorded local call eagerly and await all settlements before continuation. + * - [ ] Add scoped runtime context, progress updates, output truncation, attachment normalization, + * plugins, and cancellation settlement. + * - [x] Reload projected history and start the next explicit provider turn after local tool results. + * - [x] Continue for durable user steering accepted during an active provider turn. + * - [ ] Continue for compaction or another continuation condition when required. + * + * - Post-run maintenance + * - [ ] Settle final status and expose durable output events to replayable consumers. + * - [ ] Coalesce streamed deltas and add covering projected-history indexes. + * - [ ] Update title, summaries, compaction state, and cleanup in bounded background work. + * + * Use `llm.stream(request)` for each provider turn. Keep tool execution and continuation here. + * Durable activity recovery remains a separate future slice with an explicit retry policy. + * + * The current slice loads V2 history, translates it, resolves a model through a core service, and persists one + * provider turn. Registry definitions are advertised, local tool calls are settled durably, and a + * bounded explicit loop starts the next provider turn after local settlement. + */ + +// QUESTION: Did this exist previously, or did we add this limit? Does it make sense? +const MAX_STEPS = 25 + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const events = yield* EventV2.Service + const llm = yield* LLMClient.Service + const tools = yield* ToolRegistry.Service + const models = yield* SessionRunnerModel.Service + const store = yield* SessionStore.Service + const db = (yield* Database.Service).db + const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) { + const session = yield* store.get(sessionID) + if (!session) return yield* Effect.die(`Session not found: ${sessionID}`) + return session + }) + + const getContext = Effect.fn("SessionRunner.getContext")(function* (sessionID: SessionSchema.ID) { + return yield* store.context(sessionID) + }) + + const failInterruptedTools = Effect.fn("SessionRunner.failInterruptedTools")(function* ( + sessionID: SessionSchema.ID, + ) { + for (const message of yield* getContext(sessionID)) { + if (message.type !== "assistant") continue + for (const tool of message.content) { + if (tool.type !== "tool" || (tool.state.status !== "pending" && tool.state.status !== "running")) continue + yield* events.publish(SessionEvent.Tool.Failed, { + sessionID, + timestamp: yield* DateTime.now, + assistantMessageID: message.id, + callID: tool.id, + error: { type: "unknown", message: "Tool execution interrupted" }, + provider: { + executed: tool.provider?.executed === true, + ...(tool.provider?.metadata === undefined ? {} : { metadata: tool.provider.metadata }), + }, + }) + } + } + }) + + const awaitToolFibers = (fibers: FiberSet.FiberSet) => + Effect.raceFirst(FiberSet.join(fibers), FiberSet.awaitEmpty(fibers)) + + // Match V1: dismissing a question halts the loop instead of becoming model-facing tool output. + const isQuestionRejected = (cause: Cause.Cause) => + cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect instanceof QuestionV2.RejectedError) + + const runTurn = Effect.fn("SessionRunner.runTurn")(function* ( + session: SessionSchema.Info, + promotion: "steer" | "queue" | undefined, + ) { + const model = yield* models.resolve(session) + const toolFibers = yield* FiberSet.make() + let needsContinuation = false + if (promotion === "steer") yield* SessionInput.promoteSteers(db, events, session.id) + if (promotion === "queue") { + yield* SessionInput.promoteNextQueued(db, events, session.id) + yield* SessionInput.promoteSteers(db, events, session.id) + } + yield* failInterruptedTools(session.id) + const context = yield* getContext(session.id) + const request = LLM.request({ model, messages: toLLMMessages(context, model), tools: yield* tools.definitions() }) + const publisher = createLLMEventPublisher(events, { + sessionID: session.id, + agent: session.agent ?? "build", + model: { + id: ModelV2.ID.make(model.id), + providerID: ProviderV2.ID.make(model.provider), + ...(session.model?.variant === undefined ? {} : { variant: session.model.variant }), + }, + }) + const withPublication = Semaphore.makeUnsafe(1).withPermit + const publish = (event: LLMEvent) => withPublication(publisher.publish(event)) + const providerStream = llm.stream(request).pipe( + Stream.runForEach((event) => + Effect.gen(function* () { + yield* publish(event) + if (event.type !== "tool-call" || event.providerExecuted) return + needsContinuation = true + yield* tools.settle({ sessionID: session.id, call: event }).pipe( + Effect.catchCause((cause) => { + if (isQuestionRejected(cause)) return Effect.failCause(cause) + return Effect.succeed({ + result: { type: "error" as const, value: String(Cause.squash(cause)) }, + output: undefined, + }) + }), + Effect.flatMap((settlement) => + publish( + LLMEvent.toolResult({ + id: event.id, + name: event.name, + result: settlement.result, + output: settlement.output, + }), + ), + ), + FiberSet.run(toolFibers), + ) + }), + ), + Effect.ensuring(withPublication(publisher.flush())), + ) + + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const stream = yield* restore(providerStream).pipe(Effect.exit) + let llmFailure: LLMError | undefined + if (stream._tag === "Failure") { + for (const reason of stream.cause.reasons) { + if (!Cause.isFailReason(reason)) continue + if (reason.error instanceof LLMError) llmFailure = reason.error + } + } + if (llmFailure && !publisher.hasProviderError()) { + yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true)) + yield* withPublication( + events.publish(SessionEvent.Step.Failed, { + sessionID: session.id, + timestamp: yield* DateTime.now, + assistantMessageID: yield* publisher.startAssistant(), + error: { type: "unknown", message: llmFailure.reason.message }, + }), + ) + } + if (stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) yield* FiberSet.clear(toolFibers) + const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit) + if (settled._tag === "Failure" && isQuestionRejected(settled.cause)) { + yield* FiberSet.clear(toolFibers) + yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted")) + return yield* Effect.interrupt + } + if ( + (stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) || + (settled._tag === "Failure" && Cause.hasInterrupts(settled.cause)) + ) { + yield* FiberSet.clear(toolFibers) + yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted")) + } + if (publisher.hasProviderError()) + yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted")) + if (stream._tag === "Success" && !publisher.hasProviderError()) + yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true)) + const attempt = stream._tag === "Failure" ? stream : settled + if (attempt._tag === "Failure") return yield* Effect.failCause(attempt.cause) + return !publisher.hasProviderError() && needsContinuation + }), + ) + }, Effect.scoped) + + const run = Effect.fn("SessionRunner.run")(function* (input: { + readonly sessionID: SessionSchema.ID + readonly force?: boolean + }) { + const session = yield* getSession(input.sessionID) + const hasSteer = yield* SessionInput.hasPending(db, input.sessionID, ["steer"]) + const hasQueue = yield* SessionInput.hasPending(db, input.sessionID, ["queue"]) + if (input.force !== true && !hasSteer && !hasQueue) return + let promotion: "steer" | "queue" | undefined = hasSteer ? "steer" : hasQueue ? "queue" : undefined + let openActivity = input.force === true || hasSteer || hasQueue + while (openActivity) { + let needsContinuation = true + for (let step = 0; step < MAX_STEPS; step++) { + needsContinuation = yield* runTurn(session, promotion) + promotion = "steer" + if (!needsContinuation) needsContinuation = yield* SessionInput.hasPending(db, input.sessionID, ["steer"]) + if (!needsContinuation) break + } + if (needsContinuation) + return yield* new StepLimitExceededError({ sessionID: input.sessionID, limit: MAX_STEPS }) + openActivity = yield* SessionInput.hasPending(db, input.sessionID, ["queue"]) + promotion = openActivity ? "queue" : undefined + } + }) + + return Service.of({ + run, + }) + }), +) + +export const defaultLayer = layer diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts new file mode 100644 index 0000000000..fdca0d59e0 --- /dev/null +++ b/packages/core/src/session/runner/model.ts @@ -0,0 +1,141 @@ +export * as SessionRunnerModel from "./model" + +import { type Model } from "@opencode-ai/llm" +import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages" +import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat" +import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses" +import { Auth, type AnyRoute } from "@opencode-ai/llm/route" +import { Context, Effect, Layer, Option, Schema } from "effect" +import { produce } from "immer" +import { Catalog } from "../../catalog" +import { ModelV2 } from "../../model" +import { PluginBoot } from "../../plugin/boot" +import { ProviderV2 } from "../../provider" +import { SessionSchema } from "../schema" + +export class ModelNotSelectedError extends Schema.TaggedErrorClass()( + "SessionRunnerModel.ModelNotSelectedError", + { + sessionID: SessionSchema.ID, + }, +) {} + +export class UnsupportedApiError extends Schema.TaggedErrorClass()( + "SessionRunnerModel.UnsupportedApiError", + { + providerID: ProviderV2.ID, + modelID: ModelV2.ID, + api: Schema.String, + }, +) {} + +export type Error = + | Catalog.ProviderNotFoundError + | Catalog.ModelNotFoundError + | ModelNotSelectedError + | UnsupportedApiError + +export interface Interface { + readonly resolve: (session: SessionSchema.Info) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/SessionRunnerModel") {} + +/** Test or embedding seam for supplying a model resolver directly. */ +export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve })) + +const apiKey = (model: ModelV2.Info, provider?: ProviderV2.Info) => { + const value = model.request.body.apiKey ?? model.api.settings?.apiKey + if (typeof value === "string") return Auth.value(value) + return provider?.enabled !== false && provider?.enabled.via === "env" ? Auth.config(provider.enabled.name) : undefined +} + +const withDefaults = (model: ModelV2.Info, route: AnyRoute) => + route.with({ + provider: model.providerID, + endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url }, + headers: model.request.headers, + http: { + body: Object.fromEntries(Object.entries(model.request.body).filter(([key]) => key !== "apiKey")), + }, + limits: { context: model.limit.context, output: model.limit.output }, + }) + +const withVariant = (model: ModelV2.Info, variantID: ModelV2.VariantID | undefined) => { + const id = variantID === "default" || variantID === undefined ? model.request.variant : variantID + const variant = model.variants.find((item) => item.id === id) + if (!variant) return model + return produce(model, (draft) => { + Object.assign(draft.request.headers, variant.headers) + Object.assign(draft.request.body, variant.body) + }) +} + +const apiName = (model: ModelV2.Info) => + model.api.type === "aisdk" ? `${model.api.type}:${model.api.package}` : model.api.type + +export const fromCatalogModel = ( + model: ModelV2.Info, + provider?: ProviderV2.Info, +): Effect.Effect => { + const key = apiKey(model, provider) + if (model.api.type === "aisdk" && model.api.package === "@ai-sdk/openai") { + return Effect.succeed( + withDefaults(model, OpenAIResponses.route) + .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) + .model({ id: model.api.id }), + ) + } + if (model.api.type === "aisdk" && model.api.package === "@ai-sdk/anthropic") { + return Effect.succeed( + withDefaults(model, AnthropicMessages.route) + .with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) }) + .model({ id: model.api.id }), + ) + } + if (model.api.type === "aisdk" && model.api.package === "@ai-sdk/openai-compatible" && model.api.url) { + return Effect.succeed( + withDefaults(model, OpenAICompatibleChat.route) + .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) + .model({ id: model.api.id }), + ) + } + return Effect.fail( + new UnsupportedApiError({ + providerID: model.providerID, + modelID: model.id, + api: apiName(model), + }), + ) +} + +export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, provider?: ProviderV2.Info) => + fromCatalogModel(withVariant(model, session.model?.variant), provider) + +export const supported = (model: ModelV2.Info) => + model.api.type === "aisdk" && + (model.api.package === "@ai-sdk/openai" || + model.api.package === "@ai-sdk/anthropic" || + (model.api.package === "@ai-sdk/openai-compatible" && model.api.url !== undefined)) + +/** Resolves models from the catalog belonging to the current Location runtime. */ +export const locationLayer = Layer.effect( + Service, + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const boot = yield* PluginBoot.Service + return Service.of({ + resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) { + // Location plugins populate and filter the catalog asynchronously during layer startup. + yield* boot.wait() + const preferred = yield* catalog.model.default() + const selected = session.model + ? yield* catalog.model.get(session.model.providerID, session.model.id) + : (Option.getOrUndefined(preferred.pipe(Option.filter(supported))) ?? + (yield* catalog.model.available()).find(supported)) + if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id }) + return yield* resolve(session, selected, yield* catalog.provider.get(selected.providerID)) + }), + }) + }), +) diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts new file mode 100644 index 0000000000..ad212ac961 --- /dev/null +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -0,0 +1,355 @@ +import { ToolOutput as LLMToolOutput, type LLMEvent, type ProviderMetadata, type ToolOutput as LLMToolOutputType, type ToolResultValue, type Usage } from "@opencode-ai/llm" +import { DateTime, Effect } from "effect" +import { EventV2 } from "../../event" +import { ModelV2 } from "../../model" +import { SessionEvent } from "../event" +import { SessionSchema } from "../schema" + +type Input = { + readonly sessionID: SessionSchema.ID + readonly agent: string + readonly model: ModelV2.Ref +} + +const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? 0) : 0) + +const tokens = (usage: Usage | undefined) => { + const reasoning = safe(usage?.reasoningTokens) + const read = safe(usage?.cacheReadInputTokens) + const write = safe(usage?.cacheWriteInputTokens) + return { + input: safe(usage?.nonCachedInputTokens), + output: safe(usage?.visibleOutputTokens), + reasoning, + cache: { read, write }, + } +} + +const record = (value: unknown): Record => + typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record) : { value } + +const message = (value: unknown) => { + if (typeof value === "string") return value + try { + return JSON.stringify(value) ?? String(value) + } catch { + return String(value) + } +} + +type ToolOutput = + | { readonly structured: Record; readonly content: LLMToolOutputType["content"] } + | { readonly error: { readonly type: "unknown"; readonly message: string } } + +const settledOutput = (value: LLMToolOutputType | undefined, result: ToolResultValue): ToolOutput => { + if (result.type === "error") return { error: { type: "unknown", message: message(result.value) } } + const settled = value ?? LLMToolOutput.fromResultValue(result) + if (!settled) throw new Error(`Unsupported tool result: ${message(result)}`) + return { structured: record(settled.structured), content: settled.content } +} + +/** Persist one provider turn without executing tools or starting a continuation turn. */ +export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) => { + const tools = new Map< + string, + { readonly assistantMessageID: EventV2.ID; readonly name: string; inputEnded: boolean; called: boolean; settled: boolean; providerExecuted: boolean; providerMetadata?: ProviderMetadata } + >() + const timestamp = DateTime.now + let assistantMessageID: EventV2.ID | undefined + let providerFailed = false + + const startAssistant = Effect.fnUntraced(function* () { + if (assistantMessageID !== undefined) return assistantMessageID + assistantMessageID = (yield* events.publish(SessionEvent.Step.Started, { ...input, timestamp: yield* timestamp })).id + return assistantMessageID + }) + const currentAssistantMessageID = () => assistantMessageID === undefined + ? Effect.die("Tool event before assistant step start") + : Effect.succeed(assistantMessageID) + + const fragments = (name: string, ended: (id: string, value: string, providerMetadata?: ProviderMetadata) => Effect.Effect) => { + const chunks = new Map() + const start = (id: string) => + Effect.suspend(() => { + if (chunks.has(id)) return Effect.die(`Duplicate ${name} start: ${id}`) + chunks.set(id, []) + return Effect.void + }) + const append = (id: string, value: string) => + Effect.suspend(() => { + const current = chunks.get(id) + if (!current) return Effect.die(`${name} delta before start: ${id}`) + current.push(value) + return Effect.void + }) + const end = Effect.fnUntraced(function* (id: string, providerMetadata?: ProviderMetadata) { + const current = chunks.get(id) + if (!current) return yield* Effect.die(`${name} end before start: ${id}`) + yield* ended(id, current.join(""), providerMetadata) + chunks.delete(id) + }) + const flush = Effect.fnUntraced(function* () { + for (const id of chunks.keys()) yield* end(id) + }) + return { start, append, end, flush } + } + + const text = fragments("text", (textID, value) => + Effect.gen(function* () { + yield* events.publish(SessionEvent.Text.Ended, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + textID, + text: value, + }) + }), + ) + const reasoning = fragments("reasoning", (reasoningID, value, providerMetadata) => + Effect.gen(function* () { + yield* events.publish(SessionEvent.Reasoning.Ended, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + reasoningID, + text: value, + providerMetadata, + }) + }), + ) + const toolInput = fragments("tool input", (callID, value) => + Effect.gen(function* () { + const tool = tools.get(callID) + if (!tool) return yield* Effect.die(`Tool input end before start: ${callID}`) + yield* events.publish(SessionEvent.Tool.Input.Ended, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + assistantMessageID: tool.assistantMessageID, + callID, + text: value, + }) + tool.inputEnded = true + }), + ) + + const flushFragments = Effect.fnUntraced(function* () { + yield* text.flush() + yield* reasoning.flush() + yield* toolInput.flush() + }) + + const startToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) { + if (tools.has(event.id)) return yield* Effect.die(`Duplicate tool input start: ${event.id}`) + const assistantMessageID = yield* currentAssistantMessageID() + tools.set(event.id, { assistantMessageID, name: event.name, inputEnded: false, called: false, settled: false, providerExecuted: false }) + yield* toolInput.start(event.id) + yield* events.publish(SessionEvent.Tool.Input.Started, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + assistantMessageID, + callID: event.id, + name: event.name, + }) + }) + + const endToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) { + const tool = tools.get(event.id) + if (!tool) return yield* Effect.die(`Tool input end before start: ${event.id}`) + if (tool.name !== event.name) return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`) + if (tool.inputEnded) return yield* Effect.die(`Duplicate tool input end: ${event.id}`) + yield* toolInput.end(event.id) + }) + + const flush = Effect.fn("SessionRunner.flush")(function* () { + yield* flushFragments() + }) + + const failUnsettledTools = Effect.fn("SessionRunner.failUnsettledTools")(function* ( + message: string, + hostedOnly = false, + ) { + for (const [callID, tool] of tools) { + if (tool.settled || (hostedOnly && !tool.providerExecuted)) continue + tool.settled = true + yield* events.publish(SessionEvent.Tool.Failed, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + assistantMessageID: tool.assistantMessageID, + callID, + error: { type: "unknown", message }, + provider: { + executed: tool.providerExecuted, + ...(tool.providerMetadata === undefined ? {} : { metadata: tool.providerMetadata }), + }, + }) + } + }) + + const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* (event: LLMEvent) { + switch (event.type) { + case "step-start": + yield* startAssistant() + return + case "text-start": + yield* text.start(event.id) + yield* events.publish(SessionEvent.Text.Started, { sessionID: input.sessionID, timestamp: yield* timestamp, textID: event.id }) + return + case "text-delta": + yield* text.append(event.id, event.text) + yield* events.publish(SessionEvent.Text.Delta, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + textID: event.id, + delta: event.text, + }) + return + case "text-end": + yield* text.end(event.id) + return + case "reasoning-start": + yield* reasoning.start(event.id) + yield* events.publish(SessionEvent.Reasoning.Started, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + reasoningID: event.id, + providerMetadata: event.providerMetadata, + }) + return + case "reasoning-delta": + yield* reasoning.append(event.id, event.text) + yield* events.publish(SessionEvent.Reasoning.Delta, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + reasoningID: event.id, + delta: event.text, + }) + return + case "reasoning-end": + yield* reasoning.end(event.id, event.providerMetadata) + return + case "tool-input-start": + yield* startToolInput(event) + return + case "tool-input-delta": { + const tool = tools.get(event.id) + if (!tool) return yield* Effect.die(`Tool input delta before start: ${event.id}`) + if (tool.name !== event.name) return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`) + if (tool.inputEnded) return yield* Effect.die(`Tool input delta after end: ${event.id}`) + yield* toolInput.append(event.id, event.text) + yield* events.publish(SessionEvent.Tool.Input.Delta, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + assistantMessageID: tool.assistantMessageID, + callID: event.id, + delta: event.text, + }) + return + } + case "tool-input-end": + yield* endToolInput(event) + return + case "tool-call": { + if (!tools.has(event.id)) yield* startToolInput(event) + const tool = tools.get(event.id)! + if (!tool.inputEnded) yield* endToolInput(event) + if (tool.name !== event.name) return yield* Effect.die(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`) + if (tool.called) return yield* Effect.die(`Duplicate tool call: ${event.id}`) + tool.called = true + tool.providerExecuted = event.providerExecuted === true + tool.providerMetadata = event.providerMetadata + yield* events.publish(SessionEvent.Tool.Called, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + assistantMessageID: tool.assistantMessageID, + callID: event.id, + tool: event.name, + input: record(event.input), + provider: { + executed: tool.providerExecuted, + ...(event.providerMetadata === undefined ? {} : { metadata: event.providerMetadata }), + }, + }) + return + } + case "tool-result": { + const tool = tools.get(event.id) + if (!tool?.called) return yield* Effect.die(`Tool result before call: ${event.id}`) + if (tool.name !== event.name) return yield* Effect.die(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`) + if (tool.settled) { + if (event.result.type === "error") return + return yield* Effect.die(`Duplicate tool result: ${event.id}`) + } + tool.settled = true + const result = settledOutput(event.output, event.result) + const provider = { + executed: event.providerExecuted === true || tool.providerExecuted, + ...(event.providerMetadata === undefined ? {} : { metadata: event.providerMetadata }), + } + if ("error" in result) { + yield* events.publish(SessionEvent.Tool.Failed, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + assistantMessageID: tool.assistantMessageID, + callID: event.id, + error: result.error, + result: event.result, + provider, + }) + return + } + yield* events.publish(SessionEvent.Tool.Success, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + assistantMessageID: tool.assistantMessageID, + callID: event.id, + ...result, + result: event.result, + provider, + }) + return + } + case "tool-error": { + const tool = tools.get(event.id) + if (!tool?.called) return yield* Effect.die(`Tool error before call: ${event.id}`) + if (tool.name !== event.name) return yield* Effect.die(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`) + if (tool.settled) return yield* Effect.die(`Duplicate tool error: ${event.id}`) + tool.settled = true + yield* events.publish(SessionEvent.Tool.Failed, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + assistantMessageID: tool.assistantMessageID, + callID: event.id, + error: { type: "unknown", message: event.message }, + provider: { + executed: tool.providerExecuted, + ...(event.providerMetadata === undefined ? {} : { metadata: event.providerMetadata }), + }, + }) + return + } + case "step-finish": + yield* flush() + yield* events.publish(SessionEvent.Step.Ended, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + assistantMessageID: yield* currentAssistantMessageID(), + finish: event.reason, + cost: 0, + tokens: tokens(event.usage), + }) + return + case "finish": + return + case "provider-error": + providerFailed = true + yield* flush() + yield* events.publish(SessionEvent.Step.Failed, { + sessionID: input.sessionID, + timestamp: yield* timestamp, + assistantMessageID: yield* startAssistant(), + error: { type: "unknown", message: event.message }, + }) + return + } + }) + + return { publish, flush, failUnsettledTools, hasProviderError: () => providerFailed, startAssistant } +} diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts new file mode 100644 index 0000000000..04bf9858bd --- /dev/null +++ b/packages/core/src/session/runner/to-llm-message.ts @@ -0,0 +1,130 @@ +import { Message, ToolCallPart, ToolOutput, ToolResultPart, type ContentPart, type Model, type ProviderMetadata } from "@opencode-ai/llm" +import { SessionMessage } from "../message" +import type { FileAttachment } from "../prompt" + +const media = (file: FileAttachment): ContentPart => ({ + type: "media", + mediaType: file.mime, + data: file.uri, + filename: file.name, + metadata: file.description === undefined ? undefined : { description: file.description }, +}) + +const toolInput = (tool: SessionMessage.AssistantTool) => { + if (tool.state.status !== "pending") return tool.state.input + try { + return JSON.parse(tool.state.input) as unknown + } catch { + return tool.state.input + } +} + +const toolCall = (tool: SessionMessage.AssistantTool, providerMetadata: ProviderMetadata | undefined): ContentPart => + ToolCallPart.make({ + id: tool.id, + name: tool.name, + input: toolInput(tool), + providerExecuted: tool.provider?.executed, + providerMetadata, + }) + +const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: ProviderMetadata | undefined) => { + if (tool.state.status === "completed") { + // TODO: Materialize remote URL and managed file sources before provider-history lowering. + // ToolOutput.toResultValue intentionally rejects unmaterialized sources rather than + // guessing whether a provider can fetch them or leaking host-local resource paths. + const result = + tool.provider?.executed === true && tool.state.result !== undefined + ? tool.state.result + : ToolOutput.toResultValue({ structured: tool.state.structured, content: tool.state.content }) + return ToolResultPart.make({ + id: tool.id, + name: tool.name, + result, + providerExecuted: tool.provider?.executed, + providerMetadata, + }) + } + if (tool.state.status === "error") { + return ToolResultPart.make({ + id: tool.id, + name: tool.name, + result: + tool.provider?.executed === true && tool.state.result !== undefined + ? tool.state.result + : { error: tool.state.error, content: tool.state.content, structured: tool.state.structured }, + resultType: "error", + providerExecuted: tool.provider?.executed, + providerMetadata, + }) + } +} + +const assistant = (message: SessionMessage.Assistant, model: Model) => { + const sameModel = String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id) + const content = message.content.flatMap((item): ContentPart[] => { + if (item.type === "text") return [{ type: "text", text: item.text }] + if (item.type === "reasoning") + return sameModel + ? [{ type: "reasoning", text: item.text, providerMetadata: item.providerMetadata }] + : item.text.length > 0 + ? [{ type: "text", text: item.text }] + : [] + const call = toolCall(item, sameModel ? item.provider?.metadata : undefined) + const result = toolResult(item, sameModel ? item.provider?.resultMetadata ?? item.provider?.metadata : undefined) + return item.provider?.executed === true && result ? [call, result] : [call] + }) + const results = message.content + .filter((item): item is SessionMessage.AssistantTool => item.type === "tool" && item.provider?.executed !== true) + .map((item) => toolResult(item, sameModel ? item.provider?.resultMetadata ?? item.provider?.metadata : undefined)) + .filter((message) => message !== undefined) + .map(Message.tool) + return [Message.make({ id: message.id, role: "assistant", content, metadata: message.metadata }), ...results] +} + +function toLLMMessage(message: SessionMessage.Message, model: Model): Message[] { + switch (message.type) { + case "agent-switched": + case "model-switched": + return [] + case "user": + return [ + Message.make({ + id: message.id, + role: "user", + content: [{ type: "text", text: message.text }, ...(message.files ?? []).map(media)], + metadata: { + ...message.metadata, + ...(message.agents?.length ? { agents: message.agents } : {}), + ...(message.references?.length ? { references: message.references } : {}), + }, + }), + ] + case "synthetic": + return [Message.make({ id: message.id, role: "user", content: message.text, metadata: message.metadata })] + case "shell": + return [ + Message.make({ + id: message.id, + role: "user", + content: `Shell command: ${message.command}\n\n${message.output}`, + metadata: message.metadata, + }), + ] + case "assistant": + return assistant(message, model) + case "compaction": + return [ + Message.make({ + id: message.id, + role: "user", + content: `Summary of earlier conversation:\n${message.summary}`, + metadata: message.metadata, + }), + ] + } +} + +/** Translate projected V2 Session history into canonical @opencode-ai/llm context. */ +export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: Model) => + messages.flatMap((message) => toLLMMessage(message, model)) diff --git a/packages/core/src/session/schema.ts b/packages/core/src/session/schema.ts index c66d09b8a2..940678b1a1 100644 --- a/packages/core/src/session/schema.ts +++ b/packages/core/src/session/schema.ts @@ -4,23 +4,21 @@ import { Schema } from "effect" import { Location } from "../location" import { ModelV2 } from "../model" import { ProjectV2 } from "../project" -import { RelativePath, optionalOmitUndefined, withStatics } from "../schema" +import { externalID, type ExternalID, RelativePath, optionalOmitUndefined, withStatics } from "../schema" import { Identifier } from "../util/identifier" import { V2Schema } from "../v2-schema" import { AgentV2 } from "../agent" -export const Delivery = Schema.Literals(["immediate", "deferred"]).annotate({ - identifier: "Session.Delivery", -}) -export type Delivery = Schema.Schema.Type - -export const DefaultDelivery = "immediate" satisfies Delivery - export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe( Schema.brand("SessionID"), - withStatics((schema) => ({ - descending: (id?: string) => schema.make(id ?? "ses_" + Identifier.descending()), - })), + withStatics((schema) => { + const create = () => schema.make("ses_" + Identifier.descending()) + return { + create, + descending: (id?: string) => id === undefined ? create() : schema.make(id), + fromExternal: (input: ExternalID) => schema.make(externalID("ses", input)), + } + }), ) export type ID = typeof ID.Type diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index 2d3fe120c1..8ecbbea240 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -2,6 +2,8 @@ import { sqliteTable, text, integer, index, primaryKey, real } from "drizzle-orm import * as DatabasePath from "../database/path" import { ProjectTable } from "../project/sql" import type { SessionMessage } from "./message" +import type { Prompt } from "./prompt" +import type { SessionInput } from "./input" import type { Snapshot } from "../snapshot" import { PermissionV1 } from "../v1/permission" import { ProjectV2 } from "../project" @@ -120,12 +122,40 @@ export const SessionMessageTable = sqliteTable( .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), type: text().$type().notNull(), + seq: integer().notNull(), ...Timestamps, data: text({ mode: "json" }).notNull().$type(), }, (table) => [ - index("session_message_session_idx").on(table.session_id), - index("session_message_session_type_idx").on(table.session_id, table.type), + index("session_message_session_seq_idx").on(table.session_id, table.seq), + index("session_message_session_type_seq_idx").on(table.session_id, table.type, table.seq), + index("session_message_session_time_created_id_idx").on(table.session_id, table.time_created, table.id), index("session_message_time_created_idx").on(table.time_created), ], ) + +export const SessionInputTable = sqliteTable( + "session_input", + { + seq: integer().primaryKey({ autoIncrement: true }), + id: text().$type().notNull().unique(), + session_id: text() + .$type() + .notNull() + .references(() => SessionTable.id, { onDelete: "cascade" }), + prompt: text({ mode: "json" }).notNull().$type(), + delivery: text().$type().notNull(), + promoted_seq: integer(), + time_created: integer() + .notNull() + .$default(() => Date.now()), + }, + (table) => [ + index("session_input_session_pending_delivery_seq_idx").on( + table.session_id, + table.promoted_seq, + table.delivery, + table.seq, + ), + ], +) diff --git a/packages/core/src/session/store.ts b/packages/core/src/session/store.ts new file mode 100644 index 0000000000..8660202da9 --- /dev/null +++ b/packages/core/src/session/store.ts @@ -0,0 +1,53 @@ +export * as SessionStore from "./store" + +import { eq } from "drizzle-orm" +import { Context, Effect, Layer, Schema } from "effect" +import { Database } from "../database/database" +import { SessionContext } from "./context" +import { MessageDecodeError } from "./error" +import { SessionMessage } from "./message" +import { SessionSchema } from "./schema" +import { SessionMessageTable, SessionTable } from "./sql" +import { fromRow } from "./info" + +export interface Interface { + readonly get: (sessionID: SessionSchema.ID) => Effect.Effect + readonly context: (sessionID: SessionSchema.ID) => Effect.Effect + readonly message: ( + messageID: SessionMessage.ID, + ) => Effect.Effect<{ readonly sessionID: SessionSchema.ID; readonly message: SessionMessage.Message } | undefined> +} + +export class Service extends Context.Service()("@opencode/v2/SessionStore") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const { db } = yield* Database.Service + const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) + + return Service.of({ + get: Effect.fn("SessionStore.get")(function* (sessionID) { + const row = yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie) + return row ? fromRow(row) : undefined + }), + context: Effect.fn("SessionStore.context")(function* (sessionID) { + return yield* SessionContext.load(db, sessionID) + }), + message: Effect.fn("SessionStore.message")(function* (messageID) { + const row = yield* db + .select() + .from(SessionMessageTable) + .where(eq(SessionMessageTable.id, messageID)) + .get() + .pipe(Effect.orDie) + return row + ? { + sessionID: SessionSchema.ID.make(row.session_id), + message: yield* decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe(Effect.orDie), + } + : undefined + }), + }) + }), +) diff --git a/packages/core/src/session/todo.ts b/packages/core/src/session/todo.ts new file mode 100644 index 0000000000..7b3c3be3f6 --- /dev/null +++ b/packages/core/src/session/todo.ts @@ -0,0 +1,91 @@ +export * as SessionTodo from "./todo" + +import { asc, eq } from "drizzle-orm" +import { Context, Effect, Layer, Schema } from "effect" +import { Database } from "../database/database" +import { EventV2 } from "../event" +import { SessionSchema } from "./schema" +import { TodoTable } from "./sql" + +export const Info = Schema.Struct({ + content: Schema.String.annotate({ description: "Brief description of the task" }), + status: Schema.String.annotate({ + description: "Current status of the task: pending, in_progress, completed, cancelled", + }), + priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }), +}).annotate({ identifier: "SessionTodo.Info" }) +export type Info = typeof Info.Type + +export const Event = { + Updated: EventV2.define({ + type: "todo.updated", + schema: { + sessionID: SessionSchema.ID, + todos: Schema.Array(Info), + }, + }), +} + +export interface Interface { + readonly update: (input: { + readonly sessionID: SessionSchema.ID + readonly todos: ReadonlyArray + }) => Effect.Effect + readonly get: (sessionID: SessionSchema.ID) => Effect.Effect> +} + +export class Service extends Context.Service()("@opencode/v2/SessionTodo") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const { db } = yield* Database.Service + const events = yield* EventV2.Service + + const update = Effect.fn("SessionTodo.update")(function* (input: { + readonly sessionID: SessionSchema.ID + readonly todos: ReadonlyArray + }) { + yield* db + .transaction((tx) => + Effect.gen(function* () { + yield* tx.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run() + if (input.todos.length === 0) return + yield* tx + .insert(TodoTable) + .values( + input.todos.map((todo, position) => ({ + session_id: input.sessionID, + content: todo.content, + status: todo.status, + priority: todo.priority, + position, + })), + ) + .run() + }), + ) + .pipe(Effect.orDie) + yield* events.publish(Event.Updated, input) + }) + + const get = Effect.fn("SessionTodo.get")(function* (sessionID: SessionSchema.ID) { + const rows = yield* db + .select() + .from(TodoTable) + .where(eq(TodoTable.session_id, sessionID)) + .orderBy(asc(TodoTable.position)) + .all() + .pipe(Effect.orDie) + return rows.map((row) => ({ + content: row.content, + status: row.status, + priority: row.priority, + })) + }) + + return Service.of({ update, get }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(Database.defaultLayer)) diff --git a/packages/core/src/skill.ts b/packages/core/src/skill.ts index d1a8c7d6b5..bd384f2880 100644 --- a/packages/core/src/skill.ts +++ b/packages/core/src/skill.ts @@ -122,6 +122,8 @@ export const layer = Layer.effect( return skills }) + // QUESTION(Dax): Should local skill sources invalidate on filesystem watch + // events, following the reload policy chosen for other context sources? const cache = new Map() const list = Effect.fn("SkillV2.list")(function* () { const skills = new Map() diff --git a/packages/core/src/skill/discovery.ts b/packages/core/src/skill/discovery.ts index fa8c94548d..b8d51b9904 100644 --- a/packages/core/src/skill/discovery.ts +++ b/packages/core/src/skill/discovery.ts @@ -11,6 +11,46 @@ import * as Log from "../util/log" const skillConcurrency = 4 const fileConcurrency = 8 +function isSafeSegment(value: string) { + return ( + value.length > 0 && + value !== "." && + value !== ".." && + !value.includes("/") && + !value.includes("\\") && + !value.includes("\0") + ) +} + +function isSafeRelativePath(value: string) { + const segments = value.split("/") + return ( + value.length > 0 && + !value.includes("\\") && + !value.includes("\0") && + !value.includes("?") && + !value.includes("#") && + !URL.canParse(value) && + !path.posix.isAbsolute(value) && + !path.win32.isAbsolute(value) && + segments.every((segment) => { + try { + const decoded = decodeURIComponent(segment) + return ( + decoded.length > 0 && + decoded !== "." && + decoded !== ".." && + !decoded.includes("/") && + !decoded.includes("\\") && + !decoded.includes("\0") + ) + } catch { + return false + } + }) + ) +} + class IndexSkill extends Schema.Class("SkillDiscovery.IndexSkill")({ name: Schema.String, files: Schema.Array(Schema.String), @@ -54,7 +94,8 @@ export const layer = Layer.effect( return Service.of({ pull: Effect.fn("SkillDiscovery.pull")(function* (url) { const base = url.endsWith("/") ? url : `${url}/` - const index = new URL("index.json", base).href + const source = new URL(base) + const index = new URL("index.json", source).href const data = yield* HttpClientRequest.get(index).pipe( HttpClientRequest.acceptJson, http.execute, @@ -66,18 +107,53 @@ export const layer = Layer.effect( ) if (!data) return [] + const sourceRoot = path.resolve(global.cache, "skills", Bun.hash(base).toString(16)) return yield* Effect.forEach( - data.skills.filter((skill) => { - if (skill.files.includes("SKILL.md") || skill.files.includes(`${skill.name}.md`)) return true - log.warn("skill entry missing Markdown definition", { url: index, skill: skill.name }) - return false + data.skills.flatMap((skill) => { + if (!isSafeSegment(skill.name)) { + log.warn("skill entry has unsafe name", { url: index, skill: skill.name }) + return [] + } + if (!skill.files.includes("SKILL.md") && !skill.files.includes(`${skill.name}.md`)) { + log.warn("skill entry missing Markdown definition", { url: index, skill: skill.name }) + return [] + } + + const root = path.resolve(sourceRoot, skill.name) + if (!FSUtil.contains(sourceRoot, root) || root === sourceRoot) { + log.warn("skill entry escapes cache root", { url: index, skill: skill.name }) + return [] + } + + const skillUrl = new URL(`${encodeURIComponent(skill.name)}/`, source) + const files = skill.files.map((file) => { + if (!isSafeRelativePath(file)) return undefined + let resource: URL + try { + resource = new URL(file, skillUrl) + } catch { + return undefined + } + if (resource.origin !== source.origin) return undefined + + const destination = path.resolve(root, file) + if (!FSUtil.contains(root, destination) || destination === root) return undefined + return { + url: resource.href, + destination, + } + }) + if (files.some((file) => file === undefined)) { + log.warn("skill entry has unsafe file", { url: index, skill: skill.name }) + return [] + } + return [{ skill, root, files: files as { url: string; destination: string }[] }] }), - (skill) => + ({ skill, root, files }) => Effect.gen(function* () { - const root = path.join(global.cache, "skills", Bun.hash(base).toString(16), skill.name) yield* Effect.forEach( - skill.files, - (file) => download(new URL(file, `${base}${skill.name}/`).href, path.join(root, file)), + files, + (file) => download(file.url, file.destination), { concurrency: fileConcurrency, discard: true }, ) return (yield* fs.exists(path.join(root, "SKILL.md")).pipe(Effect.orDie)) || diff --git a/packages/core/src/state.ts b/packages/core/src/state.ts index aa3ab0e240..fab9e9780b 100644 --- a/packages/core/src/state.ts +++ b/packages/core/src/state.ts @@ -3,19 +3,50 @@ export * as State from "./state" import { Effect, Scope, Semaphore } from "effect" import type { Draft, Objectish } from "immer" +/** + * A replayable contribution applied to an editor during rebuild. + * + * Transforms are intentionally synchronous and mutation-shaped: domain editors + * hide the draft representation while preserving concise plugin/config code. + */ export type Transform = (editor: Editor) => void export type MakeEditor = (draft: Draft) => Editor export interface Options { + /** Creates the base value for initial state and every scoped-transform rebuild. */ readonly initial: () => State + /** Wraps the mutable draft in a domain-specific editor. */ readonly editor: MakeEditor - /** Completes every committed edit; reason identifies exceptional update origins. */ + /** + * Completes every committed edit. + * + * For rebuilds, this runs after all active transforms have been replayed and + * before the rebuilt state becomes visible. For direct updates, this runs + * after the current state has already been edited. The optional reason is + * caller-defined metadata for exceptional update origins. + */ readonly finalize?: (editor: Editor, reason?: string) => Effect.Effect } export interface Interface { readonly get: () => State + /** + * Registers a scoped transform slot and returns the slot updater. + * + * Acquiring the slot has no visible effect until the returned updater is + * called. Each updater call replaces that slot's transform, then rebuilds the + * materialized state from `initial()` by replaying all active transforms in + * registration order. Closing the owning Scope removes the slot and rebuilds. + */ readonly transform: () => Effect.Effect<(transform: Transform) => Effect.Effect, never, Scope.Scope> + /** + * Mutates the current materialized state directly. + * + * This is not replayable contribution state: a later rebuild starts again + * from `initial()` plus active transforms, so direct edits must be reserved + * for current-state adjustments that are intentionally outside the transform + * fold. + */ readonly update: (update: (editor: Editor) => Effect.Effect, reason?: string) => Effect.Effect } diff --git a/packages/core/src/system-context.ts b/packages/core/src/system-context.ts new file mode 100644 index 0000000000..c40f995ed2 --- /dev/null +++ b/packages/core/src/system-context.ts @@ -0,0 +1,141 @@ +export * as SystemContext from "./system-context" + +import { Effect, Schema } from "effect" +import { Hash } from "./util/hash" + +export const Key = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._/-]*$/)).pipe( + Schema.brand("SystemContext.Key"), +) +export type Key = typeof Key.Type + +export const unavailable = Symbol.for("@opencode/SystemContext.Unavailable") +export type Unavailable = typeof unavailable + +export interface Value { + /** Full component text rendered into a new epoch baseline. */ + readonly baseline: string + /** Absolute current-state text emitted when this component changes. */ + readonly update: string +} + +export interface Component { + readonly key: Key + readonly load: Effect.Effect +} + +export interface SystemContext { + readonly components: ReadonlyArray> +} + +export interface AvailableEntry extends Value { + readonly _tag: "Available" + readonly key: Key + readonly hash: string +} + +export interface UnavailableEntry { + readonly _tag: "Unavailable" + readonly key: Key +} + +export type Entry = AvailableEntry | UnavailableEntry + +export interface Snapshot { + readonly entries: ReadonlyArray +} + +export interface Part { + readonly key: Key + readonly text: string +} + +export type Checkpoint = Readonly> + +export interface Initialized { + readonly baseline: ReadonlyArray + readonly checkpoint: Checkpoint +} + +export interface Refreshed { + readonly changes: ReadonlyArray + readonly checkpoint: Checkpoint +} + +export class DuplicateKeyError extends Schema.TaggedErrorClass()("SystemContext.DuplicateKeyError", { + key: Key, +}) { + override get message() { + return `Duplicate system context key: ${this.key}` + } +} + +export const value = (component: Component): Component => component + +export function struct(components: Readonly>>): SystemContext { + const values = Object.values(components) + assertUniqueKeys(values) + return { components: values } +} + +export const load = (context: SystemContext) => + Effect.sync(() => assertUniqueKeys(context.components)).pipe( + Effect.andThen( + Effect.forEach(context.components, (component) => + component.load.pipe( + Effect.map( + (result): Entry => + result === unavailable + ? { _tag: "Unavailable", key: component.key } + : { _tag: "Available", key: component.key, ...result, hash: Hash.sha256(result.update) }, + ), + ), + ), + ), + Effect.map((entries): Snapshot => ({ entries })), + ) + +export function initialize(snapshot: Snapshot): Initialized { + return { + baseline: snapshot.entries.flatMap((entry) => + entry._tag === "Available" ? [{ key: entry.key, text: entry.baseline }] : [], + ), + checkpoint: nextCheckpoint(snapshot, {}), + } +} + +export function refresh(snapshot: Snapshot, previous: Checkpoint): Refreshed { + return { + changes: snapshot.entries.flatMap((entry) => + entry._tag === "Available" && getCheckpoint(previous, entry.key) !== entry.hash + ? [{ key: entry.key, text: entry.update }] + : [], + ), + checkpoint: nextCheckpoint(snapshot, previous), + } +} + +export function render(parts: ReadonlyArray) { + return parts.map((part) => part.text).join("\n\n") +} + +function nextCheckpoint(snapshot: Snapshot, previous: Checkpoint) { + return Object.fromEntries( + snapshot.entries.flatMap((entry) => { + if (entry._tag === "Available") return [[entry.key, entry.hash]] + const hash = getCheckpoint(previous, entry.key) + return hash === undefined ? [] : [[entry.key, hash]] + }), + ) +} + +function getCheckpoint(checkpoint: Checkpoint, key: Key) { + return Object.hasOwn(checkpoint, key) ? checkpoint[key] : undefined +} + +function assertUniqueKeys(components: ReadonlyArray>) { + const keys = new Set() + for (const component of components) { + if (keys.has(component.key)) throw new DuplicateKeyError({ key: component.key }) + keys.add(component.key) + } +} diff --git a/packages/core/src/tool-output-store.ts b/packages/core/src/tool-output-store.ts new file mode 100644 index 0000000000..90d1ea9427 --- /dev/null +++ b/packages/core/src/tool-output-store.ts @@ -0,0 +1,335 @@ +export * as ToolOutputStore from "./tool-output-store" + +import path from "path" +import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" +import { Config } from "./config" +import { FSUtil } from "./fs-util" +import { Global } from "./global" +import { NonNegativeInt, PositiveInt } from "./schema" +import { SessionSchema } from "./session/schema" +import { Identifier } from "./util/identifier" + +export const MAX_LINES = 2_000 +export const MAX_BYTES = 50 * 1024 +export const MAX_READ_BYTES = 50 * 1024 +export const RETENTION = Duration.days(7) + +const URI_PREFIX = "tool-output://" +const MANAGED_DIRECTORY = path.join("tool-output", "managed") +const ID_PATTERN = /^[0-9a-f]{12}[0-9A-Za-z]{14}$/ + +export class Resource extends Schema.Class("ToolOutputStore.Resource")({ + uri: Schema.String, + mime: Schema.String, + name: Schema.String.pipe(Schema.optional), + size: NonNegativeInt, +}) {} + +export class Page extends Schema.Class("ToolOutputStore.Page")({ + resource: Resource, + content: Schema.String, + offset: NonNegativeInt, + truncated: Schema.Boolean, + next: NonNegativeInt.pipe(Schema.optional), +}) {} + +export class AccessDeniedError extends Schema.TaggedErrorClass()("ToolOutputStore.AccessDeniedError", { + uri: Schema.String, + sessionID: SessionSchema.ID, +}) {} + +export class InvalidResourceError extends Schema.TaggedErrorClass()("ToolOutputStore.InvalidResourceError", { + uri: Schema.String, +}) {} + +export class ResourceNotFoundError extends Schema.TaggedErrorClass()( + "ToolOutputStore.ResourceNotFoundError", + { uri: Schema.String }, +) {} + +export interface WriteInput { + readonly sessionID: SessionSchema.ID + readonly toolCallID: string + readonly content: string + readonly mime?: string + readonly name?: string +} + +export interface TruncateInput extends WriteInput { + readonly maxLines?: number + readonly maxBytes?: number +} + +export interface ReadInput { + readonly sessionID: SessionSchema.ID + readonly uri: string + /** Zero-based byte offset. Returned `next` values preserve UTF-8 boundaries. */ + readonly offset?: number + readonly limit?: number +} + +export type TruncateResult = + | { readonly content: string; readonly truncated: false } + | { readonly content: string; readonly truncated: true; readonly resource: Resource } + +interface Record { + readonly version: 1 + readonly id: string + readonly uri: string + readonly sessionID: string + readonly toolCallID: string + readonly mime: string + readonly name?: string + readonly size: number + readonly created: number +} + +export interface Interface { + readonly limits: () => Effect.Effect<{ readonly maxLines: number; readonly maxBytes: number }> + readonly write: (input: WriteInput) => Effect.Effect + readonly truncate: (input: TruncateInput) => Effect.Effect + readonly read: (input: ReadInput) => Effect.Effect + readonly cleanup: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/ToolOutputStore") {} + +const uri = (id: string) => URI_PREFIX + id + +const idFromUri = (input: string) => { + if (!input.startsWith(URI_PREFIX)) return + const id = input.slice(URI_PREFIX.length) + if (!ID_PATTERN.test(id)) return + return id +} + +const validRecord = (input: unknown, id: string): input is Record => { + if (!input || typeof input !== "object") return false + const record = input as Partial + return ( + record.version === 1 && + record.id === id && + record.uri === uri(id) && + typeof record.sessionID === "string" && + typeof record.toolCallID === "string" && + typeof record.mime === "string" && + (record.name === undefined || typeof record.name === "string") && + typeof record.size === "number" && + Number.isSafeInteger(record.size) && + record.size >= 0 && + typeof record.created === "number" && + Number.isFinite(record.created) + ) +} + +const takePrefix = (input: string, maximumBytes: number) => { + let bytes = 0 + let content = "" + for (const char of input) { + const size = Buffer.byteLength(char, "utf-8") + if (bytes + size > maximumBytes) break + content += char + bytes += size + } + return content +} + +const takeSuffix = (input: string, maximumBytes: number) => { + let bytes = 0 + const content: string[] = [] + for (const char of Array.from(input).toReversed()) { + const size = Buffer.byteLength(char, "utf-8") + if (bytes + size > maximumBytes) break + content.unshift(char) + bytes += size + } + return content.join("") +} + +const preview = (text: string, maxLines: number, maxBytes: number) => { + const lines = text.split("\n") + const headLines = Math.ceil(maxLines / 2) + const tailLines = Math.floor(maxLines / 2) + const sampled = + lines.length <= maxLines + ? text + : [lines.slice(0, headLines).join("\n"), ...(tailLines > 0 ? [lines.slice(lines.length - tailLines).join("\n")] : [])].join("\n") + if (Buffer.byteLength(sampled, "utf-8") <= maxBytes) { + return lines.length <= maxLines + ? { head: sampled, tail: "" } + : { head: lines.slice(0, headLines).join("\n"), tail: tailLines > 0 ? lines.slice(lines.length - tailLines).join("\n") : "" } + } + const headBytes = Math.ceil(maxBytes / 2) + const tailBytes = Math.floor(maxBytes / 2) + return { head: takePrefix(sampled, headBytes), tail: takeSuffix(sampled, tailBytes) } +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const global = yield* Global.Service + const config = yield* Effect.serviceOption(Config.Service) + const directory = path.join(global.data, MANAGED_DIRECTORY) + const metadataPath = (id: string) => path.join(directory, `${id}.json`) + const contentPath = (id: string) => path.join(directory, `${id}.txt`) + + const load = Effect.fn("ToolOutputStore.load")(function* (resourceUri: string) { + const id = idFromUri(resourceUri) + if (!id) return yield* Effect.fail(new InvalidResourceError({ uri: resourceUri })) + const text = yield* fs.readFileStringSafe(metadataPath(id)).pipe(Effect.orDie) + if (!text) return yield* Effect.fail(new ResourceNotFoundError({ uri: resourceUri })) + const record = yield* Effect.sync(() => JSON.parse(text)).pipe(Effect.catch(() => Effect.void)) + if (!validRecord(record, id)) return yield* Effect.fail(new ResourceNotFoundError({ uri: resourceUri })) + const info = yield* fs.stat(contentPath(id)).pipe(Effect.catch(() => Effect.void)) + if (!info || info.type !== "File" || Number(info.size) !== record.size) + return yield* Effect.fail(new ResourceNotFoundError({ uri: resourceUri })) + return record + }) + + const limits = Effect.fn("ToolOutputStore.limits")(function* () { + if (Option.isNone(config)) return { maxLines: MAX_LINES, maxBytes: MAX_BYTES } + const entries = yield* config.value.entries().pipe(Effect.catch(() => Effect.succeed([] as Config.Entry[]))) + const configured = Object.assign( + {}, + ...entries.flatMap((entry) => entry.type === "document" ? [entry.info.tool_output ?? {}] : []), + ) + return { maxLines: configured.max_lines ?? MAX_LINES, maxBytes: configured.max_bytes ?? MAX_BYTES } + }) + + const write = Effect.fn("ToolOutputStore.write")(function* (input: WriteInput) { + const id = Identifier.ascending() + const resourceUri = uri(id) + const size = Buffer.byteLength(input.content, "utf-8") + const record: Record = { + version: 1, + id, + uri: resourceUri, + sessionID: input.sessionID, + toolCallID: input.toolCallID, + mime: input.mime ?? "text/plain", + ...(input.name === undefined ? {} : { name: input.name }), + size, + created: Date.now(), + } + yield* fs.ensureDir(directory).pipe(Effect.orDie) + yield* fs.writeFileString(contentPath(id), input.content, { flag: "wx" }).pipe(Effect.orDie) + yield* fs.writeFileString(metadataPath(id), JSON.stringify(record), { flag: "wx" }).pipe( + Effect.onError(() => fs.remove(contentPath(id)).pipe(Effect.catch(() => Effect.void))), + Effect.orDie, + ) + return new Resource({ uri: resourceUri, mime: record.mime, ...(record.name === undefined ? {} : { name: record.name }), size }) + }) + + const truncate = Effect.fn("ToolOutputStore.truncate")(function* (input: TruncateInput) { + const configured = yield* limits() + const maxLines = input.maxLines ?? configured.maxLines + const maxBytes = input.maxBytes ?? configured.maxBytes + if (input.content.split("\n").length <= maxLines && Buffer.byteLength(input.content, "utf-8") <= maxBytes) { + return { content: input.content, truncated: false } as const + } + const resource = yield* write(input) + const bounded = preview(input.content, maxLines, maxBytes) + const marker = `... output truncated; full content available as ${resource.uri} ...` + return { + content: bounded.tail ? `${bounded.head}\n\n${marker}\n\n${bounded.tail}` : `${bounded.head}\n\n${marker}`, + truncated: true, + resource, + } as const + }) + + const read = Effect.fn("ToolOutputStore.read")(function* (input: ReadInput) { + const record = yield* load(input.uri) + if (record.sessionID !== input.sessionID) { + return yield* Effect.fail(new AccessDeniedError({ uri: input.uri, sessionID: input.sessionID })) + } + const offset = Math.max(0, Math.min(input.offset ?? 0, record.size)) + const limit = Math.max(1, Math.min(input.limit ?? MAX_READ_BYTES, MAX_READ_BYTES)) + const bytes = yield* Effect.scoped( + Effect.gen(function* () { + const file = yield* fs.open(contentPath(record.id), { flag: "r" }).pipe(Effect.orDie) + yield* file.seek(offset, "start") + const chunk = yield* file.readAlloc(Math.min(limit + 3, record.size - offset)).pipe(Effect.orDie) + return Option.getOrElse(chunk, () => new Uint8Array()) + }), + ) + let start = 0 + while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start++ + let end = Math.min(start + limit, bytes.length) + while (end > start && end < bytes.length && (bytes[end] & 0xc0) === 0x80) end-- + if (end === start && end < bytes.length) { + end = Math.min(start + limit, bytes.length) + while (end < bytes.length && (bytes[end] & 0xc0) === 0x80) end++ + } + const absoluteStart = offset + start + const absoluteEnd = offset + end + const truncated = absoluteEnd < record.size + return new Page({ + resource: new Resource({ + uri: record.uri, + mime: record.mime, + ...(record.name === undefined ? {} : { name: record.name }), + size: record.size, + }), + content: Buffer.from(bytes.subarray(start, end)).toString("utf-8"), + offset: absoluteStart, + truncated, + ...(truncated ? { next: absoluteEnd } : {}), + }) + }) + + const cleanup = Effect.fn("ToolOutputStore.cleanup")(function* () { + const entries = yield* fs.readDirectory(directory).pipe(Effect.catch(() => Effect.succeed([]))) + const cutoff = Date.now() - Duration.toMillis(RETENTION) + const ids = new Set(entries.flatMap((entry) => { + const match = entry.match(/^([0-9a-f]{12}[0-9A-Za-z]{14})\.(?:json|txt)$/) + return match ? [match[1]] : [] + })) + const removeIfPresent = (target: string) => + fs.existsSafe(target).pipe(Effect.flatMap((exists) => exists ? fs.remove(target) : Effect.void)) + const removePair = (id: string) => + Effect.gen(function* () { + yield* removeIfPresent(contentPath(id)) + yield* removeIfPresent(metadataPath(id)) + }).pipe(Effect.catch(() => Effect.void)) + for (const id of ids) { + const text = yield* fs.readFileStringSafe(metadataPath(id)).pipe(Effect.catch(() => Effect.succeed(undefined))) + const contentExists = yield* fs.existsSafe(contentPath(id)) + if (!text) { + if (!contentExists) continue + const info = yield* fs.stat(contentPath(id)).pipe(Effect.catch(() => Effect.void)) + const modified = info ? info.mtime.pipe(Option.map((date) => date.getTime()), Option.getOrElse(() => 0)) : 0 + if (modified < cutoff) yield* removePair(id) + continue + } + const record = yield* Effect.try({ try: () => JSON.parse(text), catch: () => new globalThis.Error("Invalid metadata") }).pipe( + Effect.catch(() => Effect.succeed(undefined)), + ) + const info = contentExists ? yield* fs.stat(contentPath(id)).pipe(Effect.catch(() => Effect.void)) : undefined + if ( + !contentExists || + !validRecord(record, id) || + !info || + info.type !== "File" || + Number(info.size) !== record.size || + record.created < cutoff + ) + yield* removePair(id) + } + }) + + return Service.of({ limits, write, truncate, read, cleanup }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.defaultLayer)) + +/** Runs retention scanning once globally rather than once per active Location. */ +export const cleanupLayer = Layer.effectDiscard( + Effect.gen(function* () { + const store = yield* Service + yield* store.cleanup().pipe(Effect.repeat(Schedule.spaced(Duration.hours(1))), Effect.forkScoped) + }), +) + +export const defaultCleanupLayer = Layer.merge(defaultLayer, cleanupLayer.pipe(Layer.provide(defaultLayer))) diff --git a/packages/core/src/tool-output.ts b/packages/core/src/tool-output.ts index dee2bb11ed..055d7c248e 100644 --- a/packages/core/src/tool-output.ts +++ b/packages/core/src/tool-output.ts @@ -1,18 +1,11 @@ export * as ToolOutput from "./tool-output" +export { + ToolContent as Content, + ToolFileContent as FileContent, + ToolTextContent as TextContent, + toolFile as file, + toolText as text, +} from "@opencode-ai/llm" import { Schema } from "effect" -export class TextContent extends Schema.Class("Tool.TextContent")({ - type: Schema.Literal("text"), - text: Schema.String, -}) {} - -export class FileContent extends Schema.Class("Tool.FileContent")({ - type: Schema.Literal("file"), - uri: Schema.String, - mime: Schema.String, - name: Schema.String.pipe(Schema.optional), -}) {} - -export const Content = Schema.Union([TextContent, FileContent]).pipe(Schema.toTaggedUnion("type")) - export const Structured = Schema.Record(Schema.String, Schema.Any) diff --git a/packages/core/src/tool-registry.ts b/packages/core/src/tool-registry.ts new file mode 100644 index 0000000000..daaf2c3bdc --- /dev/null +++ b/packages/core/src/tool-registry.ts @@ -0,0 +1,148 @@ +export * as ToolRegistry from "./tool-registry" + +import { Tool, ToolFailure, ToolOutput, ToolResultValue as ToolResult, type Tool as TypedTool, type ToolCall, type ToolResultValue, type ToolSchema, type ToolSettlement } from "@opencode-ai/llm" +import { Context, Effect, Layer, Schema, Scope } from "effect" +import { castDraft, enableMapSet } from "immer" +import { PermissionV2 } from "./permission" +import { State } from "./state" +import { SessionSchema } from "./session/schema" +import type { SessionV2 } from "./session" + +export type ExecuteInput = { + readonly sessionID: SessionSchema.ID + readonly call: ToolCall +} + +/** + * Narrow cross-cutting context for one registry invocation. Leaf tools retain + * ownership of sequence-sensitive policy decisions; the registry only binds + * identity and shared helper behavior consistently. + * + * TODO: Add `source` when the runner can pass the durable owning assistant + * message ID alongside the call ID. Do not infer it from the tool call alone. + * TODO: Add cancellation and progress only when the runner exposes a real + * signal and durable/live progress sink. + */ +export type Invocation = ExecuteInput & { + readonly source?: PermissionV2.Source + readonly assertPermission: ( + input: Omit, + ) => Effect.Effect +} + +/** Kept as the leaf entry input name for backwards-compatible execute usage. */ +export type AuthorizeInput = Invocation & { + readonly parameters: Parameters +} + +export type Entry = ToolSchema, Success extends ToolSchema = ToolSchema> = { + readonly tool: TypedTool + readonly authorize?: (input: AuthorizeInput>) => Effect.Effect + readonly execute?: (input: AuthorizeInput>) => Effect.Effect, ToolFailure> +} + +type Data = { + readonly entries: Map +} + +export type Editor = { + readonly list: () => ReadonlyArray + readonly get: (name: string) => Entry | undefined + readonly set: , Success extends ToolSchema>(name: string, entry: Entry) => void + readonly remove: (name: string) => void +} + +export interface Interface { + readonly transform: State.Interface["transform"] + readonly contribute: (update: State.Transform) => Effect.Effect + readonly definitions: () => Effect.Effect[number]>> + readonly execute: (input: ExecuteInput) => Effect.Effect + readonly settle: (input: ExecuteInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/ToolRegistry") {} + +enableMapSet() + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const permission = yield* PermissionV2.Service + const state = State.create({ + initial: () => ({ entries: new Map() }), + editor: (draft) => ({ + list: () => Array.from(draft.entries.entries()) as Array<[string, Entry]>, + get: (name) => draft.entries.get(name) as Entry | undefined, + set: (name, entry) => { + draft.entries.set(name, castDraft(entry) as typeof draft.entries extends Map ? Value : never) + }, + remove: (name) => { + draft.entries.delete(name) + }, + }), + }) + + const definitions = Effect.fn("ToolRegistry.definitions")(function* () { + return Tool.toDefinitions(Object.fromEntries(Array.from(state.get().entries, ([name, entry]) => [name, entry.tool]))) + }) + + const invocation = (input: ExecuteInput): Invocation => ({ + ...input, + // Source needs the durable owning assistant message ID, which the registry does not receive yet. + assertPermission: (request) => permission.assert({ ...request, sessionID: input.sessionID }), + }) + + const settle = Effect.fn("ToolRegistry.settle")(function* (input: ExecuteInput) { + const entry = state.get().entries.get(input.call.name) + if (!entry) return { result: { type: "error" as const, value: `Unknown tool: ${input.call.name}` } } + if (!entry.execute && !entry.tool.execute) + return { result: { type: "error" as const, value: `Tool has no execute handler: ${input.call.name}` } } + + return yield* entry.tool._decode(input.call.input).pipe( + Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })), + Effect.flatMap((parameters) => { + const context = { ...invocation(input), parameters } + const execute = entry.execute?.(context) ?? + entry.tool.execute!(parameters, { id: input.call.id, name: input.call.name }) + return (entry.authorize === undefined ? execute : entry.authorize(context).pipe(Effect.andThen(execute))).pipe( + Effect.flatMap((value) => + entry.tool._encode(value).pipe( + Effect.mapError( + (error) => + new ToolFailure({ + message: `Tool returned an invalid value for its success schema: ${error.message}`, + }), + ), + ), + ), + Effect.map((value): ToolSettlement => { + if (entry.tool._legacyResult && ToolResult.is(value)) + return { result: value, output: ToolOutput.fromResultValue(value) } + const output = entry.tool._project(parameters, input.call.id, value) + const result = ToolOutput.toResultValue(output) + return result.type === "error" ? { result } : { result, output } + }), + ) + }), + Effect.catchTag("LLM.ToolFailure", (failure) => + Effect.succeed({ result: { type: "error" as const, value: failure.message } }), + ), + ) + }) + + const execute = Effect.fn("ToolRegistry.execute")(function* (input: ExecuteInput) { + return (yield* settle(input)).result + }) + + return Service.of({ + transform: state.transform, + contribute: Effect.fn("ToolRegistry.contribute")(function* (update) { + const transform = yield* state.transform() + yield* transform(update) + }), + definitions, + execute, + settle, + }) + }), + ) diff --git a/packages/core/src/tool/apply-patch.ts b/packages/core/src/tool/apply-patch.ts new file mode 100644 index 0000000000..88c9a75057 --- /dev/null +++ b/packages/core/src/tool/apply-patch.ts @@ -0,0 +1,131 @@ +export * as ApplyPatchTool from "./apply-patch" + +import { Tool, ToolFailure, toolText } from "@opencode-ai/llm" +import { Cause, Effect, Layer, Schema } from "effect" +import { FileMutation } from "../file-mutation" +import { FSUtil } from "../fs-util" +import { LocationMutation } from "../location-mutation" +import { Patch } from "../patch" +import { ToolRegistry } from "../tool-registry" + +export const name = "apply_patch" + +export const Parameters = Schema.Struct({ + patchText: Schema.String.annotate({ description: "The full patch text describing add, update, and delete operations" }), +}) + +export const Applied = Schema.Struct({ + type: Schema.Literals(["add", "update", "delete"]), + resource: Schema.String, + target: Schema.String, +}) + +export const Success = Schema.Struct({ applied: Schema.Array(Applied) }) +export type Success = typeof Success.Type + +export const toModelOutput = (output: Success) => + ["Applied patch sequentially:", ...output.applied.map((item) => `${item.type === "add" ? "A" : item.type === "delete" ? "D" : "M"} ${item.resource}`)].join("\n") + +const definition = Tool.make({ + description: + "Apply one patch containing add, update, and delete file operations. All targets are resolved and approved before target contents are read. Operations apply sequentially; if a later operation fails, earlier operations remain applied and the failure reports them explicitly. Moves and atomic rollback are not supported yet.", + parameters: Parameters, + success: Success, + toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })], +}) + +type Planned = { readonly hunk: Patch.Hunk; readonly plan: LocationMutation.Plan } +type Prepared = + | { readonly type: "add"; readonly hunk: Extract; readonly plan: LocationMutation.Plan } + | { readonly type: "delete"; readonly hunk: Extract; readonly plan: LocationMutation.Plan } + | { readonly type: "update"; readonly hunk: Extract; readonly plan: LocationMutation.Plan; readonly source: Uint8Array; readonly content: string } + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const mutation = yield* LocationMutation.Service + const files = yield* FileMutation.Service + const fs = yield* FSUtil.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, assertPermission }) => { + const applied: Array = [] + const fail = (path: string, cause: unknown) => { + const prefix = applied.length === 0 + ? `Unable to apply patch at ${path}` + : `Patch partially applied before failing at ${path}. Applied: ${applied.map((item) => item.resource).join(", ")}` + return new ToolFailure({ message: prefix, error: cause }) + } + return Effect.gen(function* () { + if (!parameters.patchText.trim()) return yield* new ToolFailure({ message: "patchText is required" }) + const hunks = yield* Effect.try({ + try: () => Patch.parse(parameters.patchText), + catch: (cause) => new ToolFailure({ message: `apply_patch verification failed: ${String(cause)}` }), + }) + if (hunks.length === 0) return yield* new ToolFailure({ message: "patch rejected: empty patch" }) + const move = hunks.find((hunk) => hunk.type === "update" && hunk.movePath !== undefined) + if (move) return yield* new ToolFailure({ message: "apply_patch moves are not supported yet" }) + + const planned: Planned[] = [] + for (const hunk of hunks) planned.push({ hunk, plan: yield* mutation.resolve({ path: hunk.path, kind: "file" }) }) + const externalDirectories = new Map() + for (const { plan } of planned) { + const external = plan.target.externalDirectory + if (external) externalDirectories.set(external.resource, external) + } + for (const external of externalDirectories.values()) { + yield* assertPermission(LocationMutation.externalDirectoryPermission(external)) + } + yield* assertPermission({ action: "edit", resources: [...new Set(planned.map(({ plan }) => plan.target.resource))], save: ["*"] }) + + const prepared: Prepared[] = [] + for (const { hunk, plan } of planned) { + if (hunk.type === "add") { + const target = yield* mutation.revalidate(plan) + if (target.exists) return yield* fail(hunk.path, new Error("Target file already exists")) + prepared.push({ type: hunk.type, hunk, plan }) + continue + } + const target = yield* mutation.revalidate(plan) + if (!target.exists || target.type !== "File") return yield* fail(hunk.path, new Error("Target file does not exist")) + if (hunk.type === "delete") { + prepared.push({ type: hunk.type, hunk, plan }) + continue + } + const source = yield* fs.readFile(target.canonical) + const update = Patch.derive(hunk.path, hunk.chunks, new TextDecoder("utf-8", { ignoreBOM: true }).decode(source)) + prepared.push({ type: hunk.type, hunk, plan, source, content: Patch.joinBom(update.content, update.bom) }) + } + + yield* Effect.uninterruptible( + Effect.forEach(prepared, (change) => + Effect.gen(function* () { + if (change.type === "add") { + const result = yield* files.create({ plan: change.plan, content: change.hunk.contents.endsWith("\n") || change.hunk.contents === "" ? change.hunk.contents : `${change.hunk.contents}\n` }) + applied.push({ type: change.type, resource: result.resource, target: result.target }) + return + } + if (change.type === "delete") { + const result = yield* files.remove({ plan: change.plan }) + applied.push({ type: change.type, resource: result.resource, target: result.target }) + return + } + const result = yield* files.writeIfUnchanged({ plan: change.plan, expected: change.source, content: change.content }) + applied.push({ type: change.type, resource: result.resource, target: result.target }) + }).pipe(Effect.catchCause((cause) => Effect.fail(fail(change.hunk.path, Cause.squash(cause))))), + { discard: true }), + ) + return { applied } + }).pipe( + Effect.catchCause((cause) => { + const error = Cause.squash(cause) + return Effect.fail(error instanceof ToolFailure ? error : fail("patch", error)) + }), + ) + }, + }), + ) + }), +) diff --git a/packages/core/src/tool/bash.ts b/packages/core/src/tool/bash.ts new file mode 100644 index 0000000000..0297def11f --- /dev/null +++ b/packages/core/src/tool/bash.ts @@ -0,0 +1,206 @@ +export * as BashTool from "./bash" + +import path from "path" +import { Tool, ToolFailure, toolText } from "@opencode-ai/llm" +import { Cause, Duration, Effect, Layer, Schema } from "effect" +import { ChildProcess } from "effect/unstable/process" +import { Config } from "../config" +import { FSUtil } from "../fs-util" +import { LocationMutation } from "../location-mutation" +import { AppProcess } from "../process" +import { PositiveInt } from "../schema" +import { ToolOutputStore } from "../tool-output-store" +import { ToolRegistry } from "../tool-registry" + +export const name = "bash" +export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000 +export const MAX_TIMEOUT_MS = 10 * 60 * 1_000 +export const MAX_CAPTURE_BYTES = 1024 * 1024 + +export const Parameters = Schema.Struct({ + command: Schema.String.annotate({ description: "Shell command string to execute" }), + workdir: Schema.String.pipe(Schema.optional).annotate({ + description: "Working directory. Defaults to the active Location; relative paths resolve from that Location.", + }), + timeout: PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_TIMEOUT_MS)) + .pipe(Schema.optional) + .annotate({ + description: `Timeout in milliseconds. Defaults to ${DEFAULT_TIMEOUT_MS} and may not exceed ${MAX_TIMEOUT_MS}.`, + }), + description: Schema.String.pipe(Schema.optional).annotate({ + description: "Concise description of the command's purpose", + }), +}) + +const Success = Schema.Struct({ + command: Schema.String, + cwd: Schema.String, + exitCode: Schema.Number.pipe(Schema.optional), + /** Bounded compact equivalent of stdout/stderr: stderr is labeled when present. */ + output: Schema.String, + truncated: Schema.Boolean, + stdoutTruncated: Schema.Boolean.pipe(Schema.optional), + stderrTruncated: Schema.Boolean.pipe(Schema.optional), + resource: ToolOutputStore.Resource.pipe(Schema.optional), + timedOut: Schema.Boolean.pipe(Schema.optional), + warnings: Schema.Array(Schema.String).pipe(Schema.optional), +}) + +type Success = typeof Success.Type + +const defaultShell = () => (process.platform === "win32" ? (process.env.COMSPEC ?? "cmd.exe") : "/bin/sh") + +const compactOutput = (stdout: string, stderr: string) => { + const output = stdout && stderr ? `${stdout}\n\nstderr:\n${stderr}` : stderr ? `stderr:\n${stderr}` : stdout + return output || "(no output)" +} + +const captureNotice = (stdoutTruncated: boolean, stderrTruncated: boolean) => { + if (stdoutTruncated && stderrTruncated) return "[stdout and stderr capture truncated at the in-memory safety limit]" + if (stdoutTruncated) return "[stdout capture truncated at the in-memory safety limit]" + if (stderrTruncated) return "[stderr capture truncated at the in-memory safety limit]" +} + +const modelOutput = (output: Success) => { + const warnings = output.warnings?.length + ? `\n\nWarnings:\n${output.warnings.map((warning) => `- ${warning}`).join("\n")}` + : "" + if (output.timedOut) return `${output.output}${warnings}\n\nCommand timed out before completion.` + return `${output.output}${warnings}\n\nCommand exited with code ${output.exitCode}.` +} + +const isTimeout = (error: AppProcess.AppProcessError) => + error.cause instanceof Error && error.cause.message === "Timed out" + +const definition = Tool.make({ + description: `Execute one shell command string with the host user's filesystem, process, and network authority. The active Location is the default working directory. Relative workdir values resolve from that Location. External workdir values require external_directory approval; best-effort command-argument path warnings are advisory only. Timeout values are milliseconds (default: ${DEFAULT_TIMEOUT_MS}; maximum: ${MAX_TIMEOUT_MS}). Uses the configured shell when set; otherwise uses /bin/sh on POSIX and COMSPEC or cmd.exe on Windows.`, + parameters: Parameters, + success: Success, + toModelOutput: ({ output }) => [toolText({ type: "text", text: modelOutput(output) })], +}) + +/** + * Minimal V2 core shell boundary. Keep parity debt visible without pulling the + * legacy shell runtime into core. + */ +// TODO: Port tree-sitter bash / PowerShell parser-based approval reduction. +// TODO: Port BashArity reusable command-prefix approvals. +// TODO: Replace token-based command-argument external-directory advisories with parser-based detection. +// TODO: Restore PowerShell and cmd-specific invocation/path handling on Windows. +// TODO: Add plugin shell.env environment augmentation once V2 plugin hooks exist. +// TODO: Add durable/live progress metadata streaming for long-running commands once V2 tool invocation progress context is wired. +// TODO: Persist background job status and define restart recovery before exposing remote observation. +// TODO: Re-add model-facing background launch only with owner-bound get/wait/cancel tools and completion delivery. +// TODO: Add HTTP background-job observation only after durable status, restart recovery, and authorization are defined. +// TODO: Revisit process-group cleanup and platform coverage with shell-specific tests if current AppProcess semantics do not fully cover it. +// TODO: Revisit binary output handling if stdout/stderr decoding is text-only. +// TODO: Stream full shell output into managed storage while retaining only a bounded in-memory preview. + +const shellTokens = (command: string) => command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [] +const unquote = (value: string) => value.replace(/^(['"])(.*)\1$/, "$2") +const externalCommandDirectories = (command: string, cwd: string) => { + const directories = new Set() + for (const token of shellTokens(command)) { + const value = unquote(token).replace(/[;,|&]+$/, "") + if (!path.isAbsolute(value)) continue + const resolved = FSUtil.resolve(value) + if (FSUtil.contains(cwd, resolved)) continue + directories.add(FSUtil.resolve(path.dirname(resolved))) + } + return [...directories] +} + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const mutation = yield* LocationMutation.Service + const appProcess = yield* AppProcess.Service + const resources = yield* ToolOutputStore.Service + const config = yield* Config.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, sessionID, call, assertPermission }) => + Effect.gen(function* () { + const plan = yield* mutation.resolve({ path: parameters.workdir ?? ".", kind: "directory" }) + const external = plan.target.externalDirectory + if (external) yield* assertPermission(LocationMutation.externalDirectoryPermission(external)) + const warnings = externalCommandDirectories(parameters.command, plan.target.canonical).map( + (directory) => + `Command argument references external directory ${path.join(directory, "*").replaceAll("\\", "/")}. Bash runs with host-user filesystem, process, and network authority; this scan is advisory only.`, + ) + yield* assertPermission({ action: name, resources: [parameters.command], save: [parameters.command] }) + + const target = yield* mutation.revalidate(plan) + if (!target.exists || target.type !== "Directory") + throw new Error(`Working directory is not a directory: ${target.canonical}`) + + const entries = yield* config.entries() + const shell = + Object.assign({}, ...entries.flatMap((entry) => (entry.type === "document" ? [entry.info] : []))).shell ?? + defaultShell() + const command = ChildProcess.make(parameters.command, [], { + cwd: target.canonical, + shell, + stdin: "ignore", + detached: process.platform !== "win32", + forceKillAfter: Duration.seconds(3), + }) + const timeout = parameters.timeout ?? DEFAULT_TIMEOUT_MS + const result = yield* appProcess + .run(command, { + timeout: Duration.millis(timeout), + maxOutputBytes: MAX_CAPTURE_BYTES, + maxErrorBytes: MAX_CAPTURE_BYTES, + }) + .pipe( + Effect.catchTag("AppProcessError", (error) => + isTimeout(error) ? Effect.succeed(undefined) : Effect.fail(error), + ), + ) + if (!result) { + return { + command: parameters.command, + cwd: target.canonical, + output: `Command exceeded timeout of ${timeout} ms. Retry with a larger timeout if the command is expected to take longer.`, + truncated: false, + timedOut: true, + ...(warnings.length ? { warnings } : {}), + } + } + + const compact = compactOutput(result.stdout.toString("utf8"), result.stderr.toString("utf8")) + const notice = captureNotice(result.stdoutTruncated, result.stderrTruncated) + const truncated = yield* resources.truncate({ + sessionID, + toolCallID: call.id, + content: notice ? `${compact}\n\n${notice}` : compact, + }) + return { + command: parameters.command, + cwd: target.canonical, + exitCode: result.exitCode, + output: truncated.content, + truncated: truncated.truncated || result.stdoutTruncated || result.stderrTruncated, + ...(warnings.length ? { warnings } : {}), + ...(result.stdoutTruncated ? { stdoutTruncated: true } : {}), + ...(result.stderrTruncated ? { stderrTruncated: true } : {}), + ...(truncated.truncated && !result.stdoutTruncated && !result.stderrTruncated + ? { resource: truncated.resource } + : {}), + } + }).pipe( + Effect.catchCause((cause) => + Effect.fail( + new ToolFailure({ + message: `Unable to execute command: ${parameters.command}`, + error: Cause.squash(cause), + }), + ), + ), + ), + }), + ) + }), +) diff --git a/packages/core/src/tool/builtins.ts b/packages/core/src/tool/builtins.ts new file mode 100644 index 0000000000..e8fcc43b2e --- /dev/null +++ b/packages/core/src/tool/builtins.ts @@ -0,0 +1,43 @@ +export * as BuiltInTools from "./builtins" + +import { Layer } from "effect" +import { BashTool } from "./bash" +import { ApplyPatchTool } from "./apply-patch" +import { EditTool } from "./edit" +import { GlobTool } from "./glob" +import { GrepTool } from "./grep" +import { QuestionTool } from "./question" +import { ReadTool } from "./read" +import { SkillTool } from "./skill" +import { TodoWriteTool } from "./todowrite" +import { WebFetchTool } from "./webfetch" +import { WebSearchTool } from "./websearch" +import { WriteTool } from "./write" + +/** + * Composes only the shipped Location-scoped built-in tool contributions. + * Each tool retains its implementation and focused tests independently. Dynamic + * MCP and plugin tools later use separate scoped ToolRegistry transforms, while + * provider/model filtering belongs to a future materialization phase rather + * than this static list. The caller intentionally supplies shared Location + * services once to this merged set. + * + * TODO: Port the remaining launch-follow-up leaves deliberately: edit fuzzy + * parity, task, LSP, + * repo_clone, repo_overview, plan_exit, and Rune/code mode. Keep MCP and plugin + * contributions separate from this static built-in list. + */ +export const locationLayer = Layer.mergeAll( + ApplyPatchTool.layer, + BashTool.layer, + EditTool.layer, + GlobTool.layer, + GrepTool.layer, + QuestionTool.layer, + ReadTool.layer, + SkillTool.layer, + TodoWriteTool.layer, + WebFetchTool.layer, + WebSearchTool.layer.pipe(Layer.provide(WebSearchTool.defaultConfigLayer)), + WriteTool.layer, +) diff --git a/packages/core/src/tool/edit.ts b/packages/core/src/tool/edit.ts new file mode 100644 index 0000000000..01eed4be99 --- /dev/null +++ b/packages/core/src/tool/edit.ts @@ -0,0 +1,169 @@ +/** + * Model-facing V2 exact-edit leaf. Relative paths resolve within the active + * Location. Absolute paths inside that Location are accepted, while explicit + * absolute external paths retain mutation capability through a separate + * external_directory approval before edit approval. Named project references + * are read-oriented and deliberately are not accepted by mutation tools. + */ +export * as EditTool from "./edit" + +import { Tool, ToolFailure, toolText } from "@opencode-ai/llm" +import { Cause, Effect, Layer, Schema } from "effect" +import { FileMutation } from "../file-mutation" +import { FSUtil } from "../fs-util" +import { LocationMutation } from "../location-mutation" +import { ToolRegistry } from "../tool-registry" + +export const name = "edit" + +export const Parameters = Schema.Struct({ + path: Schema.String.annotate({ + description: + "File path to edit. Relative paths resolve within the active Location. Absolute paths inside that Location are accepted; external absolute paths require external_directory approval. Named project references are read-oriented and are not accepted.", + }), + oldString: Schema.String.annotate({ description: "Exact text to replace" }), + newString: Schema.String.annotate({ description: "Replacement text, which must differ from oldString" }), + replaceAll: Schema.Boolean.pipe(Schema.optional).annotate({ + description: "Replace all exact occurrences of oldString (default false)", + }), +}) + +export const Success = Schema.Struct({ + operation: Schema.Literal("write"), + target: Schema.String, + resource: Schema.String, + existed: Schema.Boolean, + replacements: Schema.Number, +}) +export type Success = typeof Success.Type + +const normalizeLineEndings = (text: string) => text.replaceAll("\r\n", "\n") +const detectLineEnding = (text: string): "\n" | "\r\n" => (text.includes("\r\n") ? "\r\n" : "\n") +const convertToLineEnding = (text: string, ending: "\n" | "\r\n") => + ending === "\n" ? normalizeLineEndings(text) : normalizeLineEndings(text).replaceAll("\n", "\r\n") + +const splitBom = (text: string) => + text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text } +const joinBom = (text: string, bom: boolean) => (bom ? `\uFEFF${text}` : text) +const decodeUtf8 = (content: Uint8Array) => { + const bom = content[0] === 0xef && content[1] === 0xbb && content[2] === 0xbf + return { bom, content, text: new TextDecoder().decode(bom ? content.slice(3) : content) } +} + +const countOccurrences = (content: string, search: string) => { + if (search === "") return content.length + 1 + let count = 0 + let offset = 0 + while ((offset = content.indexOf(search, offset)) !== -1) { + count++ + offset += search.length + } + return count +} + +const previewLines = (value: string, prefix: "+" | "-") => { + const lines = normalizeLineEndings(value).split("\n") + const shown = lines.slice(0, 6).map((line) => `${prefix}${line.length > 240 ? `${line.slice(0, 240)}...` : line}`) + if (lines.length > shown.length) shown.push(`${prefix}...`) + return shown +} + +export const toModelOutput = (output: Success, oldString: string, newString: string) => + [ + `Edited file successfully: ${output.resource}`, + `Replacements: ${output.replacements}`, + "```diff", + ...previewLines(oldString, "-"), + ...previewLines(newString, "+"), + "```", + ].join("\n") + +const definition = Tool.make({ + description: + "Replace exact text in one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval. Named project references are read-oriented and are not accepted.", + parameters: Parameters, + success: Success, + toModelOutput: ({ parameters, output }) => [ + toolText({ type: "text", text: toModelOutput(output, parameters.oldString, parameters.newString) }), + ], +}) + +/** Deferred V2 edit behavior and UX integrations remain visible at the model-facing seam. */ +// TODO: Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review. +// TODO: Add formatter integration after V2 formatter runtime exists. +// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +// TODO: Add snapshots / undo after design exists. +// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists. + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const mutation = yield* LocationMutation.Service + const files = yield* FileMutation.Service + const fs = yield* FSUtil.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, assertPermission }) => { + const unableToEdit = (effect: Effect.Effect) => + effect.pipe( + Effect.catchCause((cause) => { + const error = Cause.squash(cause) + return Effect.fail( + error instanceof FileMutation.StaleContentError + ? new ToolFailure({ message: "File changed after permission approval. Read it again before editing." }) + : new ToolFailure({ message: `Unable to edit ${parameters.path}`, error }), + ) + }), + ) + + return Effect.gen(function* () { + if (parameters.oldString === parameters.newString) { + return yield* new ToolFailure({ message: "No changes to apply: oldString and newString are identical." }) + } + if (parameters.oldString === "") { + return yield* new ToolFailure({ message: "oldString must not be empty. Use write to create or overwrite a file." }) + } + + const plan = yield* unableToEdit(mutation.resolve({ path: parameters.path, kind: "file" })) + const external = plan.target.externalDirectory + if (external) { + yield* unableToEdit(assertPermission(LocationMutation.externalDirectoryPermission(external))) + } + + yield* unableToEdit(assertPermission({ action: "edit", resources: [plan.target.resource], save: ["*"] })) + const readable = yield* unableToEdit(mutation.revalidate(plan)) + const source = decodeUtf8(yield* unableToEdit(fs.readFile(readable.canonical))) + const ending = detectLineEnding(source.text) + const oldString = convertToLineEnding(parameters.oldString, ending) + const newString = convertToLineEnding(parameters.newString, ending) + const replacements = countOccurrences(source.text, oldString) + if (replacements === 0) { + return yield* new ToolFailure({ + message: + "Could not find oldString in the file. It must match exactly, including whitespace and indentation.", + }) + } + if (replacements > 1 && parameters.replaceAll !== true) { + return yield* new ToolFailure({ + message: + "Found multiple exact matches for oldString. Provide more surrounding context or set replaceAll to true.", + }) + } + + const replaced = + parameters.replaceAll === true + ? source.text.replaceAll(oldString, newString) + : source.text.replace(oldString, newString) + const next = splitBom(replaced) + const result = yield* unableToEdit( + files.writeIfUnchanged({ plan, expected: source.content, content: joinBom(next.text, source.bom || next.bom) }), + ) + return { ...result, replacements } satisfies Success + }) + }, + }), + ) + }), +) diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/glob.ts new file mode 100644 index 0000000000..9b90bbe138 --- /dev/null +++ b/packages/core/src/tool/glob.ts @@ -0,0 +1,75 @@ +export * as GlobTool from "./glob" + +import { Tool, ToolFailure, toolText } from "@opencode-ai/llm" +import { Cause, Effect, Layer, Schema } from "effect" +import { FileSystem } from "../filesystem" +import { LocationSearch } from "../location-search" +import { ToolRegistry } from "../tool-registry" + +export const name = "glob" + +export const Parameters = Schema.Struct({ + pattern: LocationSearch.FilesInput.fields.pattern.annotate({ description: "Glob pattern to match files against" }), + path: LocationSearch.FilesInput.fields.path.annotate({ description: "Relative directory to search. Defaults to the active Location." }), + reference: LocationSearch.FilesInput.fields.reference.annotate({ description: "Named project reference to search instead of the active Location" }), + limit: LocationSearch.FilesInput.fields.limit.annotate({ description: `Maximum results to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})` }), +}) + +type ModelOutput = typeof LocationSearch.FilesResult.Encoded + +/** Format raw Location search results into the concise line-oriented output models expect. */ +export const toModelOutput = (output: ModelOutput) => { + const lines = output.items.length === 0 ? ["No files found"] : output.items.map((item) => item.resource) + if (output.truncated) { + lines.push("", `(Results are truncated: showing first ${output.items.length} results. Consider using a more specific path or pattern.)`) + } + if (output.partial) lines.push("", "(Results may be incomplete because some discovered files could not be read.)") + return lines.join("\n") +} + +const definition = Tool.make({ + description: "Find files by glob pattern within the active Location or a named project reference. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.", + parameters: Parameters, + success: LocationSearch.FilesResult, + toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })], +}) + +/** + * Location-scoped glob leaf. FileSystem selects a canonical root for + * permission metadata; LocationSearch owns containment and traversal. + * + * TODO: Revisit root-specific search permission resources if named-reference policy needs independent allow/deny rules. + */ +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const filesystem = yield* FileSystem.Service + const search = yield* LocationSearch.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, assertPermission }) => + Effect.gen(function* () { + const root = yield* filesystem.resolveRoot({ path: parameters.path, reference: parameters.reference }) + yield* assertPermission({ + action: name, + resources: [parameters.pattern], + save: ["*"], + metadata: { + root: root.resource, + reference: parameters.reference, + path: parameters.path, + limit: parameters.limit, + }, + }) + return yield* search.files(parameters, root) + }).pipe( + Effect.catchCause((cause) => + Effect.fail(new ToolFailure({ message: `Unable to find files matching ${parameters.pattern}`, error: Cause.squash(cause) })), + ), + ), + }), + ) + }), +) diff --git a/packages/core/src/tool/grep.ts b/packages/core/src/tool/grep.ts new file mode 100644 index 0000000000..842c6910f9 --- /dev/null +++ b/packages/core/src/tool/grep.ts @@ -0,0 +1,91 @@ +export * as GrepTool from "./grep" + +import { Tool, ToolFailure, toolText } from "@opencode-ai/llm" +import { Cause, Effect, Layer, Schema } from "effect" +import { FileSystem } from "../filesystem" +import { LocationSearch } from "../location-search" +import { Ripgrep } from "../ripgrep" +import { ToolRegistry } from "../tool-registry" + +export const name = "grep" + +export const Parameters = Schema.Struct({ + pattern: LocationSearch.GrepInput.fields.pattern.annotate({ description: "Regex pattern to search for in file contents" }), + path: LocationSearch.GrepInput.fields.path.annotate({ description: "Relative file or directory to search. Defaults to the active Location." }), + reference: LocationSearch.GrepInput.fields.reference.annotate({ description: "Named project reference to search instead of the active Location" }), + include: LocationSearch.GrepInput.fields.include.annotate({ description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")' }), + limit: LocationSearch.GrepInput.fields.limit.annotate({ description: `Maximum matches to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})` }), +}) + +type Success = typeof LocationSearch.GrepResult.Encoded + +/** Format raw Location search matches into the familiar concise model output. */ +export const toModelOutput = (output: Success) => { + const lines = output.items.length === 0 ? ["No files found"] : [`Found ${output.items.length} matches`] + let current = "" + for (const match of output.items) { + if (current !== match.resource) { + if (current) lines.push("") + current = match.resource + lines.push(`${match.resource}:`) + } + lines.push(` Line ${match.line}: ${match.lines}${match.linePreviewTruncated ? "..." : ""}`) + } + if (output.truncated) { + lines.push("", `(Results are truncated: showing first ${output.items.length} matches. Consider using a more specific path or pattern.)`) + } + if (output.partial) lines.push("", "(Some paths were inaccessible and skipped)") + return lines.join("\n") +} + +const definition = Tool.make({ + description: "Search file contents by regular expression within the active Location or a named project reference. Use a relative path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise relative file resources, line numbers, and bounded line previews.", + parameters: Parameters, + success: LocationSearch.GrepResult, + toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })], +}) + +/** + * Location-scoped grep leaf. FileSystem selects a canonical root for + * permission metadata; LocationSearch owns containment and ripgrep execution. + * + * TODO: Revisit root-specific search permission resources if named-reference policy needs independent allow/deny rules. + */ +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const filesystem = yield* FileSystem.Service + const search = yield* LocationSearch.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, assertPermission }) => + Effect.gen(function* () { + const root = yield* filesystem.resolveRoot(parameters) + yield* assertPermission({ + action: name, + resources: [parameters.pattern], + save: ["*"], + metadata: { + root: root.resource, + reference: parameters.reference, + path: parameters.path, + include: parameters.include, + limit: parameters.limit, + }, + }) + return yield* search.grep(parameters, root) + }).pipe( + Effect.catchCause((cause) => { + const error = Cause.squash(cause) + const message = error instanceof Ripgrep.InvalidPatternError + ? `Invalid grep pattern ${JSON.stringify(parameters.pattern)}: ${error.message}` + : `Unable to grep for ${parameters.pattern}` + return Effect.fail(new ToolFailure({ message, error })) + }), + ), + }), + ) + }), +) diff --git a/packages/core/src/tool/question.ts b/packages/core/src/tool/question.ts new file mode 100644 index 0000000000..b4378fe474 --- /dev/null +++ b/packages/core/src/tool/question.ts @@ -0,0 +1,76 @@ +export * as QuestionTool from "./question" + +import { Tool, toolText } from "@opencode-ai/llm" +import { Effect, Layer, Schema } from "effect" +import { QuestionV2 } from "../question" +import { ToolRegistry } from "../tool-registry" + +export const name = "question" + +export const description = `Use this tool when you need to ask the user questions during execution. This allows you to: +1. Gather user preferences or requirements +2. Clarify ambiguous instructions +3. Get decisions on implementation choices as you work +4. Offer choices to the user about what direction to take. + +Usage notes: +- When \`custom\` is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options +- Answers are returned as arrays of labels; set \`multiple: true\` to allow selecting more than one +- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label` + +export const Parameters = Schema.Struct({ + questions: Schema.Array(QuestionV2.Prompt).annotate({ description: "Questions to ask" }), +}) + +export const Success = Schema.Struct({ + answers: Schema.Array(QuestionV2.Answer), +}) +export type Success = typeof Success.Type + +export const toModelOutput = ( + questions: ReadonlyArray, + answers: ReadonlyArray, +) => { + const formatted = questions + .map( + (question, index) => + `"${question.question}"="${answers[index]?.length ? answers[index].join(", ") : "Unanswered"}"`, + ) + .join(", ") + return `User has answered your questions: ${formatted}. You can now continue with the user's answers in mind.` +} + +const definition = Tool.make({ + description, + parameters: Parameters, + success: Success, + toModelOutput: ({ parameters, output }) => [ + toolText({ type: "text", text: toModelOutput(parameters.questions, output.answers) }), + ], +}) + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const question = yield* QuestionV2.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, sessionID, source }) => + question + .ask({ + sessionID, + questions: parameters.questions, + // The registry intentionally leaves source absent until it owns the durable assistant message ID. + tool: source?.type === "tool" ? { messageID: source.messageID, callID: source.callID } : undefined, + }) + .pipe( + Effect.map((answers) => ({ answers })), + // V1 treats a dismissed question as an interrupted tool invocation rather than model-facing text. + Effect.orDie, + ), + }), + ) + }), +) diff --git a/packages/core/src/tool/read.ts b/packages/core/src/tool/read.ts new file mode 100644 index 0000000000..a9be2ece00 --- /dev/null +++ b/packages/core/src/tool/read.ts @@ -0,0 +1,96 @@ +export * as ReadTool from "./read" + +import { Tool, ToolFailure } from "@opencode-ai/llm" +import { Cause, Effect, Layer, Schema } from "effect" +import { FileSystem } from "../filesystem" +import { NonNegativeInt, PositiveInt } from "../schema" +import { PermissionV2 } from "../permission" +import { ToolOutputStore } from "../tool-output-store" +import { ToolRegistry } from "../tool-registry" + +export const name = "read" +const LocationInput = Schema.Struct({ + ...FileSystem.ReadInput.fields, + offset: FileSystem.ListPageInput.fields.offset.annotate({ + description: "The 1-based directory entry or text line offset to start reading from", + }), + limit: FileSystem.ListPageInput.fields.limit.annotate({ + description: "The maximum number of directory entries or text lines to read", + }), +}) +const ResourceInput = Schema.Struct({ + resource: Schema.String, + offset: NonNegativeInt.pipe(Schema.optional), + limit: PositiveInt.check(Schema.isLessThanOrEqualTo(ToolOutputStore.MAX_READ_BYTES)).pipe(Schema.optional), +}) +const Input = Schema.Union([LocationInput, ResourceInput]) +const Success = Schema.Union([FileSystem.Content, FileSystem.TextPage, FileSystem.ListPage, ToolOutputStore.Page]) + +const definition = Tool.make({ + description: + "Read a text or binary file, page through a large UTF-8 text file by line offset, list a directory page relative to the current location, or page through a managed tool-output resource by opaque URI.", + parameters: Input, + success: Success, +}) + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const filesystem = yield* FileSystem.Service + const resources = yield* ToolOutputStore.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, sessionID, assertPermission }) => { + const input = parameters + return Effect.gen(function* () { + if ("resource" in input) + return yield* resources.read({ sessionID, uri: input.resource, offset: input.offset, limit: input.limit }) + const resolved = yield* filesystem.resolveReadPath(input) + if (resolved.type === "directory") { + const { offset, limit } = input + const target = resolved.target + yield* assertPermission({ action: name, resources: [target.resource], save: ["*"] }) + const final = yield* filesystem.resolveReadPath(input) + if ( + final.type !== "directory" || + final.target.resource !== target.resource || + final.target.real !== target.real + ) + return yield* Effect.die(new Error("Directory changed after permission approval")) + return yield* filesystem.listPageResolved(final.target, { offset, limit }) + } + const target = resolved.target + yield* assertPermission({ + action: name, + resources: [target.resource], + save: ["*"], + }) + const final = yield* filesystem.resolveReadPath(input) + if (final.type !== "file" || final.target.resource !== target.resource || final.target.real !== target.real) + return yield* Effect.die(new Error("File changed after permission approval")) + if (final.target.size > FileSystem.MAX_READ_BYTES || input.offset !== undefined || input.limit !== undefined) + return yield* filesystem.readTextPageResolved(final.target, { offset: input.offset, limit: input.limit }) + return yield* filesystem.readResolved(final.target, FileSystem.MAX_READ_BYTES) + }).pipe( + Effect.catchCause((cause) => + Effect.fail( + new ToolFailure({ + message: `Unable to read ${"resource" in input ? input.resource : input.path}`, + error: Cause.squash(cause), + }), + ), + ), + ) + }, + }), + ) + }), +) +export const locationLayer = layer.pipe( + Layer.provideMerge(ToolRegistry.layer), + Layer.provideMerge(FileSystem.locationLayer), + Layer.provideMerge(PermissionV2.locationLayer), + Layer.provideMerge(ToolOutputStore.defaultLayer), +) diff --git a/packages/core/src/tool/skill.ts b/packages/core/src/tool/skill.ts new file mode 100644 index 0000000000..bf06b0a3f2 --- /dev/null +++ b/packages/core/src/tool/skill.ts @@ -0,0 +1,119 @@ +export * as SkillTool from "./skill" + +import path from "path" +import { pathToFileURL } from "url" +import { Tool, ToolFailure, toolText } from "@opencode-ai/llm" +import { Cause, Effect, Layer, Schema } from "effect" +import { FSUtil } from "../fs-util" +import { PluginBoot } from "../plugin/boot" +import { SkillV2 } from "../skill" +import { ToolOutputStore } from "../tool-output-store" +import { ToolRegistry } from "../tool-registry" + +export const name = "skill" +const FILE_LIMIT = 10 + +export const Parameters = Schema.Struct({ + name: Schema.String.annotate({ description: "The name of the skill from the available skills list" }), +}) + +export const Success = Schema.Struct({ + name: Schema.String, + directory: Schema.String, + output: Schema.String, + truncated: Schema.Boolean, + resource: ToolOutputStore.Resource.pipe(Schema.optional), +}) + +export const description = (skills: ReadonlyArray) => + [ + "Load a specialized skill when the task at hand matches one of the available skills listed below.", + "", + "Use this tool to inject the skill's instructions and resources into the current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc. in the same directory as the skill.", + "", + "The skill name must match one of the available skills listed below:", + "", + ...(skills.length + ? skills.map((skill) => `- **${skill.name}**: ${skill.description ?? "No description provided."}`) + : ["No skills are currently available."]), + ].join("\n") + +export const toModelOutput = (skill: SkillV2.Info, files: ReadonlyArray) => { + const directory = path.dirname(skill.location) + return [ + ``, + `# Skill: ${skill.name}`, + "", + skill.content.trim(), + "", + `Base directory for this skill: ${pathToFileURL(directory).href}`, + "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", + "Note: file list is sampled.", + "", + "", + ...files.map((file) => `${file}`), + "", + "", + ].join("\n") +} + +const notFound = (name: string, skills: ReadonlyArray) => + new ToolFailure({ + message: `Skill "${name}" not found. Available skills: ${skills.map((skill) => skill.name).join(", ") || "none"}`, + }) + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const fs = yield* FSUtil.Service + const boot = yield* PluginBoot.Service + const skills = yield* SkillV2.Service + const resources = yield* ToolOutputStore.Service + yield* boot.wait() + const available = yield* skills.list() + const definition = Tool.make({ + description: description(available), + parameters: Parameters, + success: Success, + toModelOutput: ({ output }) => [toolText({ type: "text", text: output.output })], + }) + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, sessionID, call, assertPermission }) => + Effect.gen(function* () { + const current = yield* skills.list() + const skill = current.find((skill) => skill.name === parameters.name) + if (!skill) return yield* notFound(parameters.name, current) + return yield* Effect.gen(function* () { + yield* assertPermission({ action: name, resources: [skill.name], save: [skill.name] }) + const directory = path.dirname(skill.location) + const files = (yield* fs.glob("**/*", { cwd: directory, absolute: true, include: "file", dot: true })) + .filter((file) => path.basename(file) !== "SKILL.md") + .toSorted() + .slice(0, FILE_LIMIT) + const output = yield* resources.truncate({ + sessionID, + toolCallID: call.id, + content: toModelOutput(skill, files), + }) + return { + name: skill.name, + directory, + output: output.content, + truncated: output.truncated, + ...(output.truncated ? { resource: output.resource } : {}), + } + }).pipe( + Effect.catchCause((cause) => + Effect.fail( + new ToolFailure({ message: `Unable to load skill ${parameters.name}`, error: Cause.squash(cause) }), + ), + ), + ) + }), + }), + ) + }), +) diff --git a/packages/core/src/tool/todowrite.ts b/packages/core/src/tool/todowrite.ts new file mode 100644 index 0000000000..de43e01954 --- /dev/null +++ b/packages/core/src/tool/todowrite.ts @@ -0,0 +1,50 @@ +export * as TodoWriteTool from "./todowrite" + +import { Tool, ToolFailure, toolText } from "@opencode-ai/llm" +import { Cause, Effect, Layer, Schema } from "effect" +import { SessionTodo } from "../session/todo" +import { ToolRegistry } from "../tool-registry" + +export const name = "todowrite" + +export const Parameters = Schema.Struct({ + todos: Schema.Array(SessionTodo.Info).annotate({ description: "The updated todo list" }), +}) + +export const Success = Schema.Struct({ + todos: Schema.Array(SessionTodo.Info), +}) +export type Success = typeof Success.Type + +export const toModelOutput = (output: Success) => JSON.stringify(output.todos, null, 2) + +const definition = Tool.make({ + description: + "Create and maintain a structured task list for the current coding session. Use it to track progress during multi-step work and keep todo statuses current.", + parameters: Parameters, + success: Success, + toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })], +}) + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const todos = yield* SessionTodo.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, sessionID, assertPermission }) => + Effect.gen(function* () { + yield* assertPermission({ action: name, resources: ["*"], save: ["*"] }) + yield* todos.update({ sessionID, todos: parameters.todos }) + return { todos: parameters.todos } + }).pipe( + Effect.catchCause((cause) => + Effect.fail(new ToolFailure({ message: "Unable to update todos", error: Cause.squash(cause) })), + ), + ), + }), + ) + }), +) diff --git a/packages/core/src/tool/webfetch.ts b/packages/core/src/tool/webfetch.ts new file mode 100644 index 0000000000..d4d9db7dc6 --- /dev/null +++ b/packages/core/src/tool/webfetch.ts @@ -0,0 +1,222 @@ +export * as WebFetchTool from "./webfetch" + +import { Tool, ToolFailure, toolText } from "@opencode-ai/llm" +import { Cause, Duration, Effect, Layer, Schema, Stream } from "effect" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { Parser } from "htmlparser2" +import TurndownService from "turndown" +import { ToolOutputStore } from "../tool-output-store" +import { ToolRegistry } from "../tool-registry" + +export const name = "webfetch" +export const MAX_RESPONSE_BYTES = 5 * 1024 * 1024 +export const DEFAULT_TIMEOUT_SECONDS = 30 +export const MAX_TIMEOUT_SECONDS = 120 + +export const description = `Fetch content from an HTTP or HTTPS URL and return it as text, markdown, or HTML. Markdown is the default. + +Use a more targeted tool when one is available. This tool is read-only. Large text results are truncated with an opaque managed resource URI for paging.` + +const Timeout = Schema.Number.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(MAX_TIMEOUT_SECONDS)) + +export const Parameters = Schema.Struct({ + url: Schema.String.annotate({ description: "The HTTP or HTTPS URL to fetch content from" }), + format: Schema.Literals(["text", "markdown", "html"]) + .annotate({ description: "The format to return the content in. Defaults to markdown." }) + .pipe(Schema.withDecodingDefault(Effect.succeed("markdown" as const))), + timeout: Timeout.pipe(Schema.optional).annotate({ + description: `Optional timeout in seconds (maximum: ${MAX_TIMEOUT_SECONDS})`, + }), +}) + +const Success = Schema.Struct({ + url: Schema.String, + contentType: Schema.String, + format: Parameters.fields.format, + output: Schema.String, + truncated: Schema.Boolean, + resource: ToolOutputStore.Resource.pipe(Schema.optional), +}) + +type Format = (typeof Parameters.Type)["format"] + +const acceptHeader = (format: Format) => { + switch (format) { + case "markdown": + return "text/markdown;q=1.0, text/x-markdown;q=0.9, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1" + case "text": + return "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1" + case "html": + return "text/html;q=1.0, application/xhtml+xml;q=0.9, text/plain;q=0.8, text/markdown;q=0.7, */*;q=0.1" + } +} + +const headers = (format: Format, userAgent: string) => ({ + "User-Agent": userAgent, + Accept: acceptHeader(format), + "Accept-Language": "en-US,en;q=0.9", +}) + +const browserUserAgent = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" + +const isCloudflareChallenge = (error: unknown) => { + if (!error || typeof error !== "object" || !("reason" in error)) return false + const reason = error.reason + if ( + !reason || + typeof reason !== "object" || + !("_tag" in reason) || + reason._tag !== "StatusCodeError" || + !("response" in reason) + ) + return false + const response = reason.response as HttpClientResponse.HttpClientResponse + return response.status === 403 && response.headers["cf-mitigated"] === "challenge" +} + +const request = (url: string, format: Format, userAgent = browserUserAgent) => + HttpClientRequest.get(url).pipe(HttpClientRequest.setHeaders(headers(format, userAgent))) + +const assertHttpUrl = (url: URL) => { + if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("URL must use http:// or https://") +} + +const execute = (http: HttpClient.HttpClient, url: string, format: Format, userAgent = browserUserAgent) => + http.execute(request(url, format, userAgent)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk)) + +const collectBody = (response: HttpClientResponse.HttpClientResponse) => + Effect.gen(function* () { + const contentLength = response.headers["content-length"] + if (contentLength && Number.parseInt(contentLength, 10) > MAX_RESPONSE_BYTES) { + return yield* Effect.die(new Error(`Response too large (exceeds ${MAX_RESPONSE_BYTES} byte limit)`)) + } + const chunks: Uint8Array[] = [] + let size = 0 + yield* Stream.runForEach(response.stream, (chunk) => + Effect.sync(() => { + size += chunk.byteLength + if (size > MAX_RESPONSE_BYTES) throw new Error(`Response too large (exceeds ${MAX_RESPONSE_BYTES} byte limit)`) + chunks.push(chunk) + }), + ) + return Buffer.concat(chunks, size) + }) + +const mimeFrom = (contentType: string) => contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "" +const isImageAttachment = (mime: string) => + mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" +const isTextualMime = (mime: string) => + !mime || + mime.startsWith("text/") || + mime === "application/json" || + mime.endsWith("+json") || + mime === "application/xml" || + mime.endsWith("+xml") || + mime === "application/javascript" || + mime === "application/x-javascript" +const outputMime = (format: Format) => + format === "markdown" ? "text/markdown" : format === "html" ? "text/html" : "text/plain" + +const convert = (content: string, contentType: string, format: Format) => { + if (!contentType.includes("text/html")) return content + if (format === "markdown") return convertHTMLToMarkdown(content) + if (format === "text") return extractTextFromHTML(content) + return content +} + +const definition = Tool.make({ + description, + parameters: Parameters, + success: Success, + toModelOutput: ({ output }) => [toolText({ type: "text", text: output.output })], +}) + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const http = yield* HttpClient.HttpClient + const resources = yield* ToolOutputStore.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, sessionID, call, assertPermission }) => + Effect.gen(function* () { + const parsed = new URL(parameters.url) + assertHttpUrl(parsed) + + yield* assertPermission({ action: name, resources: [parameters.url], save: ["*"], metadata: parameters }) + + const { body, contentType } = yield* Effect.gen(function* () { + const response = yield* execute(http, parameters.url, parameters.format).pipe( + Effect.catchIf(isCloudflareChallenge, () => execute(http, parameters.url, parameters.format, "opencode")), + ) + const contentType = response.headers["content-type"] || "" + const mime = mimeFrom(contentType) + if (isImageAttachment(mime)) throw new Error(`Unsupported fetched image content type: ${mime}`) + if (!isTextualMime(mime)) throw new Error(`Unsupported fetched file content type: ${mime}`) + return { body: yield* collectBody(response), contentType } + }).pipe( + Effect.timeoutOrElse({ + duration: Duration.seconds(parameters.timeout ?? DEFAULT_TIMEOUT_SECONDS), + orElse: () => Effect.die(new Error("Request timed out")), + }), + ) + const content = convert(new TextDecoder().decode(body), contentType, parameters.format) + const truncated = yield* resources.truncate({ + sessionID, + toolCallID: call.id, + content, + mime: outputMime(parameters.format), + }) + return { + url: parameters.url, + contentType, + format: parameters.format, + output: truncated.content, + truncated: truncated.truncated, + ...(truncated.truncated ? { resource: truncated.resource } : {}), + } + }).pipe( + Effect.catchCause((cause) => + Effect.fail( + new ToolFailure({ message: `Unable to fetch ${parameters.url}`, error: Cause.squash(cause) }), + ), + ), + ), + }), + ) + }), +) + +export function extractTextFromHTML(html: string) { + let text = "" + let skipDepth = 0 + const parser = new Parser({ + onopentag(name) { + if (skipDepth > 0 || ["script", "style", "noscript", "iframe", "object", "embed"].includes(name)) skipDepth++ + }, + ontext(input) { + if (skipDepth === 0) text += input + }, + onclosetag() { + if (skipDepth > 0) skipDepth-- + }, + }) + parser.write(html) + parser.end() + return text.trim() +} + +export function convertHTMLToMarkdown(html: string) { + const turndown = new TurndownService({ + headingStyle: "atx", + hr: "---", + bulletListMarker: "-", + codeBlockStyle: "fenced", + emDelimiter: "*", + }) + turndown.remove(["script", "style", "meta", "link"]) + return turndown.turndown(html) +} diff --git a/packages/core/src/tool/websearch.ts b/packages/core/src/tool/websearch.ts new file mode 100644 index 0000000000..d43f70afe4 --- /dev/null +++ b/packages/core/src/tool/websearch.ts @@ -0,0 +1,244 @@ +export * as WebSearchTool from "./websearch" + +import { Tool, ToolFailure, toolText } from "@opencode-ai/llm" +import { Cause, Context, Duration, Effect, Layer, Schema } from "effect" +import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { truthy } from "../flag/flag" +import { InstallationVersion } from "../installation/version" +import { PositiveInt } from "../schema" +import { ToolOutputStore } from "../tool-output-store" +import { ToolRegistry } from "../tool-registry" +import { checksum } from "../util/encode" + +export const name = "websearch" +export const NO_RESULTS = "No search results found. Please try a different query." +export const EXA_URL = "https://mcp.exa.ai/mcp" +export const PARALLEL_URL = "https://search.parallel.ai/mcp" +export const MAX_NUM_RESULTS = 20 +export const MAX_CONTEXT_CHARACTERS = 50_000 +export const MAX_RESPONSE_BYTES = 256 * 1024 + +/** + * Provider-independent local web search retained in V2 core for launch parity. + * This invokes the legacy Exa/Parallel product backends itself. It is distinct + * from provider-hosted web search tools, which remain route-owned and execute + * at the model provider. Ownership of this compromise can be revisited later. + */ +export const description = `Search the web using the session's local web search provider. Use this for current information beyond knowledge cutoff. + +This is a provider-independent local tool backed by Exa or Parallel. Provider-hosted web search tools are separate and execute at the model provider. + +Optional controls support result count, live crawling ('fallback' or 'preferred'), search type ('auto', 'fast', or 'deep'), and maximum context characters. + +The current year is ${new Date().getFullYear()}. Use this year when searching for recent information or current events.` + +export const Parameters = Schema.Struct({ + query: Schema.String.annotate({ description: "Websearch query" }), + numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({ description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})` }), + livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({ + description: + "Live crawl mode - 'fallback': use live crawling as backup if cached unavailable, 'preferred': prioritize live crawling (default: 'fallback')", + }), + type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({ + description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search", + }), + contextMaxCharacters: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate({ + description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`, + }), +}) + +export const Provider = Schema.Literals(["exa", "parallel"]) +export type Provider = typeof Provider.Type + +export interface Config { + readonly provider?: Provider + readonly enableExa: boolean + readonly enableParallel: boolean + readonly exaApiKey?: string + readonly parallelApiKey?: string +} + +export class ConfigService extends Context.Service()("@opencode/v2/WebSearchConfig") {} + +/** Isolates the retained product environment contract from the generic tool implementation. */ +export const defaultConfigLayer = Layer.sync(ConfigService, () => + ConfigService.of({ + provider: process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel" + ? process.env.OPENCODE_WEBSEARCH_PROVIDER + : undefined, + enableExa: + truthy("OPENCODE_EXPERIMENTAL") || + truthy("OPENCODE_ENABLE_EXA") || + truthy("OPENCODE_EXPERIMENTAL_EXA"), + enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"), + exaApiKey: process.env.EXA_API_KEY, + parallelApiKey: process.env.PARALLEL_API_KEY, + }), +) + +export function selectProvider( + sessionID: string, + flags: Pick = { enableExa: false, enableParallel: false }, + override?: Provider, +): Provider { + if (override) return override + if (flags.enableParallel) return "parallel" + if (flags.enableExa) return "exa" + return Number.parseInt(checksum(sessionID) ?? "0", 36) % 2 === 0 ? "exa" : "parallel" +} + +const McpResult = Schema.Struct({ + result: Schema.Struct({ + content: Schema.Array(Schema.Struct({ type: Schema.String, text: Schema.String })), + }), +}) +const decodeMcpResult = Schema.decodeUnknownEffect(Schema.fromJsonString(McpResult)) + +const parsePayload = (payload: string) => + Effect.gen(function* () { + const trimmed = payload.trim() + if (!trimmed.startsWith("{")) return undefined + return (yield* decodeMcpResult(trimmed)).result.content.find((item) => item.text)?.text + }) + +export const parseResponse = Effect.fn("WebSearchTool.parseResponse")(function* (body: string) { + const trimmed = body.trim() + const direct = trimmed ? yield* parsePayload(trimmed) : undefined + if (direct) return direct + for (const line of body.split("\n")) { + if (!line.startsWith("data: ")) continue + const data = yield* parsePayload(line.substring(6)) + if (data) return data + } + return undefined +}) + +const ExaArgs = Schema.Struct({ + query: Schema.String, + type: Schema.String, + numResults: Schema.Number, + livecrawl: Schema.String, + contextMaxCharacters: Schema.optional(Schema.Number), +}) +const ParallelArgs = Schema.Struct({ + objective: Schema.String, + search_queries: Schema.Array(Schema.String), + session_id: Schema.String, +}) +const McpRequest = (args: Schema.Struct) => + Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: Schema.Literal(1), + method: Schema.Literal("tools/call"), + params: Schema.Struct({ name: Schema.String, arguments: args }), + }) + +const exaUrl = (apiKey: string | undefined) => { + if (!apiKey) return EXA_URL + const url = new URL(EXA_URL) + url.searchParams.set("exaApiKey", apiKey) + return url.toString() +} + +const callMcp = ( + http: HttpClient.HttpClient, + url: string, + tool: string, + args: Schema.Struct, + value: Schema.Struct.Type, + headers: Record = {}, +) => + Effect.gen(function* () { + const request = yield* HttpClientRequest.post(url).pipe( + HttpClientRequest.accept("application/json, text/event-stream"), + HttpClientRequest.setHeaders(headers), + HttpClientRequest.schemaBodyJson(McpRequest(args))({ + jsonrpc: "2.0" as const, + id: 1 as const, + method: "tools/call" as const, + params: { name: tool, arguments: value }, + }), + ) + return yield* Effect.gen(function* () { + const response = yield* HttpClient.filterStatusOk(http).execute(request) + const body = yield* response.text + if (Buffer.byteLength(body, "utf8") > MAX_RESPONSE_BYTES) return yield* Effect.die(new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`)) + return yield* parseResponse(body) + }).pipe(Effect.timeoutOrElse({ duration: Duration.seconds(25), orElse: () => Effect.die(new Error(`${tool} request timed out`)) })) + }) + +const Success = Schema.Struct({ + provider: Provider, + text: Schema.String, + truncated: Schema.Boolean, + resource: ToolOutputStore.Resource.pipe(Schema.optional), +}) + +const definition = Tool.make({ + description, + parameters: Parameters, + success: Success, + toModelOutput: ({ output }) => [toolText({ type: "text", text: output.text })], +}) + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const http = yield* HttpClient.HttpClient + const config = yield* ConfigService + const resources = yield* ToolOutputStore.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, sessionID, call, assertPermission }) => { + const provider = selectProvider(sessionID, config, config.provider) + return Effect.gen(function* () { + yield* assertPermission({ + action: name, + resources: [parameters.query], + save: ["*"], + metadata: { ...parameters, provider }, + }) + + const text = provider === "exa" + ? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, { + query: parameters.query, + type: parameters.type || "auto", + numResults: parameters.numResults || 8, + livecrawl: parameters.livecrawl || "fallback", + contextMaxCharacters: parameters.contextMaxCharacters, + }) + : yield* callMcp( + http, + PARALLEL_URL, + "web_search", + ParallelArgs, + { + objective: parameters.query, + search_queries: [parameters.query], + session_id: sessionID, + // V2 invocation context does not safely expose the model yet. + }, + { + "User-Agent": `opencode/${InstallationVersion}`, + ...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}), + }, + ) + const truncated = yield* resources.truncate({ sessionID, toolCallID: call.id, content: text ?? NO_RESULTS }) + return { + provider, + text: truncated.content, + truncated: truncated.truncated, + ...(truncated.truncated ? { resource: truncated.resource } : {}), + } + }).pipe( + Effect.catchCause((cause) => + Effect.fail(new ToolFailure({ message: `Unable to search the web for ${parameters.query}`, error: Cause.squash(cause) })), + ), + ) + }, + }), + ) + }), +) diff --git a/packages/core/src/tool/write.ts b/packages/core/src/tool/write.ts new file mode 100644 index 0000000000..af5dab29e7 --- /dev/null +++ b/packages/core/src/tool/write.ts @@ -0,0 +1,78 @@ +/** + * Model-facing V2 file-write leaf. Relative paths resolve within the active + * Location. Absolute paths inside that Location are accepted, while explicit + * absolute external paths retain mutation capability through a separate + * external_directory approval before edit approval. Named project references + * are read-oriented and deliberately are not accepted by mutation tools. + */ +export * as WriteTool from "./write" + +import { Tool, ToolFailure, toolText } from "@opencode-ai/llm" +import { Cause, Effect, Layer, Schema } from "effect" +import { FileMutation } from "../file-mutation" +import { LocationMutation } from "../location-mutation" +import { ToolRegistry } from "../tool-registry" + +export const name = "write" + +// TODO: Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior. +export const Parameters = Schema.Struct({ + path: Schema.String.annotate({ + description: + "File path to write. Relative paths resolve within the active Location. Absolute paths inside that Location are accepted; external absolute paths require external_directory approval. Named project references are read-oriented and are not accepted.", + }), + content: Schema.String.annotate({ description: "Content to write to the file" }), +}) + +export const Success = Schema.Struct({ + operation: Schema.Literal("write"), + target: Schema.String, + resource: Schema.String, + existed: Schema.Boolean, +}) +export type Success = typeof Success.Type + +export const toModelOutput = (output: Success) => + `${output.existed ? "Wrote" : "Created"} file successfully: ${output.resource}` + +const definition = Tool.make({ + description: + "Write content to one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval. Named project references are read-oriented and are not accepted.", + parameters: Parameters, + success: Success, + toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })], +}) + +/** Deferred V2 write UX integrations remain visible at the model-facing seam. */ +// TODO: Add formatter integration after V2 formatter runtime exists. +// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +// TODO: Add snapshots / undo after design exists. +// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists. + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const mutation = yield* LocationMutation.Service + const files = yield* FileMutation.Service + + yield* registry.contribute((editor) => + editor.set(name, { + tool: definition, + execute: ({ parameters, assertPermission }) => + Effect.gen(function* () { + const plan = yield* mutation.resolve({ path: parameters.path, kind: "file" }) + const external = plan.target.externalDirectory + if (external) yield* assertPermission(LocationMutation.externalDirectoryPermission(external)) + yield* assertPermission({ action: "edit", resources: [plan.target.resource], save: ["*"] }) + return yield* files.writeTextPreservingBom({ plan, content: parameters.content }) + }).pipe( + Effect.catchCause((cause) => + Effect.fail( + new ToolFailure({ message: `Unable to write ${parameters.path}`, error: Cause.squash(cause) }), + ), + ), + ), + }), + ) + }), +) diff --git a/packages/core/src/util/hash.ts b/packages/core/src/util/hash.ts index 680e0f40bc..e8bf1beff9 100644 --- a/packages/core/src/util/hash.ts +++ b/packages/core/src/util/hash.ts @@ -4,4 +4,8 @@ export namespace Hash { export function fast(input: string | Buffer): string { return createHash("sha1").update(input).digest("hex") } + + export function sha256(input: string | Buffer): string { + return createHash("sha256").update(input).digest("hex") + } } diff --git a/packages/core/src/v1/session.ts b/packages/core/src/v1/session.ts index 0a972fd9d7..3a732e54da 100644 --- a/packages/core/src/v1/session.ts +++ b/packages/core/src/v1/session.ts @@ -12,6 +12,8 @@ import { NamedError } from "../util/error" import { SessionSchema } from "../session/schema" import { WorkspaceV2 } from "../workspace" +const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)) + export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe( Schema.brand("MessageID"), withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + Identifier.ascending()) })), @@ -329,7 +331,7 @@ export const User = Schema.Struct({ ...messageBase, role: Schema.Literal("user"), time: Schema.Struct({ - created: NonNegativeInt, + created: Timestamp, }), format: Schema.optional(Format), summary: Schema.optional( diff --git a/packages/core/test/agent.test.ts b/packages/core/test/agent.test.ts index 2aef203351..787b16a605 100644 --- a/packages/core/test/agent.test.ts +++ b/packages/core/test/agent.test.ts @@ -1,6 +1,10 @@ import { describe, expect } from "bun:test" import { Effect, Exit, Scope } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" +import { Location } from "@opencode-ai/core/location" +import { AgentPlugin } from "@opencode-ai/core/plugin/agent" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "./fixture/location" import { testEffect } from "./lib/effect" const it = testEffect(AgentV2.locationLayer) @@ -98,4 +102,22 @@ describe("AgentV2", () => { expect(yield* agent.get(id)).toBeUndefined() }), ) + + it.effect("does not ambiently opt built-in agents into bash", () => + Effect.gen(function* () { + const agent = yield* AgentV2.Service + yield* AgentPlugin.Plugin.effect.pipe( + Effect.provideService( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make("/project") })), + ), + ) + + const agents = yield* agent.all() + expect(agents.map((item) => String(item.id)).sort()).toEqual(["build", "compaction", "explore", "general", "plan", "summary", "title"]) + for (const item of agents) { + expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false) + } + }), + ) }) diff --git a/packages/core/test/background-job.test.ts b/packages/core/test/background-job.test.ts new file mode 100644 index 0000000000..1c4f93f019 --- /dev/null +++ b/packages/core/test/background-job.test.ts @@ -0,0 +1,103 @@ +import { describe, expect } from "bun:test" +import { BackgroundJob } from "@opencode-ai/core/background-job" +import { Deferred, Effect, Exit, Scope } from "effect" +import { it } from "./lib/effect" + +describe("BackgroundJob", () => { + it.live("tracks process-local work through explicit observation", () => + Effect.gen(function* () { + const jobs = yield* BackgroundJob.Service + const latch = yield* Deferred.make() + const job = yield* jobs.start({ + type: "test", + metadata: { durable: false }, + run: Deferred.await(latch).pipe(Effect.as("done")), + }) + + expect(job).toMatchObject({ type: "test", status: "running", metadata: { durable: false } }) + expect(yield* jobs.wait({ id: job.id, timeout: 0 })).toMatchObject({ + timedOut: true, + info: { status: "running" }, + }) + + yield* Deferred.succeed(latch, undefined) + expect(yield* jobs.wait({ id: job.id })).toMatchObject({ + timedOut: false, + info: { status: "completed", output: "done" }, + }) + }).pipe(Effect.provide(BackgroundJob.layer)), + ) + + it.live("publishes jobs before starting immediately settling work", () => + Effect.gen(function* () { + const jobs = yield* BackgroundJob.Service + + yield* Effect.forEach(Array.from({ length: 100 }), (_, index) => { + const id = `job_immediate_start_${index}` + return Effect.gen(function* () { + const job = yield* jobs.start({ + id, + type: "test", + run: jobs + .get(id) + .pipe( + Effect.flatMap((info) => + info?.status === "running" + ? Effect.succeed(`done-${index}`) + : Effect.fail("job started before publish"), + ), + ), + }) + + expect(yield* jobs.wait({ id: job.id })).toMatchObject({ + timedOut: false, + info: { status: "completed", output: `done-${index}` }, + }) + }) + }) + }).pipe(Effect.provide(BackgroundJob.layer)), + ) + + it.live("increments pending work before starting immediately settling extensions", () => + Effect.gen(function* () { + const jobs = yield* BackgroundJob.Service + + yield* Effect.forEach(Array.from({ length: 100 }), (_, index) => + Effect.gen(function* () { + const first = yield* Deferred.make() + const job = yield* jobs.start({ + type: "test", + run: Deferred.await(first).pipe(Effect.as(`first-${index}`)), + }) + + expect(yield* jobs.extend({ id: job.id, run: Effect.succeed(`second-${index}`) })).toBe(true) + expect((yield* jobs.get(job.id))?.status).toBe("running") + + yield* Deferred.succeed(first, undefined) + expect(yield* jobs.wait({ id: job.id })).toMatchObject({ + timedOut: false, + info: { status: "completed", output: `second-${index}` }, + }) + }), + ) + }).pipe(Effect.provide(BackgroundJob.layer)), + ) + + it.live("interrupts live work without promising settlement after the owning process-local scope closes", () => + Effect.gen(function* () { + const scope = yield* Scope.make() + const interrupted = yield* Deferred.make() + const jobs = yield* BackgroundJob.make.pipe(Scope.provide(scope)) + const job = yield* jobs.start({ + type: "test", + run: Effect.never.pipe(Effect.ensuring(Deferred.succeed(interrupted, undefined))), + }) + + yield* Scope.close(scope, Exit.void) + + yield* Deferred.await(interrupted).pipe(Effect.timeout("1 second")) + // The abandoned in-memory registry is not a durable observation channel. + expect((yield* jobs.get(job.id))?.status).toBe("running") + }), + ) +}) diff --git a/packages/core/test/config/skill.test.ts b/packages/core/test/config/skill.test.ts index a4841cb957..830adcb279 100644 --- a/packages/core/test/config/skill.test.ts +++ b/packages/core/test/config/skill.test.ts @@ -33,6 +33,7 @@ describe("ConfigSkillPlugin.Plugin", () => { Config.Service.of({ entries: () => Effect.succeed([ + new Config.Directory({ type: "directory", path: AbsolutePath.make("/repo/.opencode") }), new Config.Document({ type: "document", info: decode({ @@ -56,6 +57,14 @@ describe("ConfigSkillPlugin.Plugin", () => { ) expect(sources).toEqual([ + new SkillV2.DirectorySource({ + type: "directory", + path: AbsolutePath.make(path.join("/repo/.opencode", "skill")), + }), + new SkillV2.DirectorySource({ + type: "directory", + path: AbsolutePath.make(path.join("/repo/.opencode", "skills")), + }), new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }), new SkillV2.DirectorySource({ type: "directory", diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index 36d68f3f4d..6ed0b9b3a7 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -1,13 +1,15 @@ import { describe, expect, test } from "bun:test" import { $ } from "bun" import { fileURLToPath } from "url" +import path from "path" import { SqliteClient } from "@effect/sql-sqlite-bun" import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" -import { Effect } from "effect" +import { Effect, Layer } from "effect" import { eq, inArray, sql } from "drizzle-orm" import { DatabaseMigration } from "@opencode-ai/core/database/migration" import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage" import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration/20260601010001_normalize_storage_paths" +import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order" import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -15,6 +17,8 @@ import { SessionSchema } from "@opencode-ai/core/session/schema" import { SessionTable } from "@opencode-ai/core/session/sql" import sessionMetadataMigration from "@opencode-ai/core/database/migration/20260511173437_session-metadata" import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient" +import { Database } from "@opencode-ai/core/database/database" +import { tmpdir } from "./fixture/tmpdir" const run = (effect: Effect.Effect) => Effect.runPromise( @@ -24,6 +28,15 @@ const run = (effect: Effect.Effect) => const makeDb = EffectDrizzleSqlite.makeWithDefaults() describe("DatabaseMigration", () => { + test("serializes concurrent embedded initialization for one database path", async () => { + await using tmp = await tmpdir() + const filename = path.join(tmp.path, "embedded.sqlite") + const layers = [Database.layerFromPath(filename), Database.layerFromPath(filename)] + + await Effect.runPromise( + Effect.all(layers.map((layer) => Effect.scoped(Layer.build(layer))), { concurrency: "unbounded" }), + ) + }) if (process.platform === "linux") { test("declared schema has no ungenerated migrations", async () => { const result = await $`bun ${fileURLToPath(new URL("../script/migration.ts", import.meta.url))} --check` @@ -43,11 +56,74 @@ describe("DatabaseMigration", () => { expect(yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`)).toEqual({ name: "session", }) - expect(yield* db.get(sql`SELECT count(*) as count FROM migration`)).toEqual({ count: 25 }) + expect( + yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_input'`), + ).toEqual({ name: "session_input" }) + expect(yield* db.get(sql`SELECT count(*) as count FROM migration`)).toEqual({ count: 29 }) + expect( + yield* db.all( + sql`SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('event_aggregate_seq_idx', 'event_aggregate_type_seq_idx', 'session_input_session_pending_seq_idx', 'session_input_session_pending_delivery_seq_idx', 'session_message_session_idx', 'session_message_session_type_idx', 'session_message_session_seq_idx', 'session_message_session_type_seq_idx', 'session_message_session_time_created_id_idx') ORDER BY name`, + ), + ).toEqual([ + { name: "event_aggregate_seq_idx" }, + { name: "event_aggregate_type_seq_idx" }, + { name: "session_input_session_pending_delivery_seq_idx" }, + { name: "session_message_session_seq_idx" }, + { name: "session_message_session_time_created_id_idx" }, + { name: "session_message_session_type_seq_idx" }, + ]) }), ) }) + test("backfills projected Session message order from durable event sequence", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run(sql`CREATE TABLE event (id text PRIMARY KEY, seq integer NOT NULL)`) + yield* db.run( + sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, time_created integer NOT NULL, data text NOT NULL)`, + ) + yield* db.run( + sql`CREATE INDEX session_message_session_time_created_id_idx ON session_message (session_id, time_created, id)`, + ) + yield* db.run( + sql`CREATE INDEX session_message_session_type_time_created_id_idx ON session_message (session_id, type, time_created, id)`, + ) + yield* db.run(sql`INSERT INTO event (id, seq) VALUES ('evt_z', 0), ('evt_a', 1)`) + yield* db.run( + sql`INSERT INTO session_message (id, session_id, type, time_created, data) VALUES ('evt_z', 'session', 'user', 0, '{}'), ('evt_a', 'session', 'user', 0, '{}')`, + ) + + yield* DatabaseMigration.applyOnly(db, [sessionMessageProjectionOrderMigration]) + + expect(yield* db.all(sql`SELECT id, seq FROM session_message ORDER BY seq`)).toEqual([ + { id: "evt_z", seq: 0 }, + { id: "evt_a", seq: 1 }, + ]) + }), + ) + }) + + test("fails projected Session message order backfill without a durable event", async () => { + await expect( + run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run(sql`CREATE TABLE event (id text PRIMARY KEY, seq integer NOT NULL)`) + yield* db.run( + sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, time_created integer NOT NULL, data text NOT NULL)`, + ) + yield* db.run( + sql`INSERT INTO session_message (id, session_id, type, time_created, data) VALUES ('evt_missing', 'session', 'user', 0, '{}')`, + ) + + yield* DatabaseMigration.applyOnly(db, [sessionMessageProjectionOrderMigration]) + }), + ), + ).rejects.toThrow("Cannot migrate session_message projections without matching durable events") + }) + test("runs session usage backfill in order with schema changes", async () => { await run( Effect.gen(function* () { diff --git a/packages/core/test/effect/keyed-mutex.test.ts b/packages/core/test/effect/keyed-mutex.test.ts new file mode 100644 index 0000000000..e4fb587344 --- /dev/null +++ b/packages/core/test/effect/keyed-mutex.test.ts @@ -0,0 +1,69 @@ +import { describe, expect } from "bun:test" +import { Deferred, Effect, Fiber } from "effect" +import { KeyedMutex } from "@opencode-ai/core/effect/keyed-mutex" +import { it } from "../lib/effect" + +describe("KeyedMutex", () => { + it.effect("serializes effects with the same key", () => + Effect.gen(function* () { + const mutex = yield* KeyedMutex.make() + const firstStarted = yield* Deferred.make() + const releaseFirst = yield* Deferred.make() + const secondStarted = yield* Deferred.make() + + const first = yield* mutex + .withLock("shared")(Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst)))) + .pipe(Effect.forkChild) + yield* Deferred.await(firstStarted) + const second = yield* mutex.withLock("shared")(Deferred.succeed(secondStarted, undefined)).pipe(Effect.forkChild) + yield* Effect.yieldNow + expect(yield* Deferred.isDone(secondStarted)).toBe(false) + + yield* Deferred.succeed(releaseFirst, undefined) + yield* Fiber.join(first) + yield* Fiber.join(second) + expect(yield* mutex.size).toBe(0) + }), + ) + + it.effect("allows different keys to proceed independently", () => + Effect.gen(function* () { + const mutex = yield* KeyedMutex.make() + const firstStarted = yield* Deferred.make() + const releaseFirst = yield* Deferred.make() + const secondFinished = yield* Deferred.make() + + const first = yield* mutex + .withLock("first")(Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst)))) + .pipe(Effect.forkChild) + yield* Deferred.await(firstStarted) + yield* mutex.withLock("second")(Deferred.succeed(secondFinished, undefined)) + expect(yield* Deferred.isDone(secondFinished)).toBe(true) + + yield* Deferred.succeed(releaseFirst, undefined) + yield* Fiber.join(first) + expect(yield* mutex.size).toBe(0) + }), + ) + + it.effect("removes an interrupted waiter without dropping the holder lock", () => + Effect.gen(function* () { + const mutex = yield* KeyedMutex.make() + const firstStarted = yield* Deferred.make() + const releaseFirst = yield* Deferred.make() + + const first = yield* mutex + .withLock("shared")(Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst)))) + .pipe(Effect.forkChild) + yield* Deferred.await(firstStarted) + const interrupted = yield* mutex.withLock("shared")(Effect.void).pipe(Effect.forkChild) + yield* Effect.yieldNow + yield* Fiber.interrupt(interrupted) + expect(yield* mutex.size).toBe(1) + + yield* Deferred.succeed(releaseFirst, undefined) + yield* Fiber.join(first) + expect(yield* mutex.size).toBe(0) + }), + ) +}) diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts index c3e5d2d75a..06c40a5ffb 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/event.test.ts @@ -1,17 +1,19 @@ import { describe, expect } from "bun:test" -import { Effect, Fiber, Layer, Schema, Stream } from "effect" +import { DateTime, Deferred, Effect, Fiber, Layer, Schema, Stream } from "effect" import { EventV2 } from "@opencode-ai/core/event" import { Database } from "@opencode-ai/core/database/database" import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql" import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" +import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { V2Schema } from "@opencode-ai/core/v2-schema" import { eq } from "drizzle-orm" import { location } from "./fixture/location" import { testEffect } from "./lib/effect" const locationLayer = Layer.succeed( Location.Service, - Location.Service.of(location({ directory: AbsolutePath.make("project"), workspaceID: "workspace" })), + Location.Service.of(location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") })), ) const eventLayer = Layer.mergeAll(EventV2.defaultLayer, Database.defaultLayer) const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer))) @@ -67,7 +69,30 @@ const VersionedMessage = EventV2.define({ }, }) +const SyncTimestamp = EventV2.define({ + type: "test.timestamp", + sync: { + version: 1, + aggregate: "id", + }, + schema: { + id: Schema.String, + timestamp: V2Schema.DateTimeUtcFromMillis, + }, +}) + describe("EventV2", () => { + it.effect("derives stable namespaced external IDs", () => + Effect.sync(() => { + const input = { namespace: "opencord.agent-input", key: "input-1" } + + expect(EventV2.ID.fromExternal(input)).toBe(EventV2.ID.fromExternal(input)) + expect(EventV2.ID.fromExternal(input)).toMatch(/^evt_[a-f0-9]{64}$/) + expect(EventV2.ID.fromExternal({ ...input, namespace: "another-app" })).not.toBe(EventV2.ID.fromExternal(input)) + expect(EventV2.ID.fromExternal({ namespace: "a:b", key: "c" })).not.toBe(EventV2.ID.fromExternal({ namespace: "a", key: "b:c" })) + }), + ) + it.effect("publishes events with the current location", () => Effect.gen(function* () { const events = yield* EventV2.Service @@ -80,7 +105,7 @@ describe("EventV2", () => { expect(event.type).toBe("test.message") expect(event).not.toHaveProperty("version") expect(event.data).toEqual({ text: "hello" }) - expect(event.location).toEqual({ directory: AbsolutePath.make("project"), workspaceID: "workspace" }) + expect(event.location).toEqual({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }) }), ) @@ -204,6 +229,24 @@ describe("EventV2", () => { }), ) + it.effect("does not synchronize live-only events", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const synchronized = new Array() + const unsubscribe = yield* events.sync((event) => + Effect.sync(() => { + synchronized.push(event.type) + }), + ) + yield* Effect.addFinalizer(() => unsubscribe) + + yield* events.publish(Message, { text: "live only" }) + yield* events.publish(SyncMessage, { id: "one", text: "durable" }) + + expect(synchronized).toEqual([SyncMessage.type]) + }), + ) + it.effect("inserts sync event rows on publish", () => Effect.gen(function* () { const events = yield* EventV2.Service @@ -243,6 +286,102 @@ describe("EventV2", () => { }), ) + it.effect("replays durable aggregate events after a cursor and tails new events", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const aggregateID = EventV2.ID.create() + yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" }) + yield* events.publish(SyncMessage, { id: aggregateID, text: "one" }) + const fiber = yield* events.aggregateEvents({ aggregateID, after: EventV2.Cursor.make(0) }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) + yield* Effect.yieldNow + + yield* events.publish(SyncMessage, { id: aggregateID, text: "two" }) + + expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual([ + [EventV2.Cursor.make(1), { id: aggregateID, text: "one" }], + [EventV2.Cursor.make(2), { id: aggregateID, text: "two" }], + ]) + }), + ) + + it.effect("catches durable aggregate events published during replay handoff", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const aggregateID = EventV2.ID.create() + yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" }) + const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) + + yield* events.publish(SyncMessage, { id: aggregateID, text: "one" }) + + expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, (event.event.data as { text: string }).text])).toEqual([ + [EventV2.Cursor.make(0), "zero"], + [EventV2.Cursor.make(1), "one"], + ]) + }), + ) + + it.effect("retains a durable wake committed while historical replay is paused", () => + Effect.gen(function* () { + const readStarted = yield* Deferred.make() + const continueRead = yield* Deferred.make() + let pause = true + const database = Database.layerFromPath(":memory:") + const eventLayer = EventV2.layerWith({ + beforeAggregateRead: () => + pause + ? Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(continueRead))) + : Effect.void, + }).pipe(Layer.provide(database)) + + yield* Effect.gen(function* () { + const events = yield* EventV2.Service + const aggregateID = EventV2.ID.create() + const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + yield* Deferred.await(readStarted) + + pause = false + yield* events.publish(SyncMessage, { id: aggregateID, text: "during handoff" }) + yield* Deferred.succeed(continueRead, undefined) + + expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual([ + [EventV2.Cursor.make(0), { id: aggregateID, text: "during handoff" }], + ]) + }).pipe(Effect.provide(Layer.mergeAll(database, eventLayer))) + }), + ) + + it.effect("coalesces durable aggregate wakes while draining every committed event", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const aggregateID = EventV2.ID.create() + const count = 64 + const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped) + yield* Effect.yieldNow + + for (let index = 0; index < count; index++) { + yield* events.publish(SyncMessage, { id: aggregateID, text: String(index) }) + } + + expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual( + Array.from({ length: count }, (_, index) => [EventV2.Cursor.make(index), { id: aggregateID, text: String(index) }]), + ) + }), + ) + + it.effect("omits live-only events from durable aggregate streams", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const aggregateID = EventV2.ID.create() + const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + yield* Effect.yieldNow + + yield* events.publish(Message, { text: "live only" }) + yield* events.publish(SyncMessage, { id: aggregateID, text: "durable" }) + + expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.event.type)).toEqual([SyncMessage.type]) + }), + ) + it.effect("uses custom sync aggregate field", () => Effect.gen(function* () { const events = yield* EventV2.Service @@ -311,6 +450,49 @@ describe("EventV2", () => { }), ) + it.effect("replay rejects an envelope aggregate that differs from its payload without mutating the payload aggregate", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const { db } = yield* Database.Service + const envelopeAggregateID = EventV2.ID.create() + const payloadAggregateID = EventV2.ID.create() + const received = new Array() + yield* events.publish(SyncMessage, { id: payloadAggregateID, text: "seed" }) + yield* events.project(SyncMessage, (event) => + Effect.sync(() => { + received.push(event) + }), + ) + + const exit = yield* events + .replay({ + id: EventV2.ID.create(), + type: EventV2.versionedType(SyncMessage.type, 1), + seq: 1, + aggregateID: envelopeAggregateID, + data: { id: payloadAggregateID, text: "replayed" }, + }) + .pipe(Effect.exit) + const rows = yield* db + .select() + .from(EventTable) + .where(eq(EventTable.aggregate_id, payloadAggregateID)) + .all() + .pipe(Effect.orDie) + const sequence = yield* db + .select({ seq: EventSequenceTable.seq }) + .from(EventSequenceTable) + .where(eq(EventSequenceTable.aggregate_id, payloadAggregateID)) + .get() + .pipe(Effect.orDie) + + expect(String(exit)).toContain("Aggregate mismatch") + expect(received).toHaveLength(0) + expect(rows).toHaveLength(1) + expect(sequence).toEqual({ seq: 0 }) + }), + ) + it.effect("replay defects on sequence mismatch", () => Effect.gen(function* () { const events = yield* EventV2.Service @@ -337,6 +519,29 @@ describe("EventV2", () => { }), ) + it.effect("replay decodes synchronized transformed values before projection", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const aggregateID = EventV2.ID.create() + const received = new Array() + yield* events.project(SyncTimestamp, (event) => + Effect.sync(() => { + received.push(event) + }), + ) + + yield* events.replay({ + id: EventV2.ID.create(), + type: EventV2.versionedType(SyncTimestamp.type, 1), + seq: 0, + aggregateID, + data: { id: aggregateID, timestamp: 0 }, + }) + + expect(received[0]?.data.timestamp).toEqual(DateTime.makeUnsafe(0)) + }), + ) + it.effect("replay defects on unknown event type", () => Effect.gen(function* () { const events = yield* EventV2.Service @@ -485,11 +690,109 @@ describe("EventV2", () => { }), ) + it.effect("replay claims an existing unowned sequence before fencing a different owner", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const { db } = yield* Database.Service + const aggregateID = EventV2.ID.create() + yield* events.publish(SyncMessage, { id: aggregateID, text: "local" }) + + yield* events.replay( + { + id: EventV2.ID.create(), + type: EventV2.versionedType(SyncMessage.type, 1), + seq: 1, + aggregateID, + data: { id: aggregateID, text: "claimed" }, + }, + { ownerID: "owner-1" }, + ) + yield* events.replay( + { + id: EventV2.ID.create(), + type: EventV2.versionedType(SyncMessage.type, 1), + seq: 2, + aggregateID, + data: { id: aggregateID, text: "fenced" }, + }, + { ownerID: "owner-2" }, + ) + const rows = yield* db + .select() + .from(EventTable) + .where(eq(EventTable.aggregate_id, aggregateID)) + .all() + .pipe(Effect.orDie) + const sequence = yield* db + .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id }) + .from(EventSequenceTable) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .get() + .pipe(Effect.orDie) + + expect(rows.map((row) => row.seq)).toEqual([0, 1]) + expect(sequence).toEqual({ seq: 1, ownerID: "owner-1" }) + }), + ) + + it.effect("strict replay rejects an owner conflict instead of silently skipping it", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const aggregateID = EventV2.ID.create() + yield* events.replay( + { + id: EventV2.ID.create(), + type: EventV2.versionedType(SyncMessage.type, 1), + seq: 0, + aggregateID, + data: { id: aggregateID, text: "claimed" }, + }, + { ownerID: "owner-1" }, + ) + + const exit = yield* events.replay( + { + id: EventV2.ID.create(), + type: EventV2.versionedType(SyncMessage.type, 1), + seq: 1, + aggregateID, + data: { id: aggregateID, text: "conflict" }, + }, + { ownerID: "owner-2", strictOwner: true }, + ).pipe(Effect.exit) + + expect(String(exit)).toContain("Replay owner mismatch") + }), + ) + + it.effect("publishes accepted replay with its durable sequence and suppresses stale replay", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const received = new Array() + const aggregateID = EventV2.ID.create() + yield* events.listen((event) => Effect.sync(() => received.push(event))) + const replayed = { + id: EventV2.ID.create(), + type: EventV2.versionedType(SyncMessage.type, 1), + seq: 0, + aggregateID, + data: { id: aggregateID, text: "replayed" }, + } + + yield* events.replay(replayed, { publish: true }) + yield* events.replay(replayed, { publish: true }) + + expect(received).toMatchObject([{ id: replayed.id, seq: 0, data: replayed.data }]) + }), + ) + it.effect("replay from a different owner leaves claimed sequence unchanged", () => Effect.gen(function* () { const events = yield* EventV2.Service const { db } = yield* Database.Service const aggregateID = EventV2.ID.create() + const received = new Array() + yield* events.listen((event) => Effect.sync(() => received.push(event))) yield* events.replay( { @@ -509,7 +812,7 @@ describe("EventV2", () => { aggregateID, data: { id: aggregateID, text: "ignored" }, }, - { ownerID: "owner-2" }, + { ownerID: "owner-2", publish: true }, ) const rows = yield* db .select() @@ -526,6 +829,7 @@ describe("EventV2", () => { expect(rows).toHaveLength(1) expect(sequence).toEqual({ seq: 0, ownerID: "owner-1" }) + expect(received).toHaveLength(0) }), ) diff --git a/packages/core/test/file-mutation.test.ts b/packages/core/test/file-mutation.test.ts new file mode 100644 index 0000000000..72b7608c9a --- /dev/null +++ b/packages/core/test/file-mutation.test.ts @@ -0,0 +1,315 @@ +import fs from "fs/promises" +import path from "path" +import { describe, expect } from "bun:test" +import { Deferred, Effect, Fiber, Layer } from "effect" +import { FileMutation } from "@opencode-ai/core/file-mutation" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Location } from "@opencode-ai/core/location" +import { LocationMutation } from "@opencode-ai/core/location-mutation" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "./fixture/location" +import { tmpdir } from "./fixture/tmpdir" +import { it } from "./lib/effect" + +function provide(directory: string, filesystem = FSUtil.defaultLayer) { + const activeLocation = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make(directory) })), + ) + const planning = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation)) + const commits = FileMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(planning)) + return Effect.provide(Layer.mergeAll(planning, commits)) +} + +function withTmp(f: (directory: string) => Effect.Effect) { + return Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe(Effect.flatMap((tmp) => f(tmp.path))) +} + +describe("FileMutation", () => { + it.live("writes an existing internal file and returns a stable result", () => + withTmp((directory) => + Effect.gen(function* () { + const targetPath = path.join(directory, "hello.txt") + yield* Effect.promise(() => fs.writeFile(targetPath, "before")) + const plan = yield* (yield* LocationMutation.Service).resolve({ path: "hello.txt" }) + + expect(yield* (yield* FileMutation.Service).write({ plan, content: "after" })).toEqual({ + operation: "write", + target: plan.target.canonical, + resource: "hello.txt", + existed: true, + }) + expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("after") + }).pipe(provide(directory)), + ), + ) + + it.live("writes a prospective internal file and creates parent directories", () => + withTmp((directory) => + Effect.gen(function* () { + const plan = yield* (yield* LocationMutation.Service).resolve({ path: path.join("src", "nested", "hello.txt") }) + const result = yield* (yield* FileMutation.Service).write({ plan, content: "hello" }) + + expect(result).toEqual({ + operation: "write", + target: plan.target.canonical, + resource: "src/nested/hello.txt", + existed: false, + }) + expect(yield* Effect.promise(() => fs.readFile(result.target, "utf8"))).toBe("hello") + }).pipe(provide(directory)), + ), + ) + + it.live("preserves exactly one BOM for text writes and normalizes created text", () => + withTmp((directory) => + Effect.gen(function* () { + const preservedPath = path.join(directory, "preserved.txt") + yield* Effect.promise(() => fs.writeFile(preservedPath, "\uFEFFbefore")) + const preserved = yield* (yield* LocationMutation.Service).resolve({ path: "preserved.txt" }) + const created = yield* (yield* LocationMutation.Service).resolve({ path: "created.txt" }) + const files = yield* FileMutation.Service + + yield* files.writeTextPreservingBom({ plan: preserved, content: "\uFEFFafter" }) + yield* files.writeTextPreservingBom({ plan: created, content: "\uFEFF\uFEFF\uFEFFcreated" }) + + expect(yield* Effect.promise(() => fs.readFile(preservedPath, "utf8"))).toBe("\uFEFFafter") + expect(yield* Effect.promise(() => fs.readFile(created.target.canonical, "utf8"))).toBe("\uFEFFcreated") + }).pipe(provide(directory)), + ), + ) + + it.live("rejects create when a prospective target appears after planning", () => + withTmp((directory) => + Effect.gen(function* () { + const targetPath = path.join(directory, "appeared.txt") + const plan = yield* (yield* LocationMutation.Service).resolve({ path: "appeared.txt" }) + yield* Effect.promise(() => fs.writeFile(targetPath, "winner")) + + expect(yield* (yield* FileMutation.Service).create({ plan, content: "replacement" }).pipe(Effect.flip)).toMatchObject({ + _tag: "LocationMutation.RevalidationError", + }) + expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("winner") + }).pipe(provide(directory)), + ), + ) + + it.live("removes an existing internal file", () => + withTmp((directory) => + Effect.gen(function* () { + const targetPath = path.join(directory, "remove.txt") + yield* Effect.promise(() => fs.writeFile(targetPath, "remove")) + const plan = yield* (yield* LocationMutation.Service).resolve({ path: "remove.txt" }) + const result = yield* (yield* FileMutation.Service).remove({ plan }) + + expect(result).toEqual({ operation: "remove", target: plan.target.canonical, resource: "remove.txt", existed: true }) + expect(yield* Effect.promise(() => fs.stat(targetPath).then(() => true, () => false))).toBe(false) + }).pipe(provide(directory)), + ), + ) + + it.live("writes an explicitly planned external target", () => + withTmp((directory) => + withTmp((outside) => + Effect.gen(function* () { + const targetPath = path.join(outside, "external.txt") + const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath }) + const result = yield* (yield* FileMutation.Service).write({ plan, content: "external" }) + + expect(result).toEqual({ operation: "write", target: plan.target.canonical, resource: plan.target.resource, existed: false }) + expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("external") + }).pipe(provide(directory)), + ), + ), + ) + + it.live("removes an explicitly planned external target", () => + withTmp((directory) => + withTmp((outside) => + Effect.gen(function* () { + const targetPath = path.join(outside, "external.txt") + yield* Effect.promise(() => fs.writeFile(targetPath, "external")) + const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath }) + const result = yield* (yield* FileMutation.Service).remove({ plan }) + + expect(result).toEqual({ operation: "remove", target: plan.target.canonical, resource: plan.target.resource, existed: true }) + expect(yield* Effect.promise(() => fs.stat(targetPath).then(() => true, () => false))).toBe(false) + }).pipe(provide(directory)), + ), + ), + ) + + it.live("propagates revalidation rejection after an ancestor swap", () => + withTmp((directory) => + withTmp((outside) => + Effect.gen(function* () { + if (process.platform === "win32") return + const parent = path.join(directory, "parent") + yield* Effect.promise(() => fs.mkdir(parent)) + const plan = yield* (yield* LocationMutation.Service).resolve({ path: path.join("parent", "new.txt") }) + yield* Effect.promise(async () => { + await fs.rmdir(parent) + await fs.symlink(outside, parent) + }) + + expect(yield* (yield* FileMutation.Service).write({ plan, content: "escape" }).pipe(Effect.flip)).toMatchObject({ + _tag: "LocationMutation.RevalidationError", + }) + expect(yield* Effect.promise(() => fs.stat(path.join(outside, "new.txt")).then(() => true, () => false))).toBe(false) + }).pipe(provide(directory)), + ), + ), + ) + + it.live("serializes concurrent writes to the same canonical target", () => + withTmp((directory) => + Effect.gen(function* () { + const targetPath = path.join(directory, "shared.txt") + yield* Effect.promise(() => fs.writeFile(targetPath, "initial")) + const firstStarted = yield* Deferred.make() + const releaseFirst = yield* Deferred.make() + const secondStarted = yield* Deferred.make() + let writes = 0 + const filesystem = instrumentWrites((write) => + Effect.gen(function* () { + writes++ + if (writes === 1) { + yield* Deferred.succeed(firstStarted, undefined) + yield* Deferred.await(releaseFirst) + } else { + yield* Deferred.succeed(secondStarted, undefined) + } + yield* write + }), + ) + + yield* Effect.gen(function* () { + const mutation = yield* LocationMutation.Service + const files = yield* FileMutation.Service + const firstPlan = yield* mutation.resolve({ path: "shared.txt" }) + const secondPlan = yield* mutation.resolve({ path: "shared.txt" }) + const first = yield* files.write({ plan: firstPlan, content: "first" }).pipe(Effect.forkChild) + yield* Deferred.await(firstStarted) + const second = yield* files.write({ plan: secondPlan, content: "second" }).pipe(Effect.forkChild) + yield* Effect.yieldNow + expect(yield* Deferred.isDone(secondStarted)).toBe(false) + + yield* Deferred.succeed(releaseFirst, undefined) + yield* Deferred.await(secondStarted) + yield* Fiber.join(first) + yield* Fiber.join(second) + expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("second") + }).pipe(provide(directory, filesystem)) + }), + ), + ) + + it.live("allows only one concurrent conditional write based on the same bytes", () => + withTmp((directory) => + Effect.gen(function* () { + const targetPath = path.join(directory, "shared.txt") + yield* Effect.promise(() => fs.writeFile(targetPath, "initial")) + const firstStarted = yield* Deferred.make() + const releaseFirst = yield* Deferred.make() + let writes = 0 + const filesystem = instrumentWrites((write) => + Effect.gen(function* () { + writes++ + if (writes === 1) { + yield* Deferred.succeed(firstStarted, undefined) + yield* Deferred.await(releaseFirst) + } + yield* write + }), + ) + + yield* Effect.gen(function* () { + const mutation = yield* LocationMutation.Service + const files = yield* FileMutation.Service + const plan = yield* mutation.resolve({ path: "shared.txt" }) + const expected = new TextEncoder().encode("initial") + const first = yield* files.writeIfUnchanged({ plan, expected, content: "first" }).pipe(Effect.forkChild) + yield* Deferred.await(firstStarted) + const second = yield* files.writeIfUnchanged({ plan, expected, content: "second" }).pipe(Effect.flip, Effect.forkChild) + + yield* Deferred.succeed(releaseFirst, undefined) + yield* Fiber.join(first) + expect(yield* Fiber.join(second)).toMatchObject({ _tag: "FileMutation.StaleContentError" }) + expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("first") + expect(writes).toBe(1) + }).pipe(provide(directory, filesystem)) + }), + ), + ) + + it.live("rejects a conditional write when target content is already stale", () => + withTmp((directory) => + Effect.gen(function* () { + const targetPath = path.join(directory, "stale.txt") + yield* Effect.promise(() => fs.writeFile(targetPath, "current")) + const plan = yield* (yield* LocationMutation.Service).resolve({ path: "stale.txt" }) + + expect( + yield* (yield* FileMutation.Service) + .writeIfUnchanged({ plan, expected: new TextEncoder().encode("older"), content: "replacement" }) + .pipe(Effect.flip), + ).toMatchObject({ _tag: "FileMutation.StaleContentError", path: plan.target.canonical }) + expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("current") + }).pipe(provide(directory)), + ), + ) + + it.live("allows distinct canonical targets to proceed independently", () => + withTmp((directory) => + Effect.gen(function* () { + const firstStarted = yield* Deferred.make() + const releaseFirst = yield* Deferred.make() + const secondFinished = yield* Deferred.make() + const secondPath = path.join(directory, "second.txt") + let writes = 0 + const filesystem = instrumentWrites((write) => + ++writes === 1 + ? Deferred.succeed(firstStarted, undefined).pipe( + Effect.andThen(Deferred.await(releaseFirst)), + Effect.andThen(write), + ) + : write.pipe(Effect.andThen(Deferred.succeed(secondFinished, undefined))), + ) + + yield* Effect.gen(function* () { + const mutation = yield* LocationMutation.Service + const files = yield* FileMutation.Service + const firstPlan = yield* mutation.resolve({ path: "first.txt" }) + const secondPlan = yield* mutation.resolve({ path: "second.txt" }) + const first = yield* files.write({ plan: firstPlan, content: "first" }).pipe(Effect.forkChild) + yield* Deferred.await(firstStarted) + const second = yield* files.write({ plan: secondPlan, content: "second" }).pipe(Effect.forkChild) + yield* Deferred.await(secondFinished) + expect(yield* Effect.promise(() => fs.readFile(secondPath, "utf8"))).toBe("second") + + yield* Deferred.succeed(releaseFirst, undefined) + yield* Fiber.join(first) + yield* Fiber.join(second) + }).pipe(provide(directory, filesystem)) + }), + ), + ) +}) + +function instrumentWrites( + run: (write: Effect.Effect, target: string) => Effect.Effect, +) { + return Layer.effect( + FSUtil.Service, + Effect.gen(function* () { + const filesystem = yield* FSUtil.Service + return FSUtil.Service.of({ + ...filesystem, + writeWithDirs: (target, content, mode) => run(filesystem.writeWithDirs(target, content, mode), target), + }) + }), + ).pipe(Layer.provide(FSUtil.defaultLayer)) +} diff --git a/packages/core/test/filesystem/filesystem.test.ts b/packages/core/test/filesystem/filesystem.test.ts index 9774044cf5..635148ba64 100644 --- a/packages/core/test/filesystem/filesystem.test.ts +++ b/packages/core/test/filesystem/filesystem.test.ts @@ -354,13 +354,18 @@ describe("FSUtil", () => { test("contains checks path containment", () => { expect(FSUtil.contains("/a/b", "/a/b/c")).toBe(true) + expect(FSUtil.contains("/a/b", "/a/b")).toBe(true) expect(FSUtil.contains("/a/b", "/a/c")).toBe(false) + expect(FSUtil.contains("/a/b", "/a/bad")).toBe(false) + if (process.platform === "win32") expect(FSUtil.contains("C:\\a", "D:\\b")).toBe(false) }) test("overlaps detects overlapping paths", () => { expect(FSUtil.overlaps("/a/b", "/a/b/c")).toBe(true) expect(FSUtil.overlaps("/a/b/c", "/a/b")).toBe(true) expect(FSUtil.overlaps("/a", "/b")).toBe(false) + expect(FSUtil.overlaps("/a/b", "/a/bad")).toBe(false) + if (process.platform === "win32") expect(FSUtil.overlaps("C:\\a", "D:\\b")).toBe(false) }) }) }) diff --git a/packages/core/test/filesystem/ripgrep.test.ts b/packages/core/test/filesystem/ripgrep.test.ts index f141519a8a..56ba598988 100644 --- a/packages/core/test/filesystem/ripgrep.test.ts +++ b/packages/core/test/filesystem/ripgrep.test.ts @@ -49,6 +49,17 @@ const withRipgrepConfig = (value: string, effect: Effect.Effect { + it.live("exposes a cached managed executable filepath", () => + Effect.gen(function* () { + const ripgrep = yield* Ripgrep.Service + const first = yield* ripgrep.filepath + const second = yield* ripgrep.filepath + + expect(first).toBe(second) + expect((yield* Effect.promise(() => fs.stat(first))).isFile()).toBe(true) + }), + ) + it.live("defaults to include hidden", () => Effect.gen(function* () { const dir = yield* tmpdir((dir) => diff --git a/packages/core/test/fixture/tmpdir.ts b/packages/core/test/fixture/tmpdir.ts index 950b1401b6..7ee31a002b 100644 --- a/packages/core/test/fixture/tmpdir.ts +++ b/packages/core/test/fixture/tmpdir.ts @@ -3,11 +3,22 @@ import { tmpdir as osTmpdir } from "os" import path from "path" export const tmpdir = async () => { - const dir = await fs.mkdtemp(path.join(osTmpdir(), "opencode-core-test-")) + const dir = await fs.realpath(await fs.mkdtemp(path.join(osTmpdir(), "opencode-core-test-"))) return { path: dir, async [Symbol.asyncDispose]() { - await fs.rm(dir, { recursive: true, force: true }) + await remove(dir) }, } } + +async function remove(dir: string, retries = 10): Promise { + try { + await fs.rm(dir, { recursive: true, force: true }) + } catch (error) { + if (retries === 0 || !error || typeof error !== "object" || !("code" in error) || error.code !== "EBUSY") + throw error + await Bun.sleep(100) + return remove(dir, retries - 1) + } +} diff --git a/packages/core/test/fixtures/recordings/session-runner/openai-chat-streams-text.json b/packages/core/test/fixtures/recordings/session-runner/openai-chat-streams-text.json new file mode 100644 index 0000000000..bad659f93e --- /dev/null +++ b/packages/core/test/fixtures/recordings/session-runner/openai-chat-streams-text.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "metadata": { + "name": "session-runner/openai-chat-streams-text", + "recordedAt": "2026-06-02T19:52:25.084Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in one short sentence.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":20,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "data: {\"id\":\"chatcmpl-DmPRwO9SjY0GJZ3TFFe1Be72wysEG\",\"object\":\"chat.completion.chunk\",\"created\":1780429944,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_40bf7dabb5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"f3yrdno80\"}\n\ndata: {\"id\":\"chatcmpl-DmPRwO9SjY0GJZ3TFFe1Be72wysEG\",\"object\":\"chat.completion.chunk\",\"created\":1780429944,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_40bf7dabb5\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"fDsGzJ\"}\n\ndata: {\"id\":\"chatcmpl-DmPRwO9SjY0GJZ3TFFe1Be72wysEG\",\"object\":\"chat.completion.chunk\",\"created\":1780429944,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_40bf7dabb5\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"!\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"RqaP5kpPNU\"}\n\ndata: {\"id\":\"chatcmpl-DmPRwO9SjY0GJZ3TFFe1Be72wysEG\",\"object\":\"chat.completion.chunk\",\"created\":1780429944,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_40bf7dabb5\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null,\"obfuscation\":\"B19l5\"}\n\ndata: {\"id\":\"chatcmpl-DmPRwO9SjY0GJZ3TFFe1Be72wysEG\",\"object\":\"chat.completion.chunk\",\"created\":1780429944,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_40bf7dabb5\",\"choices\":[],\"usage\":{\"prompt_tokens\":22,\"completion_tokens\":2,\"total_tokens\":24,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"audio_tokens\":0,\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0}},\"obfuscation\":\"kbiJobM55YE\"}\n\ndata: [DONE]\n\n" + } + } + ] +} diff --git a/packages/core/test/location-filesystem.test.ts b/packages/core/test/location-filesystem.test.ts index eaa6df08d8..c06c9cf6ed 100644 --- a/packages/core/test/location-filesystem.test.ts +++ b/packages/core/test/location-filesystem.test.ts @@ -1,8 +1,8 @@ import fs from "fs/promises" import path from "path" import { fileURLToPath } from "url" -import { describe, expect } from "bun:test" -import { Effect, Exit, Layer } from "effect" +import { describe, expect, test } from "bun:test" +import { Effect, Exit, Layer, Schema } from "effect" import { FSUtil } from "@opencode-ai/core/fs-util" import { Location } from "@opencode-ai/core/location" import { FileSystem } from "@opencode-ai/core/filesystem" @@ -22,12 +22,12 @@ const inertReferences = ProjectReference.Service.of({ containsManagedPath: () => Effect.succeed(false), }) -function provide(directory: string, references = inertReferences) { +function provide(directory: string, references = inertReferences, filesystem = FSUtil.defaultLayer) { return Effect.provide( FileSystem.layer.pipe( Layer.provide( Layer.mergeAll( - FSUtil.defaultLayer, + filesystem, Ripgrep.defaultLayer, Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))), Layer.succeed(ProjectReference.Service, references), @@ -63,6 +63,43 @@ describe("FileSystem", () => { encoding: "base64", mime: "application/octet-stream", }) + const binary = yield* service.resolveRead({ path: RelativePath.make("data.bin") }) + expect(Exit.isFailure(yield* service.readTextPageResolved(binary).pipe(Effect.exit))).toBe(true) + }).pipe(provide(directory)), + ), + ) + + it.live("pages large UTF-8 text files by line with continuation", () => + withTmp((directory) => + Effect.gen(function* () { + const lines = Array.from({ length: 30 }, (_, index) => `line-${index + 1}-é`.padEnd(2_000, "x")) + yield* Effect.promise(() => fs.writeFile(path.join(directory, "large.txt"), lines.join("\n"))) + const service = yield* FileSystem.Service + const target = yield* service.resolveRead({ path: RelativePath.make("large.txt") }) + + const first = yield* service.readTextPageResolved(target) + expect(first).toMatchObject({ + type: "text-page", + offset: 1, + truncated: true, + }) + expect(first.next).toBeDefined() + const next = first.next! + expect(yield* service.readTextPageResolved(target, { offset: next, limit: 1 })).toEqual({ + type: "text-page", + content: lines[next - 1], + mime: "text/plain", + offset: next, + truncated: true, + next: next + 1, + }) + expect(yield* service.readTextPageResolved(target, { offset: 30 })).toEqual({ + type: "text-page", + content: lines[29], + mime: "text/plain", + offset: 30, + truncated: false, + }) }).pipe(provide(directory)), ), ) @@ -98,6 +135,163 @@ describe("FileSystem", () => { ), ) + it.live("lists stable bounded pages", () => + withTmp((directory) => + Effect.gen(function* () { + yield* Effect.promise(async () => { + await fs.mkdir(path.join(directory, "src")) + await fs.writeFile(path.join(directory, "README.md"), "# Test") + }) + const service = yield* FileSystem.Service + + expect(yield* service.listPage({ limit: 1 })).toMatchObject({ + entries: [{ path: "src", type: "directory" }], + truncated: true, + next: 2, + }) + expect(yield* service.listPage({ offset: 2, limit: 1 })).toMatchObject({ + entries: [{ path: "README.md", type: "file" }], + truncated: false, + }) + expect((yield* service.resolveList()).resource).toBe(".") + }).pipe(provide(directory)), + ), + ) + + it.live("materializes only the selected direct children for a page", () => + withTmp((directory) => { + const realPaths: string[] = [] + const filesystem = Layer.effect( + FSUtil.Service, + Effect.gen(function* () { + const service = yield* FSUtil.Service + return FSUtil.Service.of({ + ...service, + realPath: (target) => + Effect.sync(() => realPaths.push(target)).pipe(Effect.andThen(service.realPath(target))), + }) + }), + ).pipe(Layer.provide(FSUtil.defaultLayer)) + return Effect.gen(function* () { + yield* Effect.promise(async () => { + await fs.mkdir(path.join(directory, "src")) + await fs.writeFile(path.join(directory, "alpha.txt"), "alpha") + await fs.writeFile(path.join(directory, "beta.txt"), "beta") + }) + const service = yield* FileSystem.Service + + expect(yield* service.listPage({ offset: 2, limit: 1 })).toMatchObject({ + entries: [{ path: "alpha.txt", type: "file" }], + truncated: true, + next: 3, + }) + expect(realPaths.filter((target) => target !== directory)).toEqual([path.join(directory, "alpha.txt")]) + }).pipe(provide(directory, inertReferences, filesystem)) + }), + ) + + it.live("materializes selected page entries with at most 16 concurrent real path lookups", () => + withTmp((directory) => { + let active = 0 + let maximum = 0 + const filesystem = Layer.effect( + FSUtil.Service, + Effect.gen(function* () { + const service = yield* FSUtil.Service + return FSUtil.Service.of({ + ...service, + realPath: (target) => + target === directory + ? service.realPath(target) + : Effect.acquireUseRelease( + Effect.sync(() => { + active++ + maximum = Math.max(maximum, active) + }), + () => Effect.sleep("10 millis").pipe(Effect.andThen(service.realPath(target))), + () => Effect.sync(() => active--), + ), + }) + }), + ).pipe(Layer.provide(FSUtil.defaultLayer)) + return Effect.gen(function* () { + yield* Effect.promise(() => + Promise.all(Array.from({ length: 32 }, (_, index) => fs.writeFile(path.join(directory, `${index}.txt`), ""))), + ) + const service = yield* FileSystem.Service + + expect((yield* service.listPage({ limit: 32 })).entries).toHaveLength(32) + expect(maximum).toBe(16) + }).pipe(provide(directory, inertReferences, filesystem)) + }), + ) + + it.live("caps direct list page service calls at 2000 entries", () => + withTmp((directory) => + Effect.gen(function* () { + yield* Effect.promise(() => + Promise.all( + Array.from({ length: 2_001 }, (_, index) => + fs.writeFile(path.join(directory, `${index.toString().padStart(4, "0")}.txt`), ""), + ), + ), + ) + const service = yield* FileSystem.Service + const target = yield* service.resolveList() + + expect((yield* service.listPageResolved(target, { limit: 2_001 })).entries).toHaveLength(2_000) + }).pipe(provide(directory)), + ), + ) + + test("rejects empty list aliases and page limits over 2000", () => { + const decode = Schema.decodeUnknownSync(FileSystem.ListPageInput) + expect(() => decode({ reference: "" })).toThrow() + expect(() => decode({ limit: 2_001 })).toThrow() + }) + + it.live("rejects escaping list paths and omits escaping symlink children", () => + withTmp((directory) => + Effect.gen(function* () { + if (process.platform === "win32") return + const outside = `${directory}-outside` + yield* Effect.promise(async () => { + await fs.mkdir(outside) + await fs.writeFile(path.join(outside, "secret.txt"), "secret") + await fs.symlink(outside, path.join(directory, "escape")) + }) + const service = yield* FileSystem.Service + + expect( + Exit.isFailure(yield* service.listPage({ path: RelativePath.make("../outside") }).pipe(Effect.exit)), + ).toBe(true) + expect((yield* service.listPage()).entries).toEqual([]) + yield* Effect.promise(() => fs.rm(outside, { recursive: true, force: true })) + }).pipe(provide(directory)), + ), + ) + + it.live("paginates visible entries after omitting escaping symlink children", () => + withTmp((directory) => + Effect.gen(function* () { + if (process.platform === "win32") return + const outside = `${directory}-outside` + yield* Effect.promise(async () => { + await fs.mkdir(outside) + await fs.symlink(outside, path.join(directory, "a-escape")) + await fs.writeFile(path.join(directory, "b-visible.txt"), "visible") + }) + const service = yield* FileSystem.Service + + expect(yield* service.listPage({ limit: 1 })).toMatchObject({ + entries: [{ path: "b-visible.txt", type: "file" }], + truncated: false, + }) + yield* Effect.promise(() => fs.rm(outside, { recursive: true, force: true })) + }).pipe(provide(directory)), + ), + ) + it.live("rejects paths outside the location", () => withTmp((directory) => Effect.gen(function* () { @@ -109,42 +303,6 @@ describe("FileSystem", () => { ), ) - it.live("finds files and directories", () => - withTmp((directory) => - Effect.gen(function* () { - yield* Effect.promise(() => fs.mkdir(path.join(directory, "src"))) - yield* Effect.promise(() => fs.writeFile(path.join(directory, "src", "index.ts"), "const needle = true\n")) - const service = yield* FileSystem.Service - - expect((yield* service.find({ query: "index", type: "file" })).map((item) => item.path)).toEqual([ - RelativePath.make(path.join("src", "index.ts")), - ]) - expect((yield* service.find({ query: "src", type: "directory" })).map((item) => item.path)).toEqual([ - RelativePath.make("src"), - ]) - }).pipe(provide(directory)), - ), - ) - - it.live("greps file contents", () => - withTmp((directory) => - Effect.gen(function* () { - yield* Effect.promise(() => fs.writeFile(path.join(directory, "index.ts"), "const needle = true\n")) - const service = yield* FileSystem.Service - - expect(yield* service.grep({ pattern: "needle" })).toEqual([ - { - path: RelativePath.make("index.ts"), - lines: "const needle = true\n", - line: 1, - offset: 0, - submatches: [{ text: "needle", start: 6, end: 12 }], - }, - ]) - }).pipe(provide(directory)), - ), - ) - it.live("reads and lists paths relative to a local project reference", () => withTmp((directory) => { const docs = path.join(directory, "docs") diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index 8eb56bebef..baaa27a374 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -17,6 +17,8 @@ import { ModelsDev } from "../src/models-dev" import { Npm } from "../src/npm" import { Project } from "../src/project" import { ProjectReference } from "../src/project-reference" +import { LocationSearch } from "../src/location-search" +import { ToolRegistry } from "../src/tool-registry" const it = testEffect( LocationServiceMap.layer.pipe( @@ -55,18 +57,48 @@ describe("LocationServiceMap", () => { Effect.gen(function* () { yield* PluginBoot.Service.use((boot) => boot.wait()) yield* ProjectReference.Service + yield* LocationSearch.Service const catalog = yield* Catalog.Service const transform = yield* catalog.transform() yield* transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {})) - return yield* catalog.provider.all() + return { + providers: yield* catalog.provider.all(), + tools: yield* (yield* ToolRegistry.Service).definitions(), + } }).pipe(Effect.scoped, Effect.provide(LocationServiceMap.get({ directory: AbsolutePath.make(directory) }))) - expect((yield* update(blocked.path)).some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe( - false, - ) - expect((yield* update(allowed.path)).some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe( - true, - ) + const blockedState = yield* update(blocked.path) + expect(blockedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(false) + expect(blockedState.tools.map((tool) => tool.name).sort()).toEqual([ + "apply_patch", + "bash", + "edit", + "glob", + "grep", + "question", + "read", + "skill", + "todowrite", + "webfetch", + "websearch", + "write", + ]) + const allowedState = yield* update(allowed.path) + expect(allowedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(true) + expect(allowedState.tools.map((tool) => tool.name).sort()).toEqual([ + "apply_patch", + "bash", + "edit", + "glob", + "grep", + "question", + "read", + "skill", + "todowrite", + "webfetch", + "websearch", + "write", + ]) }), ), ), diff --git a/packages/core/test/location-mutation.test.ts b/packages/core/test/location-mutation.test.ts new file mode 100644 index 0000000000..bcfeaf2139 --- /dev/null +++ b/packages/core/test/location-mutation.test.ts @@ -0,0 +1,234 @@ +import fs from "fs/promises" +import path from "path" +import { describe, expect, test } from "bun:test" +import { Effect, Layer, Schema } from "effect" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Location } from "@opencode-ai/core/location" +import { LocationMutation } from "@opencode-ai/core/location-mutation" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { tmpdir } from "./fixture/tmpdir" +import { location } from "./fixture/location" +import { it } from "./lib/effect" + +function provide(directory: string) { + return Effect.provide( + LocationMutation.layer.pipe( + Layer.provide( + Layer.mergeAll( + FSUtil.defaultLayer, + Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))), + ), + ), + ), + ) +} + +function withTmp(f: (directory: string) => Effect.Effect) { + return Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe(Effect.flatMap((tmp) => f(tmp.path))) +} + +describe("LocationMutation", () => { + it.live("resolves an active relative existing file target", () => + withTmp((directory) => + Effect.gen(function* () { + const targetPath = path.join(directory, "hello.txt") + yield* Effect.promise(() => fs.writeFile(targetPath, "hello")) + const plan = yield* (yield* LocationMutation.Service).resolve({ path: "hello.txt" }) + + expect(plan.target).toMatchObject({ + canonical: yield* Effect.promise(() => fs.realpath(targetPath)), + exists: true, + resource: "hello.txt", + }) + expect(plan.target.externalDirectory).toBeUndefined() + expect(yield* (yield* LocationMutation.Service).revalidate(plan)).toMatchObject({ + canonical: plan.target.canonical, + }) + }).pipe(provide(directory)), + ), + ) + + it.live("resolves an active relative prospective file target", () => + withTmp((directory) => + Effect.gen(function* () { + yield* Effect.promise(() => fs.mkdir(path.join(directory, "src"))) + const plan = yield* (yield* LocationMutation.Service).resolve({ path: path.join("src", "new.txt") }) + const root = yield* Effect.promise(() => fs.realpath(directory)) + + expect(plan.target).toMatchObject({ + canonical: path.join(root, "src", "new.txt"), + exists: false, + resource: "src/new.txt", + }) + expect(plan.authority.canonical).toBe(path.join(root, "src")) + expect(yield* (yield* LocationMutation.Service).revalidate(plan)).toMatchObject({ + canonical: plan.target.canonical, + }) + }).pipe(provide(directory)), + ), + ) + + it.live("rejects a relative lexical escape instead of promoting it to external authority", () => + withTmp((directory) => + Effect.gen(function* () { + const error = yield* Effect.flip((yield* LocationMutation.Service).resolve({ path: "../outside.txt" })) + expect(error).toMatchObject({ _tag: "LocationMutation.PathError", reason: "relative_escape" }) + }).pipe(provide(directory)), + ), + ) + + it.live("rejects a prospective target below an escaping symlink ancestor", () => + withTmp((directory) => { + const outside = `${directory}-outside` + return Effect.gen(function* () { + if (process.platform === "win32") return + yield* Effect.promise(async () => { + await fs.mkdir(outside) + await fs.symlink(outside, path.join(directory, "escape")) + }) + const error = yield* Effect.flip( + (yield* LocationMutation.Service).resolve({ path: path.join("escape", "new.txt") }), + ) + expect(error).toMatchObject({ _tag: "LocationMutation.PathError", reason: "location_escape" }) + yield* Effect.promise(() => fs.rm(outside, { recursive: true, force: true })) + }).pipe(provide(directory)) + }), + ) + + it.live("accepts an explicit absolute in-location target without external approval", () => + withTmp((directory) => + Effect.gen(function* () { + const targetPath = path.join(directory, "new.txt") + const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath }) + expect(plan.target).toMatchObject({ + canonical: path.join(yield* Effect.promise(() => fs.realpath(directory)), "new.txt"), + resource: "new.txt", + }) + expect(plan.target.externalDirectory).toBeUndefined() + }).pipe(provide(directory)), + ), + ) + + it.live("requires external-directory authorization for an explicit external absolute target", () => + withTmp((directory) => + withTmp((outside) => + Effect.gen(function* () { + const targetPath = path.join(outside, "new.txt") + const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath }) + const root = yield* Effect.promise(() => fs.realpath(outside)) + expect(plan.target).toMatchObject({ + canonical: path.join(root, "new.txt"), + resource: path.join(root, "new.txt").replaceAll("\\", "/"), + }) + expect(plan.target.externalDirectory).toMatchObject({ + directory: root, + resource: path.join(root, "*").replaceAll("\\", "/"), + }) + }).pipe(provide(directory)), + ), + ), + ) + + it.live("resolves an existing external file target", () => + withTmp((directory) => + withTmp((outside) => + Effect.gen(function* () { + const targetPath = path.join(outside, "existing.txt") + yield* Effect.promise(() => fs.writeFile(targetPath, "existing")) + const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath }) + const root = yield* Effect.promise(() => fs.realpath(outside)) + expect(plan.target).toMatchObject({ canonical: path.join(root, "existing.txt"), exists: true }) + expect(plan.authority.canonical).toBe(path.join(root, "existing.txt")) + expect(plan.target.externalDirectory?.directory).toBe(root) + }).pipe(provide(directory)), + ), + ), + ) + + it.live("anchors prospective external descendants at their stable existing directory", () => + withTmp((directory) => + withTmp((outside) => + Effect.gen(function* () { + const targetPath = path.join(outside, "new", "nested", "file.txt") + const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath }) + const root = yield* Effect.promise(() => fs.realpath(outside)) + expect(plan.authority.canonical).toBe(root) + expect(plan.target.externalDirectory).toMatchObject({ + directory: root, + resource: path.join(root, "*").replaceAll("\\", "/"), + }) + }).pipe(provide(directory)), + ), + ), + ) + + it.live("rejects a symlink-ancestor swap during post-approval revalidation", () => + withTmp((directory) => + withTmp((outside) => + Effect.gen(function* () { + if (process.platform === "win32") return + const parent = path.join(directory, "parent") + yield* Effect.promise(() => fs.mkdir(parent)) + const service = yield* LocationMutation.Service + const plan = yield* service.resolve({ path: path.join("parent", "new.txt") }) + yield* Effect.promise(async () => { + await fs.rmdir(parent) + await fs.symlink(outside, parent) + }) + + const error = yield* Effect.flip(service.revalidate(plan)) + expect(error).toMatchObject({ _tag: "LocationMutation.RevalidationError" }) + }).pipe(provide(directory)), + ), + ), + ) + + it.live("rejects an existing target identity swap during post-approval revalidation", () => + withTmp((directory) => + Effect.gen(function* () { + const targetPath = path.join(directory, "existing.txt") + yield* Effect.promise(() => fs.writeFile(targetPath, "first")) + const service = yield* LocationMutation.Service + const plan = yield* service.resolve({ path: "existing.txt" }) + yield* Effect.promise(async () => { + const replacementPath = path.join(directory, "replacement.txt") + await fs.writeFile(replacementPath, "second") + await fs.rm(targetPath) + await fs.rename(replacementPath, targetPath) + }) + + const error = yield* Effect.flip(service.revalidate(plan)) + expect(error).toMatchObject({ + _tag: "LocationMutation.RevalidationError", + reason: "mutation authority changed", + }) + }).pipe(provide(directory)), + ), + ) + + it.live("rejects a nearer prospective ancestor introduced after approval", () => + withTmp((directory) => + Effect.gen(function* () { + const service = yield* LocationMutation.Service + const plan = yield* service.resolve({ path: path.join("new", "nested", "file.txt") }) + yield* Effect.promise(() => fs.mkdir(path.join(directory, "new"))) + + const error = yield* Effect.flip(service.revalidate(plan)) + expect(error).toMatchObject({ + _tag: "LocationMutation.RevalidationError", + reason: "mutation authority changed", + }) + }).pipe(provide(directory)), + ), + ) + + test("keeps project references outside the mutation input API", () => { + expect(Object.keys(LocationMutation.ResolveInput.fields)).toEqual(["path", "kind"]) + expect(Schema.decodeUnknownSync(LocationMutation.ResolveInput)({ path: "README.md", reference: "docs" })).toEqual({ + path: "README.md", + }) + }) +}) diff --git a/packages/core/test/location-search.test.ts b/packages/core/test/location-search.test.ts new file mode 100644 index 0000000000..2ee2df1fd9 --- /dev/null +++ b/packages/core/test/location-search.test.ts @@ -0,0 +1,282 @@ +import fs from "fs/promises" +import path from "path" +import { describe, expect, test } from "bun:test" +import { Cause, Effect, Exit, Layer, Schema } from "effect" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Location } from "@opencode-ai/core/location" +import { FileSystem } from "@opencode-ai/core/filesystem" +import { LocationSearch } from "@opencode-ai/core/location-search" +import { AppProcess } from "@opencode-ai/core/process" +import { Ripgrep as FileSystemRipgrep } from "@opencode-ai/core/filesystem/ripgrep" +import { ProjectReference } from "@opencode-ai/core/project-reference" +import { Ripgrep } from "@opencode-ai/core/ripgrep" +import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema" +import { tmpdir } from "./fixture/tmpdir" +import { location } from "./fixture/location" +import { it } from "./lib/effect" + +const inertReferences = references({}) + +function provide(directory: string, projectReferences = inertReferences) { + const dependencies = Layer.mergeAll( + FSUtil.defaultLayer, + FileSystemRipgrep.defaultLayer, + AppProcess.defaultLayer, + Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))), + Layer.succeed(ProjectReference.Service, projectReferences), + ) + const filesystem = FileSystem.layer.pipe(Layer.provide(dependencies)) + const search = LocationSearch.layer.pipe( + Layer.provide(filesystem), + Layer.provide(Ripgrep.layer.pipe(Layer.provide(dependencies))), + Layer.provide(FSUtil.defaultLayer), + Layer.provide(dependencies), + ) + return Effect.provide(Layer.merge(filesystem, search)) +} + +function withTmp(f: (directory: string) => Effect.Effect) { + return Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe(Effect.flatMap((tmp) => f(tmp.path))) +} + +describe("LocationSearch", () => { + it.live("searches files in the active Location with structured bounded results", () => + withTmp((directory) => + Effect.gen(function* () { + yield* Effect.promise(async () => { + await fs.mkdir(path.join(directory, "src")) + await fs.writeFile(path.join(directory, "src", "index.ts"), "export const value = 1\n") + await fs.writeFile(path.join(directory, "notes.txt"), "notes\n") + }) + const result = yield* (yield* LocationSearch.Service).files({ pattern: "*.ts" }) + const canonical = yield* Effect.promise(() => fs.realpath(path.join(directory, "src", "index.ts"))) + + expect(result).toMatchObject({ truncated: false, partial: false }) + expect(result.items).toHaveLength(1) + expect(result.items[0]).toMatchObject({ + path: RelativePath.make("src/index.ts"), + canonical, + resource: "src/index.ts", + }) + expect(typeof result.items[0].mtime).toBe("number") + }).pipe(provide(directory)), + ), + ) + + it.live("searches files under a relative subdirectory and named local reference", () => + withTmp((directory) => { + const docs = path.join(directory, "docs") + return Effect.gen(function* () { + yield* Effect.promise(async () => { + await fs.mkdir(path.join(directory, "src")) + await fs.mkdir(docs) + await fs.writeFile(path.join(directory, "src", "active.ts"), "active\n") + await fs.writeFile(path.join(docs, "guide.md"), "guide\n") + }) + const search = yield* LocationSearch.Service + + expect( + (yield* search.files({ pattern: "*.ts", path: RelativePath.make("src") })).items.map((item) => item.path), + ).toEqual([RelativePath.make("src/active.ts")]) + const guide = yield* Effect.promise(() => fs.realpath(path.join(docs, "guide.md"))) + expect((yield* search.files({ pattern: "*.md", reference: "docs" })).items).toMatchObject([ + { path: RelativePath.make("guide.md"), resource: "docs:guide.md", canonical: guide }, + ]) + }).pipe(provide(directory, references({ docs: { name: "docs", kind: "local", path: docs } }))) + }), + ) + + it.live("greps the Location, exact relative files and directories, and include globs", () => + withTmp((directory) => + Effect.gen(function* () { + yield* Effect.promise(async () => { + await fs.mkdir(path.join(directory, "src")) + await fs.writeFile(path.join(directory, "src", "one.ts"), "needle ts\n") + await fs.writeFile(path.join(directory, "src", "two.txt"), "needle txt\n") + await fs.writeFile(path.join(directory, "root.md"), "needle root\n") + }) + const search = yield* LocationSearch.Service + + expect((yield* search.grep({ pattern: "needle" })).items.map((item) => item.path).sort()).toEqual([ + RelativePath.make("root.md"), + RelativePath.make("src/one.ts"), + RelativePath.make("src/two.txt"), + ]) + expect( + (yield* search.grep({ pattern: "needle", path: RelativePath.make("src") })).items + .map((item) => item.path) + .sort(), + ).toEqual([RelativePath.make("src/one.ts"), RelativePath.make("src/two.txt")]) + expect((yield* search.grep({ pattern: "needle", path: RelativePath.make("src/one.ts") })).items).toMatchObject([ + { path: RelativePath.make("src/one.ts"), resource: "src/one.ts", lines: "needle ts\n", line: 1, offset: 0 }, + ]) + expect((yield* search.grep({ pattern: "needle", include: "*.ts" })).items.map((item) => item.path)).toEqual([ + RelativePath.make("src/one.ts"), + ]) + }).pipe(provide(directory)), + ), + ) + + it.live("does not discover hidden files during broad V2 searches", () => + withTmp((directory) => + Effect.gen(function* () { + yield* Effect.promise(async () => { + await fs.mkdir(path.join(directory, "nested", ".private"), { recursive: true }) + await fs.writeFile(path.join(directory, "visible.txt"), "needle visible\n") + await fs.writeFile(path.join(directory, ".env"), "needle root secret\n") + await fs.writeFile(path.join(directory, "nested", "visible.txt"), "needle nested visible\n") + await fs.writeFile(path.join(directory, "nested", ".env"), "needle nested secret\n") + await fs.writeFile(path.join(directory, "nested", ".private", "secret.txt"), "needle hidden directory\n") + }) + const search = yield* LocationSearch.Service + + expect((yield* search.files({ pattern: "*" })).items.map((item) => item.path).sort()).toEqual([ + RelativePath.make("nested/visible.txt"), + RelativePath.make("visible.txt"), + ]) + expect((yield* search.files({ pattern: ".env" })).items).toEqual([]) + expect((yield* search.grep({ pattern: "needle", include: "*" })).items.map((item) => item.path).sort()).toEqual([ + RelativePath.make("nested/visible.txt"), + RelativePath.make("visible.txt"), + ]) + }).pipe(provide(directory)), + ), + ) + + it.live("caps result counts and line previews", () => + withTmp((directory) => + Effect.gen(function* () { + yield* Effect.promise(async () => { + await Promise.all( + Array.from({ length: 101 }, (_, index) => fs.writeFile(path.join(directory, `${index}.txt`), "needle\n")), + ) + await fs.writeFile( + path.join(directory, "long.txt"), + `needle ${"x".repeat(LocationSearch.MAX_LINE_PREVIEW_LENGTH)}\n`, + ) + }) + const search = yield* LocationSearch.Service + const files = yield* search.files({ pattern: "*.txt", limit: 2 }) + const hardCappedFiles = yield* search.files({ pattern: "*.txt", limit: LocationSearch.MAX_RESULT_LIMIT + 1 }) + const hardCappedGrep = yield* search.grep({ pattern: "needle", limit: LocationSearch.MAX_RESULT_LIMIT + 1 }) + const grep = yield* search.grep({ pattern: "needle", path: RelativePath.make("long.txt") }) + + expect(files.items).toHaveLength(2) + expect(files.truncated).toBe(true) + expect(hardCappedFiles.items).toHaveLength(LocationSearch.MAX_RESULT_LIMIT) + expect(hardCappedFiles.truncated).toBe(true) + expect(hardCappedGrep.items).toHaveLength(LocationSearch.MAX_RESULT_LIMIT) + expect(hardCappedGrep.truncated).toBe(true) + expect(grep.items[0].lines).toHaveLength(LocationSearch.MAX_LINE_PREVIEW_LENGTH) + expect(grep.items[0].linePreviewTruncated).toBe(true) + }).pipe(provide(directory)), + ), + ) + + it.live("reports invalid regex as a typed failure", () => + withTmp((directory) => + Effect.gen(function* () { + yield* Effect.promise(() => fs.writeFile(path.join(directory, "notes.txt"), "notes\n")) + const exit = yield* (yield* LocationSearch.Service).grep({ pattern: "[" }).pipe(Effect.exit) + + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Ripgrep.InvalidPatternError) + }).pipe(provide(directory)), + ), + ) + + it.live("rejects oversized ripgrep JSON records before durable projection", () => + withTmp((directory) => + Effect.gen(function* () { + yield* Effect.promise(() => fs.writeFile(path.join(directory, "huge.txt"), `needle ${"x".repeat(Ripgrep.MAX_RECORD_BYTES)}\n`)) + const exit = yield* (yield* LocationSearch.Service).grep({ pattern: "needle" }).pipe(Effect.exit) + + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) expect(String(Cause.squash(exit.cause))).toContain("Ripgrep JSON record exceeded") + }).pipe(provide(directory)), + ), + ) + + it.live("rejects lexical and symlink escapes through root resolution", () => + withTmp((directory) => + Effect.gen(function* () { + if (process.platform === "win32") return + const outside = `${directory}-outside` + yield* Effect.promise(async () => { + await fs.mkdir(outside) + await fs.writeFile(path.join(outside, "secret.txt"), "secret\n") + await fs.symlink(outside, path.join(directory, "escape")) + }) + const search = yield* LocationSearch.Service + + expect( + Exit.isFailure( + yield* search.files({ pattern: "*", path: RelativePath.make("../outside") }).pipe(Effect.exit), + ), + ).toBe(true) + expect( + Exit.isFailure(yield* search.files({ pattern: "*", path: RelativePath.make("escape") }).pipe(Effect.exit)), + ).toBe(true) + yield* Effect.promise(() => fs.rm(outside, { recursive: true, force: true })) + }).pipe(provide(directory)), + ), + ) + + it.live("rejects an approved root swapped to a symlink before ripgrep traversal", () => + withTmp((directory) => + Effect.gen(function* () { + if (process.platform === "win32") return + const source = path.join(directory, "src") + const outside = `${directory}-outside` + yield* Effect.promise(async () => { + await fs.mkdir(source) + await fs.mkdir(outside) + await fs.writeFile(path.join(outside, "secret.txt"), "secret\n") + }) + const filesystem = yield* FileSystem.Service + const approved = yield* filesystem.resolveRoot({ path: RelativePath.make("src") }) + yield* Effect.promise(async () => { + await fs.rmdir(source) + await fs.symlink(outside, source) + }) + + expect(Exit.isFailure(yield* (yield* LocationSearch.Service).files({ pattern: "*" }, approved).pipe(Effect.exit))).toBe(true) + yield* Effect.promise(() => fs.rm(outside, { recursive: true, force: true })) + }).pipe(provide(directory)), + ), + ) + + it.live("honors a pre-aborted cancellation signal", () => + withTmp((directory) => + Effect.gen(function* () { + const controller = new AbortController() + controller.abort() + const exit = yield* (yield* LocationSearch.Service) + .files({ pattern: "*", signal: controller.signal }) + .pipe(Effect.exit) + expect(Exit.isFailure(exit)).toBe(true) + }).pipe(provide(directory)), + ), + ) + + test("exposes schema-testable search bounds", () => { + const decode = Schema.decodeUnknownSync(LocationSearch.FilesInput) + expect(LocationSearch.DEFAULT_RESULT_LIMIT).toBe(100) + expect(LocationSearch.MAX_RESULT_LIMIT).toBe(100) + expect(LocationSearch.MAX_LINE_PREVIEW_LENGTH).toBe(2_000) + expect(() => decode({ pattern: "*", limit: LocationSearch.MAX_RESULT_LIMIT + 1 })).toThrow() + }) +}) + +function references(entries: Record) { + return ProjectReference.Service.of({ + list: () => Effect.succeed(Object.values(entries)), + get: (name) => Effect.succeed(entries[name]), + resolveMention: () => Effect.succeed(undefined), + ensurePath: () => Effect.void, + containsManagedPath: () => Effect.succeed(false), + }) +} diff --git a/packages/core/test/location.test.ts b/packages/core/test/location.test.ts index 04d1320638..327c5bff9f 100644 --- a/packages/core/test/location.test.ts +++ b/packages/core/test/location.test.ts @@ -3,9 +3,11 @@ import { Effect, Layer } from "effect" import { Location } from "@opencode-ai/core/location" import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" +import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { testEffect } from "./lib/effect" -const ref = { directory: AbsolutePath.make("/repo/packages/app"), workspaceID: "workspace" } +const workspaceID = WorkspaceV2.ID.make("wrk_test") +const ref = { directory: AbsolutePath.make("/repo/packages/app"), workspaceID } const projectLayer = Layer.succeed( Project.Service, Project.Service.of({ @@ -27,7 +29,7 @@ describe("Location", () => { const location = yield* Location.Service expect(location.directory).toBe(AbsolutePath.make("/repo/packages/app")) - expect(location.workspaceID).toBe("workspace") + expect(location.workspaceID).toBe(workspaceID) expect(location.project.id).toBe(Project.ID.make("project")) expect(location.project.directory).toBe(AbsolutePath.make("/repo")) expect(location.vcs).toEqual({ diff --git a/packages/core/test/opencode.test.ts b/packages/core/test/opencode.test.ts new file mode 100644 index 0000000000..3c0c499f5b --- /dev/null +++ b/packages/core/test/opencode.test.ts @@ -0,0 +1,16 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { OpenCode } from "@opencode-ai/core/opencode" +import { testEffect } from "./lib/effect" + +const it = testEffect(OpenCode.layer) + +describe("OpenCode.layer", () => { + it.effect("exposes Sessions through the public embedded API", () => + Effect.gen(function* () { + const opencode = yield* OpenCode.Service + + expect(yield* opencode.sessions.list()).toBeArray() + }), + ) +}) diff --git a/packages/core/test/patch.test.ts b/packages/core/test/patch.test.ts new file mode 100644 index 0000000000..fa91010f1f --- /dev/null +++ b/packages/core/test/patch.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from "bun:test" +import { Patch } from "@opencode-ai/core/patch" + +describe("Patch", () => { + test("parses add, update, and delete hunks", () => { + expect( + Patch.parse("*** Begin Patch\n*** Add File: add.txt\n+added\n*** Update File: update.txt\n@@ section\n-old\n+new\n*** Delete File: delete.txt\n*** End Patch"), + ).toEqual([ + { type: "add", path: "add.txt", contents: "added" }, + { type: "update", path: "update.txt", chunks: [{ oldLines: ["old"], newLines: ["new"], changeContext: "section", endOfFile: undefined }], movePath: undefined }, + { type: "delete", path: "delete.txt" }, + ]) + }) + + test("strips a heredoc wrapper", () => { + expect(Patch.parse("cat <<'EOF'\n*** Begin Patch\n*** Add File: add.txt\n+added\n*** End Patch\nEOF")).toEqual([ + { type: "add", path: "add.txt", contents: "added" }, + ]) + }) + + test("derives fuzzy line updates while preserving BOM", () => { + const update = Patch.derive( + "update.txt", + [{ oldLines: [" old "], newLines: ["new"] }], + "\uFEFFold\n", + ) + expect(update).toEqual({ content: "new\n", bom: true }) + expect(Patch.joinBom(update.content, update.bom)).toBe("\uFEFFnew\n") + }) + + test("matches EOF-anchored chunks from the end", () => { + expect( + Patch.derive( + "update.txt", + [{ oldLines: ["marker", "end"], newLines: ["marker changed", "end"], endOfFile: true }], + "marker\nmiddle\nmarker\nend\n", + ).content, + ).toBe("marker\nmiddle\nmarker changed\nend\n") + }) + + test("parses the EOF marker inside update chunks", () => { + expect(Patch.parse("*** Begin Patch\n*** Update File: update.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch")).toEqual([ + { + type: "update", + path: "update.txt", + movePath: undefined, + chunks: [{ oldLines: ["last"], newLines: ["end"], changeContext: undefined, endOfFile: true }], + }, + ]) + }) + + test("rejects malformed hunk bodies", () => { + expect(() => Patch.parse("*** Begin Patch\n*** Add File: add.txt\nmissing plus\n*** End Patch")).toThrow("Invalid add file line") + expect(() => Patch.parse("*** Begin Patch\n*** Update File: update.txt\n*** End Patch")).toThrow("expected at least one @@ chunk") + expect(() => Patch.parse("*** Begin Patch\n*** Delete File: delete.txt\nunexpected body\n*** End Patch")).toThrow("Invalid patch line") + }) +}) diff --git a/packages/core/test/permission.test.ts b/packages/core/test/permission.test.ts index a3abc4e720..21df457de7 100644 --- a/packages/core/test/permission.test.ts +++ b/packages/core/test/permission.test.ts @@ -12,6 +12,8 @@ import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionV2 } from "@opencode-ai/core/session" import { SessionTable } from "@opencode-ai/core/session/sql" +import { SessionExecution } from "@opencode-ai/core/session/execution" +import { SessionStore } from "@opencode-ai/core/session/store" import { eq } from "drizzle-orm" import { location } from "./fixture/location" import { testEffect } from "./lib/effect" @@ -22,13 +24,16 @@ const current = Layer.succeed( Location.Service.of(location({ directory: AbsolutePath.make("/project") })), ) const events = EventV2.layer.pipe(Layer.provide(database)) -const sessions = SessionV2.layer.pipe(Layer.provide(database)) +const store = SessionStore.layer.pipe(Layer.provide(database)) +const sessions = SessionV2.layer.pipe(Layer.provide(events), Layer.provide(database), Layer.provide(store), Layer.provide(Project.defaultLayer), Layer.provide(SessionExecution.noopLayer)) const saved = PermissionSaved.layer.pipe(Layer.provide(database)) const layer = PermissionV2.locationLayer.pipe( Layer.provideMerge(database), + Layer.provideMerge(store), Layer.provideMerge(events), Layer.provideMerge(current), Layer.provideMerge(sessions), + Layer.provideMerge(SessionExecution.noopLayer), Layer.provideMerge(saved), ) const it = testEffect(layer) @@ -127,6 +132,67 @@ describe("PermissionV2", () => { }), ) + it.effect("uses build permissions when the Session agent is omitted", () => + Effect.gen(function* () { + yield* setup() + const { db } = yield* Database.Service + yield* db + .update(SessionTable) + .set({ agent: null }) + .where(eq(SessionTable.id, SessionV2.ID.make("ses_test"))) + .run() + .pipe(Effect.orDie) + const agents = yield* AgentV2.Service + const update = yield* agents.transform() + yield* update((editor) => + editor.update(AgentV2.ID.make("build"), (agent) => { + agent.permissions = [{ action: "todowrite", resource: "*", effect: "allow" }] + }), + ) + + const service = yield* PermissionV2.Service + expect(yield* service.ask(assertion({ action: "todowrite", resources: ["*"] }))).toEqual({ + id: PermissionV2.ID.create("per_test"), + effect: "allow", + }) + expect(yield* service.list()).toEqual([]) + }), + ) + + it.effect("evaluates bash with the normal configured-rule semantics", () => + Effect.gen(function* () { + yield* setup([{ action: "*", resource: "*", effect: "allow" }]) + const service = yield* PermissionV2.Service + const bash = assertion({ action: "bash", resources: ["pwd"] }) + expect(yield* service.ask(bash)).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "allow" }) + + yield* setRules([]) + expect(yield* service.ask(bash)).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "ask" }) + expect(yield* service.get(PermissionV2.ID.create("per_test"))).toBeDefined() + }), + ) + + it.effect("uses saved bash approvals while preserving configured deny precedence", () => + Effect.gen(function* () { + yield* setup() + const saved = yield* PermissionSaved.Service + yield* saved.add({ projectID: Project.ID.global, action: "bash", resources: ["pwd"] }) + + const service = yield* PermissionV2.Service + expect(yield* service.ask(assertion({ action: "bash", resources: ["pwd"] }))).toEqual({ + id: PermissionV2.ID.create("per_test"), + effect: "allow", + }) + expect(yield* service.list()).toEqual([]) + + yield* setRules([{ action: "bash", resource: "*", effect: "deny" }]) + expect(yield* service.ask(assertion({ action: "bash", resources: ["pwd"] }))).toEqual({ + id: PermissionV2.ID.create("per_test"), + effect: "deny", + }) + }), + ) + it.effect("resolves an asked permission once", () => Effect.gen(function* () { yield* setup() diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts new file mode 100644 index 0000000000..b292dc16ef --- /dev/null +++ b/packages/core/test/plugin.test.ts @@ -0,0 +1,90 @@ +import { describe, expect } from "bun:test" +import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect" +import { EventV2 } from "@opencode-ai/core/event" +import { PluginV2 } from "@opencode-ai/core/plugin" +import { State } from "@opencode-ai/core/state" +import { it } from "./lib/effect" + +const events = Layer.mock(EventV2.Service)({ + publish: (definition, data) => + Effect.succeed({ + id: EventV2.ID.make("evt_plugin_test"), + type: definition.type, + data, + }), +}) +const plugins = PluginV2.layer.pipe(Layer.provide(events)) + +function state() { + return State.create({ + initial: () => ({ values: [] as string[] }), + editor: (draft) => ({ + add: (value: string) => draft.values.push(value), + }), + }) +} + +describe("PluginV2", () => { + it.effect("closes plugin-owned scopes when the registry layer finalizes", () => + Effect.gen(function* () { + const values = state() + const layerScope = yield* Scope.fork(yield* Scope.Scope) + const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service) + + yield* plugin.add({ + id: PluginV2.ID.make("scoped"), + effect: Effect.gen(function* () { + const transform = yield* values.transform() + yield* transform((editor) => editor.add("scoped")) + }), + }) + expect(values.get().values).toEqual(["scoped"]) + + yield* Scope.close(layerScope, Exit.void) + expect(values.get().values).toEqual([]) + }), + ) + + it.effect("serializes same-ID additions and leaves one removable contribution", () => + Effect.gen(function* () { + const values = state() + const layerScope = yield* Scope.fork(yield* Scope.Scope) + const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service) + const id = PluginV2.ID.make("shared") + const firstStarted = yield* Deferred.make() + const releaseFirst = yield* Deferred.make() + + const first = yield* plugin + .add({ + id, + effect: Effect.gen(function* () { + const transform = yield* values.transform() + yield* transform((editor) => editor.add("first")) + yield* Deferred.succeed(firstStarted, undefined) + yield* Deferred.await(releaseFirst) + }), + }) + .pipe(Effect.forkChild) + yield* Deferred.await(firstStarted) + + const second = yield* plugin + .add({ + id, + effect: Effect.gen(function* () { + const transform = yield* values.transform() + yield* transform((editor) => editor.add("second")) + }), + }) + .pipe(Effect.forkChild({ startImmediately: true })) + expect(values.get().values).toEqual(["first"]) + + yield* Deferred.succeed(releaseFirst, undefined) + yield* Fiber.join(first) + yield* Fiber.join(second) + expect(values.get().values).toEqual(["second"]) + + yield* plugin.remove(id) + expect(values.get().values).toEqual([]) + }), + ) +}) diff --git a/packages/core/test/process/process.test.ts b/packages/core/test/process/process.test.ts index f6a52b7a68..f8377f718a 100644 --- a/packages/core/test/process/process.test.ts +++ b/packages/core/test/process/process.test.ts @@ -1,7 +1,9 @@ import { describe, expect } from "bun:test" +import fs from "fs/promises" import { realpathSync } from "node:fs" import { tmpdir } from "node:os" -import { Effect, Exit, Stream } from "effect" +import path from "node:path" +import { Effect, Exit, Fiber, Stream } from "effect" import { ChildProcess } from "effect/unstable/process" import { AppProcess } from "@opencode-ai/core/process" import { testEffect } from "../lib/effect" @@ -11,6 +13,18 @@ const it = testEffect(AppProcess.defaultLayer) const NODE = process.execPath const cmd = (...args: string[]) => ChildProcess.make(NODE, args) +const waitForFile = (file: string) => + Effect.promise(async () => { + while (true) { + try { + return await fs.readFile(file, "utf8") + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error + await new Promise((resolve) => setTimeout(resolve, 10)) + } + } + }) + describe("AppProcess", () => { describe("run", () => { it.effect( @@ -118,6 +132,50 @@ describe("AppProcess", () => { expect(result.command).toBe(`${NODE} -e process.stdout.write('hi')`) }), ) + + if (process.platform !== "win32") { + it.live( + "timeout cleans up the scoped child process", + Effect.acquireUseRelease( + Effect.promise(() => fs.mkdtemp(path.join(tmpdir(), "opencode-process-timeout-"))), + (directory) => { + const ready = path.join(directory, "ready") + const settled = path.join(directory, "settled") + const script = `const fs=require('fs');fs.writeFileSync(${JSON.stringify(ready)},String(process.pid));process.on('SIGTERM',()=>{fs.writeFileSync(${JSON.stringify(settled)},'settled');process.exit(0)});setInterval(()=>{},60000)` + return Effect.gen(function* () { + const svc = yield* AppProcess.Service + const exit = yield* Effect.exit(svc.run(cmd("-e", script), { timeout: "1 second" })) + expect(Exit.isFailure(exit)).toBe(true) + expect(yield* waitForFile(ready)).toMatch(/^\d+$/) + expect(yield* waitForFile(settled)).toBe("settled") + }) + }, + (directory) => Effect.promise(() => fs.rm(directory, { recursive: true, force: true })), + ), + 5_000, + ) + + it.live( + "fiber interruption cleans up the scoped child process after readiness", + Effect.acquireUseRelease( + Effect.promise(() => fs.mkdtemp(path.join(tmpdir(), "opencode-process-interrupt-"))), + (directory) => { + const ready = path.join(directory, "ready") + const settled = path.join(directory, "settled") + const script = `const fs=require('fs');fs.writeFileSync(${JSON.stringify(ready)},String(process.pid));process.on('SIGTERM',()=>{fs.writeFileSync(${JSON.stringify(settled)},'settled');process.exit(0)});setInterval(()=>{},60000)` + return Effect.gen(function* () { + const svc = yield* AppProcess.Service + const fiber = yield* svc.run(cmd("-e", script)).pipe(Effect.forkChild) + expect(yield* waitForFile(ready)).toMatch(/^\d+$/) + yield* Fiber.interrupt(fiber) + expect(yield* waitForFile(settled)).toBe("settled") + }) + }, + (directory) => Effect.promise(() => fs.rm(directory, { recursive: true, force: true })), + ), + 5_000, + ) + } }) describe("inherited platform methods", () => { diff --git a/packages/core/test/public-catalog.test.ts b/packages/core/test/public-catalog.test.ts new file mode 100644 index 0000000000..6f4c419dfb --- /dev/null +++ b/packages/core/test/public-catalog.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, test } from "bun:test" +import { Catalog } from "@opencode-ai/core/catalog" +import { EventV2 } from "@opencode-ai/core/event" +import { Location } from "@opencode-ai/core/location" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { Effect, Layer, Schema } from "effect" +import { location } from "./fixture/location" +import { testEffect } from "./lib/effect" + +const locationLayer = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make("test") })), +) +const it = testEffect( + Catalog.locationLayer.pipe(Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(locationLayer)), +) + +const encodeProvider = Schema.encodeSync(ProviderV2.PublicInfo) +const encodeModel = Schema.encodeSync(ModelV2.PublicInfo) + +describe("public catalog DTOs", () => { + test("provider DTO excludes credentials and internal settings", () => { + const providerID = ProviderV2.ID.make("test") + const encoded = encodeProvider( + ProviderV2.toPublic( + new ProviderV2.Info({ + ...ProviderV2.Info.empty(providerID), + enabled: { via: "account", service: "test-account" }, + env: ["TEST_API_KEY"], + api: { type: "native", url: "https://example.com", settings: { apiKey: "settings-secret" } }, + request: { + headers: { Authorization: "Bearer header-secret", "x-api-key": "header-secret" }, + body: { apiKey: "body-secret", account: "account-body-secret" }, + }, + }), + ), + ) + + expect(encoded).toEqual({ + id: "test", + name: "test", + enabled: { via: "account", service: "test-account" }, + env: ["TEST_API_KEY"], + api: { type: "native", url: "https://example.com" }, + }) + expect(JSON.stringify(encoded)).not.toMatch(/Authorization|x-api-key|apiKey|account-body-secret|settings-secret/) + }) + + test("provider DTO excludes custom enabled metadata", () => { + const providerID = ProviderV2.ID.make("custom") + const encoded = encodeProvider( + ProviderV2.toPublic( + new ProviderV2.Info({ + ...ProviderV2.Info.empty(providerID), + enabled: { via: "custom", data: { apiKey: "custom-secret" } }, + }), + ), + ) + + expect(encoded.enabled).toEqual({ via: "custom" }) + expect(JSON.stringify(encoded)).not.toContain("custom-secret") + }) + + it.effect("model DTO excludes resolved provider requests and variant requests", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const providerID = ProviderV2.ID.make("test") + const modelID = ModelV2.ID.make("model") + const transform = yield* catalog.transform() + + yield* transform((catalog) => { + catalog.provider.update(providerID, (provider) => { + provider.enabled = { via: "account", service: "test-account" } + provider.api = { type: "native", url: "https://example.com", settings: { apiKey: "settings-secret" } } + provider.request.headers.Authorization = "Bearer provider-secret" + provider.request.body.apiKey = "provider-body-secret" + provider.request.body.account = "account-body-secret" + }) + catalog.model.update(providerID, modelID, (model) => { + model.request.headers["x-api-key"] = "model-header-secret" + model.request.body.apiKey = "model-body-secret" + model.variants.push({ + id: ModelV2.VariantID.make("fast"), + headers: { Authorization: "Bearer variant-secret" }, + body: { apiKey: "variant-body-secret" }, + }) + }) + }) + + const model = yield* catalog.model.get(providerID, modelID) + expect(model.request.headers.Authorization).toBe("Bearer provider-secret") + expect(model.request.headers["x-api-key"]).toBe("model-header-secret") + expect(model.request.body.apiKey).toBe("model-body-secret") + expect(model.request.body.account).toBe("account-body-secret") + expect(model.api).toHaveProperty("settings.apiKey", "settings-secret") + + const encoded = encodeModel(ModelV2.toPublic(model)) + expect(encoded.api).toEqual({ id: "model", type: "native", url: "https://example.com" }) + expect(encoded.variants).toEqual([{ id: "fast" }]) + expect(encoded).not.toHaveProperty("request") + expect(JSON.stringify(encoded)).not.toMatch( + /Authorization|x-api-key|apiKey|account-body-secret|provider-secret|model-header-secret|variant-secret|settings-secret/, + ) + }), + ) +}) diff --git a/packages/core/test/question.test.ts b/packages/core/test/question.test.ts new file mode 100644 index 0000000000..57bf399669 --- /dev/null +++ b/packages/core/test/question.test.ts @@ -0,0 +1,115 @@ +import { describe, expect } from "bun:test" +import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { QuestionV2 } from "@opencode-ai/core/question" +import { SessionV2 } from "@opencode-ai/core/session" +import { testEffect } from "./lib/effect" + +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const questions = QuestionV2.layer.pipe(Layer.provide(events)) +const it = testEffect(Layer.mergeAll(database, events, questions)) + +const sessionID = SessionV2.ID.make("ses_question_test") +const question: QuestionV2.Info = { + question: "Which option?", + header: "Option", + options: [{ label: "One", description: "First option" }], +} + +const waitForAsk = Effect.fn("QuestionV2Test.waitForAsk")(function* ( + service: QuestionV2.Interface, + input: QuestionV2.AskInput, +) { + const events = yield* EventV2.Service + const asked = yield* Deferred.make() + const unsubscribe = yield* events.listen((event) => + event.type === QuestionV2.Event.Asked.type + ? Deferred.succeed(asked, event.data as QuestionV2.Request).pipe(Effect.asVoid) + : Effect.void, + ) + yield* Effect.addFinalizer(() => unsubscribe) + const fiber = yield* service.ask(input).pipe(Effect.forkScoped) + return { fiber, request: yield* Deferred.await(asked) } +}) + +describe("QuestionV2", () => { + it.effect("publishes lifecycle events and settles a pending reply", () => + Effect.gen(function* () { + const service = yield* QuestionV2.Service + const events = yield* EventV2.Service + const published: EventV2.Payload[] = [] + const unsubscribe = yield* events.listen((event) => + Effect.sync(() => { + if (event.type.startsWith("question.v2.")) published.push(event) + }), + ) + yield* Effect.addFinalizer(() => unsubscribe) + const { fiber, request } = yield* waitForAsk(service, { sessionID, questions: [question] }) + + expect(request.id).toMatch(/^que_/) + expect(yield* service.list()).toEqual([request]) + yield* service.reply({ requestID: request.id, answers: [["One"]] }) + + expect(yield* Fiber.join(fiber)).toEqual([["One"]]) + expect(yield* service.list()).toEqual([]) + expect(published.map((event) => [event.type, event.data])).toEqual([ + [QuestionV2.Event.Asked.type, request], + [QuestionV2.Event.Replied.type, { sessionID, requestID: request.id, answers: [["One"]] }], + ]) + }), + ) + + it.effect("publishes rejection, fails the ask, and rejects unknown IDs", () => + Effect.gen(function* () { + const service = yield* QuestionV2.Service + const events = yield* EventV2.Service + const published: EventV2.Payload[] = [] + const unsubscribe = yield* events.listen((event) => + Effect.sync(() => { + if (event.type === QuestionV2.Event.Rejected.type) published.push(event) + }), + ) + yield* Effect.addFinalizer(() => unsubscribe) + const { fiber, request } = yield* waitForAsk(service, { sessionID, questions: [question] }) + + yield* service.reject(request.id) + const exit = yield* Fiber.await(fiber) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("QuestionV2.RejectedError") + expect(published.map((event) => event.data)).toEqual([{ sessionID, requestID: request.id }]) + + const unknown = QuestionV2.ID.ascending("que_unknown") + expect(yield* service.reply({ requestID: unknown, answers: [] }).pipe(Effect.flip)).toEqual( + new QuestionV2.NotFoundError({ requestID: unknown }), + ) + expect(yield* service.reject(unknown).pipe(Effect.flip)).toEqual( + new QuestionV2.NotFoundError({ requestID: unknown }), + ) + }), + ) + + it.effect("isolates pending requests by location-layer instance and rejects them on finalization", () => + Effect.gen(function* () { + const firstScope = yield* Scope.make() + const secondScope = yield* Scope.make() + const first = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), firstScope), QuestionV2.Service) + const second = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), secondScope), QuestionV2.Service) + const fiber = yield* first.ask({ sessionID, questions: [question] }).pipe(Effect.forkScoped) + yield* Effect.yieldNow + const request = (yield* first.list())[0]! + + expect(yield* second.list()).toEqual([]) + expect(yield* second.reply({ requestID: request.id, answers: [["One"]] }).pipe(Effect.flip)).toEqual( + new QuestionV2.NotFoundError({ requestID: request.id }), + ) + + yield* Scope.close(firstScope, Exit.void) + const exit = yield* Fiber.await(fiber) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("QuestionV2.RejectedError") + yield* Scope.close(secondScope, Exit.void) + }), + ) +}) diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts new file mode 100644 index 0000000000..5ce1912869 --- /dev/null +++ b/packages/core/test/session-create.test.ts @@ -0,0 +1,257 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer, Stream } from "effect" +import { AgentV2 } from "@opencode-ai/core/agent" +import { eq } from "drizzle-orm" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { EventTable } from "@opencode-ai/core/event/sql" +import { Location } from "@opencode-ai/core/location" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProjectV2 } from "@opencode-ai/core/project" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { SessionV1 } from "@opencode-ai/core/v1/session" +import { Prompt } from "@opencode-ai/core/session/prompt" +import { SessionProjector } from "@opencode-ai/core/session/projector" +import { SessionExecution } from "@opencode-ai/core/session/execution" +import { SessionInput } from "@opencode-ai/core/session/input" +import { SessionTable } from "@opencode-ai/core/session/sql" +import { SessionStore } from "@opencode-ai/core/session/store" +import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { testEffect } from "./lib/effect" + +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const projects = Layer.succeed( + ProjectV2.Service, + ProjectV2.Service.of({ + resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + directories: () => Effect.succeed([]), + commit: () => Effect.void, + }), +) +const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database)) +const store = SessionStore.layer.pipe(Layer.provide(database)) +const sessions = SessionV2.layer.pipe( + Layer.provide(events), + Layer.provide(database), + Layer.provide(store), + Layer.provide(projects), + Layer.provide(SessionExecution.noopLayer), +) +const it = testEffect( + Layer.mergeAll(database, events, projects, projector, store, SessionExecution.noopLayer, sessions), +) +const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) +const id = SessionV2.ID.create() + +describe("SessionV2.create", () => { + it.effect("derives stable namespaced external IDs", () => + Effect.sync(() => { + const input = { namespace: "opencord.agent-thread", key: "thread-1" } + + expect(SessionV2.ID.fromExternal(input)).toBe(SessionV2.ID.fromExternal(input)) + expect(SessionV2.ID.fromExternal(input)).toMatch(/^ses_[a-f0-9]{64}$/) + expect(SessionV2.ID.fromExternal({ ...input, namespace: "another-app" })).not.toBe( + SessionV2.ID.fromExternal(input), + ) + expect(SessionV2.ID.fromExternal({ namespace: "a:b", key: "c" })).not.toBe( + SessionV2.ID.fromExternal({ namespace: "a", key: "b:c" }), + ) + }), + ) + + it.effect("creates a fresh projected session when the ID is omitted", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + + const first = yield* session.create({ location }) + const second = yield* session.create({ location }) + + expect(second.id).not.toBe(first.id) + expect(yield* session.list()).toHaveLength(2) + }), + ) + + it.effect("returns the original session when the ID is retried", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const input = { id, location } + + const first = yield* session.create(input) + const retried = yield* session.create(input) + + expect(retried).toEqual(first) + expect(yield* session.list()).toEqual([first]) + }), + ) + + it.effect("stores supplied immutable create attributes", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const workspaceID = WorkspaceV2.ID.make("wrk_test") + const model = ModelV2.Ref.make({ + id: ModelV2.ID.make("sonnet"), + providerID: ProviderV2.ID.anthropic, + variant: ModelV2.VariantID.make("fast"), + }) + + expect( + yield* session.create({ + location: Location.Ref.make({ directory: location.directory, workspaceID }), + agent: AgentV2.ID.make("build"), + model, + }), + ).toMatchObject({ location: { directory: location.directory, workspaceID }, agent: "build", model }) + }), + ) + + it.effect("returns the existing Session when one ID is reused with different create arguments", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const created = yield* session.create({ id, location }) + const changed = [ + { id, location: Location.Ref.make({ directory: AbsolutePath.make("/other") }) }, + { id, location, agent: AgentV2.ID.make("build") }, + { + id, + location, + model: ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }), + }, + ] + + for (const input of changed) { + expect(yield* session.create(input)).toEqual(created) + } + expect(yield* session.list()).toHaveLength(1) + }), + ) + + it.effect("returns one recorded session to concurrent exact retries", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const input = { id, location } + + const created = yield* Effect.all([session.create(input), session.create(input)], { concurrency: "unbounded" }) + + expect(created[1]).toEqual(created[0]) + expect(yield* session.list()).toEqual([created[0]]) + }), + ) + + it.effect("returns the current Session projection after updates", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const { db } = yield* Database.Service + const input = { id, location } + const created = yield* session.create(input) + + yield* db.update(SessionTable).set({ agent: "build" }).where(eq(SessionTable.id, id)).run().pipe(Effect.orDie) + + expect(yield* session.create(input)).toMatchObject({ id: created.id, agent: "build" }) + }), + ) + + it.effect("returns the current Session projection after projected updates", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + const input = { id, location } + const created = yield* session.create(input) + + yield* events.publish(SessionV1.Event.Updated, { + sessionID: id, + info: SessionV1.SessionInfo.make({ + id, + slug: "updated", + version: "test", + projectID: created.projectID, + directory: created.location.directory, + title: "updated", + agent: "build", + time: { created: 0, updated: 1 }, + }), + }) + + expect(yield* session.create(input)).toMatchObject({ id, agent: "build" }) + }), + ) + + it.effect("persists creation through the existing legacy created event", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const { db } = yield* Database.Service + const created = yield* session.create({ location }) + + expect( + yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).all().pipe(Effect.orDie), + ).toMatchObject([{ type: EventV2.versionedType(SessionV1.Event.Created.type, 1) }]) + }), + ) + + it.effect("persists caller-ID creation through the existing created event", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const { db } = yield* Database.Service + const created = yield* session.create({ id, location }) + + expect( + yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).get().pipe(Effect.orDie), + ).toMatchObject({ + data: { sessionID: id }, + }) + }), + ) + + it.effect("omits legacy creation rows from the V2 Session event stream", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + const { db } = yield* Database.Service + const created = yield* session.create({ location }) + yield* session.prompt({ sessionID: created.id, prompt: new Prompt({ text: "Hello" }), resume: false }) + yield* SessionInput.promoteSteers(db, events, created.id) + + expect( + Array.from(yield* session.events({ sessionID: created.id }).pipe(Stream.take(1), Stream.runCollect)), + ).toMatchObject([{ cursor: 1, event: { type: "session.next.prompted", data: { prompt: { text: "Hello" } } } }]) + }), + ) + + it.effect("does not mask unrelated created projector defects", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const event = yield* EventV2.Service + const defect = new Error("unrelated projector defect") + yield* event.project(SessionV1.Event.Created, () => Effect.die(defect)) + + expect(yield* session.create({ id, location }).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect) + }), + ) + + it.effect("reports unfinished Session operations as unavailable", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const created = yield* session.create({ location }) + const unavailable = (effect: Effect.Effect) => + effect.pipe( + Effect.flip, + Effect.map((error) => (error instanceof SessionV2.OperationUnavailableError ? error.operation : "not-found")), + ) + + expect(yield* unavailable(session.move({ sessionID: created.id, location }))).toBe("move") + expect(yield* unavailable(session.shell({ sessionID: created.id, command: "pwd" }))).toBe("shell") + expect(yield* unavailable(session.skill({ sessionID: created.id, skill: "review" }))).toBe("skill") + expect(yield* unavailable(session.switchAgent({ sessionID: created.id, agent: "build" }))).toBe("switchAgent") + expect( + yield* unavailable( + session.switchModel({ + sessionID: created.id, + model: ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }), + }), + ), + ).toBe("switchModel") + }), + ) +}) diff --git a/packages/core/test/session-projector.test.ts b/packages/core/test/session-projector.test.ts new file mode 100644 index 0000000000..cbdeea4b23 --- /dev/null +++ b/packages/core/test/session-projector.test.ts @@ -0,0 +1,441 @@ +import { describe, expect } from "bun:test" +import { DateTime, Effect, Layer, Schema } from "effect" +import { asc, eq } from "drizzle-orm" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { ModelV2 } from "@opencode-ai/core/model" +import { Project } from "@opencode-ai/core/project" +import { ProjectTable } from "@opencode-ai/core/project/sql" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { SessionEvent } from "@opencode-ai/core/session/event" +import { SessionMessage } from "@opencode-ai/core/session/message" +import { Prompt } from "@opencode-ai/core/session/prompt" +import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater" +import { SessionProjector } from "@opencode-ai/core/session/projector" +import { SessionExecution } from "@opencode-ai/core/session/execution" +import { SessionInput } from "@opencode-ai/core/session/input" +import { SessionStore } from "@opencode-ai/core/session/store" +import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql" +import { testEffect } from "./lib/effect" + +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database)) +const it = testEffect(Layer.mergeAll(database, events, projector)) +const sessionID = SessionV2.ID.make("ses_projector_test") +const created = DateTime.makeUnsafe(0) +const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") } +const encodeMessage = Schema.encodeSync(SessionMessage.Message) + +const assistantRow = ( + id: SessionMessage.ID, + seq: number, + time: { created: DateTime.Utc; completed?: DateTime.Utc } = { created }, +) => { + const { + id: _, + type, + ...data + } = encodeMessage(new SessionMessage.Assistant({ id, type: "assistant", agent: "build", model, content: [], time })) + return { id, session_id: sessionID, type, seq, time_created: DateTime.toEpochMillis(time.created), data } +} + +describe("SessionProjector", () => { + it.effect("orders projected messages and context by durable aggregate sequence", () => + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "test", + directory: "/project", + title: "test", + version: "test", + }) + .run() + .pipe(Effect.orDie) + const events = yield* EventV2.Service + + yield* events.publish( + SessionEvent.Prompted, + { sessionID, timestamp: created, prompt: new Prompt({ text: "first" }), delivery: "steer" }, + { id: SessionMessage.ID.make("evt_z") }, + ) + yield* events.publish( + SessionEvent.Prompted, + { sessionID, timestamp: created, prompt: new Prompt({ text: "second" }), delivery: "steer" }, + { id: SessionMessage.ID.make("evt_a") }, + ) + + const sessions = yield* SessionV2.Service + const firstPage = yield* sessions.messages({ sessionID, limit: 1, order: "asc" }) + expect(firstPage.map((message) => (message.type === "user" ? message.text : message.type))).toEqual(["first"]) + const secondPage = yield* sessions.messages({ + sessionID, + limit: 1, + order: "asc", + cursor: { id: firstPage[0]!.id, direction: "next" }, + }) + expect(secondPage.map((message) => (message.type === "user" ? message.text : message.type))).toEqual(["second"]) + expect( + ( + yield* sessions.messages({ + sessionID, + limit: 1, + order: "asc", + cursor: { id: secondPage[0]!.id, direction: "previous" }, + }) + ).map((message) => (message.type === "user" ? message.text : message.type)), + ).toEqual(["first"]) + expect( + (yield* sessions.context(sessionID)).map((message) => + message.type === "user" ? message.text : message.type, + ), + ).toEqual(["first", "second"]) + }).pipe( + Effect.provide( + SessionV2.layer.pipe( + Layer.provide(events), + Layer.provide(database), + Layer.provide(Project.defaultLayer), + Layer.provide(SessionStore.layer.pipe(Layer.provide(database))), + Layer.provide(SessionExecution.noopLayer), + ), + ), + ), + ) + + it.effect("marks an admitted inbox row promoted with the Prompted event sequence", () => + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "test", + directory: "/project", + title: "test", + version: "test", + }) + .run() + .pipe(Effect.orDie) + const events = yield* EventV2.Service + const id = SessionMessage.ID.make("evt_admitted") + yield* SessionInput.admit(db, { id, sessionID, prompt: new Prompt({ text: "promote me" }), delivery: "steer" }) + + const event = yield* events.publish( + SessionEvent.Prompted, + { sessionID, timestamp: created, prompt: new Prompt({ text: "promote me" }), delivery: "steer" }, + { id }, + ) + + expect( + yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie), + ).toMatchObject({ promoted_seq: event.seq }) + }), + ) + + it.effect("projects durable context messages supported by the updater", () => + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "test", + directory: "/project", + title: "test", + version: "test", + }) + .run() + .pipe(Effect.orDie) + const events = yield* EventV2.Service + + yield* events.publish(SessionEvent.AgentSwitched, { sessionID, timestamp: created, agent: "build" }) + yield* events.publish(SessionEvent.ModelSwitched, { sessionID, timestamp: created, model }) + yield* events.publish(SessionEvent.Synthetic, { sessionID, timestamp: created, text: "synthetic context" }) + yield* events.publish(SessionEvent.Shell.Started, { + sessionID, + timestamp: created, + callID: "shell-1", + command: "pwd", + }) + yield* events.publish(SessionEvent.Shell.Ended, { + sessionID, + timestamp: DateTime.makeUnsafe(1), + callID: "shell-1", + output: "/project", + }) + yield* events.publish(SessionEvent.Compaction.Started, { sessionID, timestamp: created, reason: "manual" }) + yield* events.publish(SessionEvent.Compaction.Delta, { sessionID, timestamp: created, text: "partial" }) + yield* events.publish(SessionEvent.Compaction.Ended, { + sessionID, + timestamp: DateTime.makeUnsafe(1), + text: "summary", + include: "msg-1", + }) + + const rows = yield* db + .select() + .from(SessionMessageTable) + .where(eq(SessionMessageTable.session_id, sessionID)) + .orderBy(asc(SessionMessageTable.seq)) + .all() + .pipe(Effect.orDie) + const messages = rows.map((row) => + Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }), + ) + + expect(messages.map((message) => message.type)).toEqual([ + "agent-switched", + "model-switched", + "synthetic", + "shell", + "compaction", + ]) + expect(messages.find((message) => message.type === "shell")).toMatchObject({ + output: "/project", + time: { completed: DateTime.makeUnsafe(1) }, + }) + expect(messages.find((message) => message.type === "compaction")).toMatchObject({ + summary: "summary", + include: "msg-1", + }) + expect(yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie)).toMatchObject({ + agent: "build", + model, + time_updated: DateTime.toEpochMillis(created), + }) + }), + ) + + it.effect("rejects a Prompted event that conflicts with an admitted inbox row", () => + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "test", + directory: "/project", + title: "test", + version: "test", + }) + .run() + .pipe(Effect.orDie) + const events = yield* EventV2.Service + const id = SessionMessage.ID.make("evt_conflict") + yield* SessionInput.admit(db, { id, sessionID, prompt: new Prompt({ text: "admitted" }), delivery: "steer" }) + + const exit = yield* events + .publish( + SessionEvent.Prompted, + { sessionID, timestamp: created, prompt: new Prompt({ text: "different" }), delivery: "steer" }, + { id }, + ) + .pipe(Effect.exit) + + expect(String(exit)).toContain("Prompt projection conflicts with admitted input") + expect( + yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie), + ).toMatchObject({ promoted_seq: null }) + }), + ) + + it.effect("rejects a Prompted delivery mode that conflicts with an admitted inbox row", () => + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db.insert(ProjectTable).values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }).run().pipe(Effect.orDie) + yield* db.insert(SessionTable).values({ id: sessionID, project_id: Project.ID.global, slug: "test", directory: "/project", title: "test", version: "test" }).run().pipe(Effect.orDie) + const events = yield* EventV2.Service + const id = SessionMessage.ID.make("evt_delivery_conflict") + const prompt = new Prompt({ text: "admitted" }) + yield* SessionInput.admit(db, { id, sessionID, prompt, delivery: "queue" }) + + const exit = yield* events.publish(SessionEvent.Prompted, { sessionID, timestamp: created, prompt, delivery: "steer" }, { id }).pipe(Effect.exit) + + expect(String(exit)).toContain("Prompt projection conflicts with admitted input") + expect(yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie)).toMatchObject({ delivery: "queue", promoted_seq: null }) + }), + ) + + it.effect("does not revive a stale incomplete in-memory assistant projection", () => + Effect.gen(function* () { + const stale = new SessionMessage.Assistant({ + id: SessionMessage.ID.make("evt_assistant_stale"), + type: "assistant", + agent: "build", + model, + content: [], + time: { created }, + }) + const completed = new SessionMessage.Assistant({ + id: SessionMessage.ID.make("evt_assistant_completed"), + type: "assistant", + agent: "build", + model, + content: [], + time: { created: DateTime.makeUnsafe(1), completed: DateTime.makeUnsafe(2) }, + }) + + expect( + yield* SessionMessageUpdater.memory({ messages: [stale, completed] }).getCurrentAssistant(), + ).toBeUndefined() + }), + ) + + it.effect("updates only the newest incomplete assistant projection", () => + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "test", + directory: "/project", + title: "test", + version: "test", + }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionMessageTable) + .values([ + assistantRow(SessionMessage.ID.make("evt_assistant_1"), 0), + assistantRow(SessionMessage.ID.make("evt_assistant_2"), 1), + ]) + .run() + .pipe(Effect.orDie) + + const service = yield* EventV2.Service + yield* service.publish(SessionEvent.Step.Ended, { + sessionID, + timestamp: DateTime.makeUnsafe(1), + assistantMessageID: SessionMessage.ID.make("evt_assistant_2"), + finish: "stop", + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + }) + + const rows = yield* db + .select() + .from(SessionMessageTable) + .where(eq(SessionMessageTable.session_id, sessionID)) + .orderBy(asc(SessionMessageTable.id)) + .all() + .pipe(Effect.orDie) + const messages = rows.map((row) => + Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }), + ) + expect(messages[0]).not.toHaveProperty("time.completed") + expect(messages[1]).toMatchObject({ + type: "assistant", + finish: "stop", + time: { completed: DateTime.makeUnsafe(1) }, + }) + }), + ) + + it.effect("does not revive a stale incomplete assistant projection", () => + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "test", + directory: "/project", + title: "test", + version: "test", + }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionMessageTable) + .values([ + assistantRow(SessionMessage.ID.make("evt_assistant_stale"), 0), + assistantRow(SessionMessage.ID.make("evt_assistant_completed"), 1, { + created: DateTime.makeUnsafe(1), + completed: DateTime.makeUnsafe(2), + }), + ]) + .run() + .pipe(Effect.orDie) + + const service = yield* EventV2.Service + yield* service.publish(SessionEvent.Text.Started, { + sessionID, + timestamp: DateTime.makeUnsafe(3), + textID: "text-stale", + }) + + const rows = yield* db + .select() + .from(SessionMessageTable) + .where(eq(SessionMessageTable.session_id, sessionID)) + .orderBy(asc(SessionMessageTable.id)) + .all() + .pipe(Effect.orDie) + const messages = rows.map((row) => + Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }), + ) + expect(messages).toEqual([ + new SessionMessage.Assistant({ + id: SessionMessage.ID.make("evt_assistant_completed"), + type: "assistant", + agent: "build", + model, + content: [], + time: { created: DateTime.makeUnsafe(1), completed: DateTime.makeUnsafe(2) }, + }), + new SessionMessage.Assistant({ + id: SessionMessage.ID.make("evt_assistant_stale"), + type: "assistant", + agent: "build", + model, + content: [], + time: { created }, + }), + ]) + }), + ) +}) diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts new file mode 100644 index 0000000000..d97ab281d0 --- /dev/null +++ b/packages/core/test/session-prompt.test.ts @@ -0,0 +1,461 @@ +import { describe, expect } from "bun:test" +import { DateTime, Effect, Fiber, Layer, Stream } from "effect" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { SessionEvent } from "@opencode-ai/core/session/event" +import { Project } from "@opencode-ai/core/project" +import { ProjectTable } from "@opencode-ai/core/project/sql" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { Prompt } from "@opencode-ai/core/session/prompt" +import { SessionMessage } from "@opencode-ai/core/session/message" +import { SessionProjector } from "@opencode-ai/core/session/projector" +import { SessionExecution } from "@opencode-ai/core/session/execution" +import { SessionInput } from "@opencode-ai/core/session/input" +import { SessionInputTable, SessionTable } from "@opencode-ai/core/session/sql" +import { SessionStore } from "@opencode-ai/core/session/store" +import { testEffect } from "./lib/effect" + +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database)) +const store = SessionStore.layer.pipe(Layer.provide(database)) +const executionCalls: SessionV2.ID[] = [] +const wakeCalls: SessionV2.ID[] = [] +const execution = Layer.succeed( + SessionExecution.Service, + SessionExecution.Service.of({ + resume: (sessionID) => + Effect.sync(() => { + executionCalls.push(sessionID) + }), + wake: (sessionID) => + Effect.sync(() => { + wakeCalls.push(sessionID) + }), + }), +) +const sessions = SessionV2.layer.pipe( + Layer.provide(events), + Layer.provide(database), + Layer.provide(store), + Layer.provide(Project.defaultLayer), + Layer.provide(execution), +) +const it = testEffect(Layer.mergeAll(database, events, projector, store, execution, sessions)) +const sessionID = SessionV2.ID.make("ses_prompt_test") +const messageID = SessionMessage.ID.create() + +const setup = Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "test", + directory: "/project", + title: "test", + version: "test", + }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) +}) + +const admitted = (id: SessionMessage.ID) => Database.Service.use(({ db }) => SessionInput.find(db, id)) +const admittedCount = Database.Service.use(({ db }) => + db + .select() + .from(SessionInputTable) + .all() + .pipe( + Effect.orDie, + Effect.map((rows) => rows.length), + ), +) + +describe("SessionV2.prompt", () => { + it.effect("delegates execution continuation through SessionExecution", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + executionCalls.length = 0 + wakeCalls.length = 0 + yield* session.resume(sessionID) + expect(executionCalls).toEqual([sessionID]) + expect(wakeCalls).toEqual([]) + }), + ) + + it.effect("durably admits one user message before transcript promotion", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + + const message = yield* session.prompt({ + sessionID, + prompt: new Prompt({ text: "Fix the failing tests" }), + resume: false, + }) + + expect(message.type).toBe("user") + expect(message.text).toBe("Fix the failing tests") + expect(yield* session.messages({ sessionID })).toEqual([]) + expect(yield* admitted(message.id)).toMatchObject({ + id: message.id, + sessionID, + prompt: { text: "Fix the failing tests" }, + delivery: "steer", + }) + }), + ) + + it.effect("streams durable Session events after an aggregate cursor", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + const { db } = yield* Database.Service + const fiber = yield* session.events({ sessionID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) + yield* Effect.yieldNow + + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* SessionInput.promoteSteers(db, events, sessionID) + const streamed = Array.from(yield* Fiber.join(fiber)) + + expect( + streamed.map((event) => [event.cursor, event.event.type, (event.event.data as { prompt: Prompt }).prompt.text]), + ).toEqual([ + [EventV2.Cursor.make(0), "session.next.prompted", "First"], + [EventV2.Cursor.make(1), "session.next.prompted", "Second"], + ]) + expect( + Array.from( + yield* session.events({ sessionID, after: streamed[0]!.cursor }).pipe(Stream.take(1), Stream.runCollect), + ).map((event) => [event.cursor, (event.event.data as { prompt: Prompt }).prompt.text]), + ).toEqual([[EventV2.Cursor.make(1), "Second"]]) + }), + ) + + it.effect("resumes through a recorded message without appending another prompt", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const message = yield* session.prompt({ + sessionID, + prompt: new Prompt({ text: "Fix the failing tests" }), + resume: false, + }) + + executionCalls.length = 0 + wakeCalls.length = 0 + yield* session.resume(sessionID) + + expect(yield* session.messages({ sessionID })).toEqual([]) + expect(yield* admitted(message.id)).not.toHaveProperty("promotedSeq") + expect(executionCalls).toEqual([sessionID]) + expect(wakeCalls).toEqual([]) + }), + ) + + it.effect("records distinct messages when the ID is omitted", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const input = { sessionID, prompt: new Prompt({ text: "Fix the failing tests" }), resume: false } + + const first = yield* session.prompt(input) + const second = yield* session.prompt(input) + + expect(second.id).not.toBe(first.id) + expect(yield* session.messages({ sessionID })).toEqual([]) + expect(yield* admittedCount).toBe(2) + }), + ) + + it.effect("returns the original recorded message when the ID is retried", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const input = { + sessionID, + id: messageID, + prompt: new Prompt({ text: "Fix the failing tests" }), + resume: false, + } + + const first = yield* session.prompt(input) + const retried = yield* session.prompt(input) + + expect(retried).toEqual(first) + expect(yield* session.messages({ sessionID })).toEqual([]) + expect(yield* admittedCount).toBe(1) + }), + ) + + it.effect("wakes execution when an exact prompt retry recovers a committed message", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const input = { + sessionID, + id: messageID, + prompt: new Prompt({ text: "Recover committed prompt" }), + resume: false, + } + const first = yield* session.prompt(input) + wakeCalls.length = 0 + + const retried = yield* session.prompt({ ...input, resume: true }) + + expect(retried).toEqual(first) + expect(wakeCalls).toEqual([sessionID]) + }), + ) + + it.effect("rejects reuse of one ID with a different prompt", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + + yield* session.prompt({ + sessionID, + id: messageID, + prompt: new Prompt({ text: "Fix the failing tests" }), + }) + const failure = yield* session + .prompt({ + sessionID, + id: messageID, + prompt: new Prompt({ text: "Delete the failing tests" }), + resume: false, + }) + .pipe(Effect.flip) + + expect(failure._tag).toBe("Session.PromptConflictError") + expect(yield* session.messages({ sessionID })).toHaveLength(0) + expect(yield* admittedCount).toBe(1) + }), + ) + + it.effect("rejects reuse of one ID with a different delivery mode", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + + yield* session.prompt({ + id: messageID, + sessionID, + prompt: new Prompt({ text: "Fix the failing tests" }), + resume: false, + }) + const failure = yield* session + .prompt({ + id: messageID, + sessionID, + prompt: new Prompt({ text: "Fix the failing tests" }), + delivery: "queue", + resume: false, + }) + .pipe(Effect.flip) + + expect(failure._tag).toBe("Session.PromptConflictError") + }), + ) + + it.effect("does not match pending inputs when no delivery modes are eligible", () => + Effect.gen(function* () { + yield* setup + const { db } = yield* Database.Service + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Wait" }), resume: false }) + + expect(yield* SessionInput.hasPending(db, sessionID, [])).toBe(false) + }), + ) + + it.effect("returns one recorded message to concurrent exact retries", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const input = { + sessionID, + id: messageID, + prompt: new Prompt({ text: "Fix the failing tests" }), + resume: false, + } + + const messages = yield* Effect.all([session.prompt(input), session.prompt(input)], { concurrency: "unbounded" }) + + expect(messages[1]).toEqual(messages[0]) + expect(yield* session.messages({ sessionID })).toEqual([]) + expect(yield* admittedCount).toBe(1) + }), + ) + + it.effect("reconciles an existing projected prompt into a promoted inbox record", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + const prompt = new Prompt({ text: "Historical prompt" }) + yield* events.publish( + SessionEvent.Prompted, + { sessionID, timestamp: yield* DateTime.now, prompt, delivery: "steer" }, + { id: messageID }, + ) + + const retried = yield* session.prompt({ id: messageID, sessionID, prompt, resume: false }) + + expect(retried).toMatchObject({ id: messageID, text: "Historical prompt" }) + expect(yield* admitted(messageID)).toHaveProperty("promotedSeq") + }), + ) + + it.effect("reconciles an existing projected queued prompt with its delivery mode", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + const prompt = new Prompt({ text: "Historical queued prompt" }) + yield* events.publish( + SessionEvent.Prompted, + { sessionID, timestamp: yield* DateTime.now, prompt, delivery: "queue" }, + { id: messageID }, + ) + + const retried = yield* session.prompt({ id: messageID, sessionID, prompt, delivery: "queue", resume: false }) + + expect(retried).toMatchObject({ id: messageID, text: "Historical queued prompt" }) + expect(yield* admitted(messageID)).toMatchObject({ delivery: "queue" }) + }), + ) + + it.effect("rejects an input ID already used by a durable non-prompt event", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + yield* events.publish( + SessionEvent.Synthetic, + { sessionID, timestamp: yield* DateTime.now, text: "Collision" }, + { id: messageID }, + ) + + const failure = yield* session + .prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "Collision" }), resume: false }) + .pipe(Effect.flip) + + expect(failure._tag).toBe("Session.PromptConflictError") + expect(yield* admitted(messageID)).toBeUndefined() + }), + ) + + it.effect("rejects a durable event ID reserved by an admitted prompt without poisoning promotion", () => + Effect.gen(function* () { + yield* setup + const { db } = yield* Database.Service + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + const prompt = new Prompt({ text: "Reserved prompt" }) + yield* session.prompt({ id: messageID, sessionID, prompt, resume: false }) + + const failure = yield* events + .publish( + SessionEvent.Synthetic, + { sessionID, timestamp: yield* DateTime.now, text: "Conflicting synthetic" }, + { id: messageID }, + ) + .pipe(Effect.catchDefect(Effect.succeed)) + + expect(failure).toBe("Durable event conflicts with admitted prompt input") + expect(yield* admitted(messageID)).not.toHaveProperty("promotedSeq") + expect(yield* session.messages({ sessionID })).toEqual([]) + + yield* SessionInput.promoteSteers(db, events, sessionID) + + expect(yield* admitted(messageID)).toMatchObject({ promotedSeq: 0 }) + expect(yield* session.messages({ sessionID })).toMatchObject([{ id: messageID, type: "user", text: "Reserved prompt" }]) + }), + ) + + it.effect("rejects reuse of one globally unique message ID across sessions", () => + Effect.gen(function* () { + yield* setup + const { db } = yield* Database.Service + const session = yield* SessionV2.Service + const other = SessionV2.ID.make("ses_prompt_other") + yield* db + .insert(SessionTable) + .values({ + id: other, + project_id: Project.ID.global, + slug: "other", + directory: "/project", + title: "other", + version: "test", + }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + const prompt = new Prompt({ text: "Fix the failing tests" }) + + yield* session.prompt({ id: messageID, sessionID, prompt, resume: false }) + const failure = yield* session + .prompt({ id: messageID, sessionID: other, prompt, resume: false }) + .pipe(Effect.flip) + + expect(failure).toMatchObject({ _tag: "Session.PromptConflictError", sessionID: other, messageID }) + }), + ) + + it.effect("starts execution by default after recording the prompt", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + executionCalls.length = 0 + wakeCalls.length = 0 + + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run by default" }) }) + + expect(executionCalls).toEqual([]) + expect(wakeCalls).toEqual([sessionID]) + }), + ) + + it.effect("starts execution when resume is explicitly true", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + executionCalls.length = 0 + wakeCalls.length = 0 + + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run explicitly" }), resume: true }) + + expect(executionCalls).toEqual([]) + expect(wakeCalls).toEqual([sessionID]) + }), + ) + + it.effect("only records the prompt when resume is false", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + executionCalls.length = 0 + wakeCalls.length = 0 + + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Do not run" }), resume: false }) + + expect(executionCalls).toEqual([]) + expect(wakeCalls).toEqual([]) + }), + ) +}) diff --git a/packages/core/test/session-run-coordinator.test.ts b/packages/core/test/session-run-coordinator.test.ts new file mode 100644 index 0000000000..8f67662ccd --- /dev/null +++ b/packages/core/test/session-run-coordinator.test.ts @@ -0,0 +1,384 @@ +import { describe, expect } from "bun:test" +import { Cause, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect" +import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator" +import { testEffect } from "./lib/effect" + +const it = testEffect(Layer.empty) + +describe("SessionRunCoordinator", () => { + it.effect("joins concurrent resumes for one key", () => + Effect.scoped( + Effect.gen(function* () { + const gate = yield* Deferred.make() + let runs = 0 + const coordinator = yield* SessionRunCoordinator.make({ + drain: () => Effect.sync(() => runs++).pipe(Effect.andThen(Deferred.await(gate))), + }) + + const first = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Effect.yieldNow + const second = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Effect.yieldNow + + expect(runs).toBe(1) + yield* Deferred.succeed(gate, undefined) + yield* Fiber.join(first) + yield* Fiber.join(second) + expect(runs).toBe(1) + }), + ), + ) + + it.effect("starts a drain when woken while idle", () => + Effect.scoped( + Effect.gen(function* () { + const drained = yield* Deferred.make() + const coordinator = yield* SessionRunCoordinator.make({ drain: () => Deferred.succeed(drained, undefined) }) + + yield* coordinator.wake("session") + yield* Deferred.await(drained) + }), + ), + ) + + it.effect("coalesces wakes received during an active run", () => + Effect.scoped( + Effect.gen(function* () { + const gate = yield* Deferred.make() + let runs = 0 + const coordinator = yield* SessionRunCoordinator.make({ + drain: () => + Effect.sync(() => ++runs).pipe(Effect.flatMap((run) => (run === 1 ? Deferred.await(gate) : Effect.void))), + }) + + const first = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Effect.yieldNow + yield* Effect.all([coordinator.wake("session"), coordinator.wake("session"), coordinator.wake("session")], { + concurrency: "unbounded", + }) + yield* Deferred.succeed(gate, undefined) + yield* Fiber.join(first) + + expect(runs).toBe(2) + }), + ), + ) + + it.effect("waits for a coalesced ownership chain to become idle", () => + Effect.scoped( + Effect.gen(function* () { + const firstGate = yield* Deferred.make() + const secondGate = yield* Deferred.make() + const secondStarted = yield* Deferred.make() + const idleSettled = yield* Deferred.make() + let runs = 0 + const coordinator = yield* SessionRunCoordinator.make({ + drain: () => + Effect.sync(() => ++runs).pipe( + Effect.flatMap((run) => + run === 1 + ? Deferred.await(firstGate) + : Deferred.succeed(secondStarted, undefined).pipe(Effect.andThen(Deferred.await(secondGate))), + ), + ), + }) + + yield* coordinator.wake("session") + const idle = yield* coordinator + .awaitIdle("session") + .pipe(Effect.andThen(Deferred.succeed(idleSettled, undefined)), Effect.forkChild) + yield* coordinator.wake("session") + yield* Deferred.succeed(firstGate, undefined) + yield* Deferred.await(secondStarted) + expect(yield* Deferred.isDone(idleSettled)).toBeFalse() + yield* Deferred.succeed(secondGate, undefined) + yield* Fiber.join(idle) + + expect(runs).toBe(2) + }), + ), + ) + + it.effect("reports the first defect after a failed chain becomes idle", () => + Effect.scoped( + Effect.gen(function* () { + const firstGate = yield* Deferred.make() + const secondGate = yield* Deferred.make() + const secondStarted = yield* Deferred.make() + const defect = new Error("defect") + let runs = 0 + const coordinator = yield* SessionRunCoordinator.make({ + drain: () => + Effect.sync(() => ++runs).pipe( + Effect.flatMap((run) => + run === 1 + ? Deferred.await(firstGate).pipe(Effect.andThen(Effect.die(defect))) + : Deferred.succeed(secondStarted, undefined).pipe(Effect.andThen(Deferred.await(secondGate))), + ), + ), + }) + + yield* coordinator.wake("session") + const idle = yield* coordinator + .awaitIdle("session") + .pipe(Effect.catchDefect(Effect.succeed), Effect.forkChild({ startImmediately: true })) + yield* coordinator.wake("session") + yield* Deferred.succeed(firstGate, undefined) + yield* Deferred.await(secondStarted) + yield* Deferred.succeed(secondGate, undefined) + + expect(yield* Fiber.join(idle)).toBe(defect) + expect(runs).toBe(2) + }), + ), + ) + + it.effect("runs again when woken during the coalesced drain", () => + Effect.scoped( + Effect.gen(function* () { + const firstGate = yield* Deferred.make() + const secondStarted = yield* Deferred.make() + const secondGate = yield* Deferred.make() + let runs = 0 + const coordinator = yield* SessionRunCoordinator.make({ + drain: () => + Effect.sync(() => ++runs).pipe( + Effect.flatMap((run) => + run === 1 + ? Deferred.await(firstGate) + : run === 2 + ? Deferred.succeed(secondStarted, undefined).pipe(Effect.andThen(Deferred.await(secondGate))) + : Effect.void, + ), + ), + }) + + const first = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Effect.yieldNow + yield* coordinator.wake("session") + yield* Deferred.succeed(firstGate, undefined) + yield* Deferred.await(secondStarted) + yield* coordinator.wake("session") + yield* Deferred.succeed(secondGate, undefined) + yield* Fiber.join(first) + + expect(runs).toBe(3) + }), + ), + ) + + it.effect("starts one successor after a wake races with failure", () => + Effect.scoped( + Effect.gen(function* () { + const gate = yield* Deferred.make() + const failure = new Error("failed") + let runs = 0 + const coordinator = yield* SessionRunCoordinator.make({ + drain: () => + Effect.sync(() => ++runs).pipe( + Effect.flatMap((run) => + run === 1 ? Deferred.await(gate).pipe(Effect.andThen(Effect.fail(failure))) : Effect.void, + ), + ), + }) + + const first = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Effect.yieldNow + yield* coordinator.wake("session") + yield* Deferred.succeed(gate, undefined) + expect(yield* Fiber.join(first).pipe(Effect.flip)).toBe(failure) + + yield* Effect.yieldNow + expect(runs).toBe(2) + }), + ), + ) + + it.effect("upgrades an active wake when an explicit run joins it", () => + Effect.scoped( + Effect.gen(function* () { + const wakeStarted = yield* Deferred.make() + const wakeGate = yield* Deferred.make() + const modes: SessionRunCoordinator.Mode[] = [] + const coordinator = yield* SessionRunCoordinator.make({ + drain: (_key, mode) => + Effect.sync(() => modes.push(mode)).pipe( + Effect.andThen( + mode === "wake" + ? Deferred.succeed(wakeStarted, undefined).pipe(Effect.andThen(Deferred.await(wakeGate))) + : Effect.void, + ), + ), + }) + + yield* coordinator.wake("session") + yield* Deferred.await(wakeStarted) + const run = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Deferred.succeed(wakeGate, undefined) + yield* Fiber.join(run) + + expect(modes).toEqual(["wake", "run"]) + }), + ), + ) + + it.effect("upgrades a recursive wake drain when an explicit run joins it", () => + Effect.scoped( + Effect.gen(function* () { + const runGate = yield* Deferred.make() + const wakeStarted = yield* Deferred.make() + const wakeGate = yield* Deferred.make() + const forcedStarted = yield* Deferred.make() + const modes: SessionRunCoordinator.Mode[] = [] + const coordinator = yield* SessionRunCoordinator.make({ + drain: (_key, mode) => + Effect.gen(function* () { + modes.push(mode) + if (modes.length === 1) return yield* Deferred.await(runGate) + if (modes.length === 2) + return yield* Deferred.succeed(wakeStarted, undefined).pipe(Effect.andThen(Deferred.await(wakeGate))) + yield* Deferred.succeed(forcedStarted, undefined) + }), + }) + + const first = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Effect.yieldNow + yield* coordinator.wake("session") + yield* Deferred.succeed(runGate, undefined) + yield* Deferred.await(wakeStarted) + const second = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Deferred.succeed(wakeGate, undefined) + yield* Deferred.await(forcedStarted) + yield* Fiber.join(first) + yield* Fiber.join(second) + + expect(modes).toEqual(["run", "wake", "run"]) + }), + ), + ) + + it.effect("propagates an upgraded explicit run failure before a successful advisory successor", () => + Effect.scoped( + Effect.gen(function* () { + const wakeStarted = yield* Deferred.make() + const wakeGate = yield* Deferred.make() + const runStarted = yield* Deferred.make() + const runGate = yield* Deferred.make() + const advisoryStarted = yield* Deferred.make() + const failure = new Error("explicit run failed") + const modes: SessionRunCoordinator.Mode[] = [] + const coordinator = yield* SessionRunCoordinator.make({ + drain: (_key, mode) => + Effect.sync(() => modes.push(mode)).pipe( + Effect.flatMap((run) => + run === 1 + ? Deferred.succeed(wakeStarted, undefined).pipe(Effect.andThen(Deferred.await(wakeGate))) + : run === 2 + ? Deferred.succeed(runStarted, undefined).pipe( + Effect.andThen(Deferred.await(runGate)), + Effect.andThen(Effect.fail(failure)), + ) + : Deferred.succeed(advisoryStarted, undefined), + ), + ), + }) + + yield* coordinator.wake("session") + yield* Deferred.await(wakeStarted) + const run = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Deferred.succeed(wakeGate, undefined) + yield* Deferred.await(runStarted) + yield* coordinator.wake("session") + yield* Deferred.succeed(runGate, undefined) + yield* Deferred.await(advisoryStarted) + + expect(yield* Fiber.join(run).pipe(Effect.flip)).toBe(failure) + expect(modes).toEqual(["wake", "run", "wake"]) + }), + ), + ) + + it.effect("settles active callers when its owning scope closes", () => + Effect.gen(function* () { + const scope = yield* Scope.make() + const started = yield* Deferred.make() + const coordinator = yield* SessionRunCoordinator.make({ + drain: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)), + }).pipe(Scope.provide(scope)) + + const run = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Deferred.await(started) + const idle = yield* coordinator.awaitIdle("session").pipe(Effect.forkChild) + yield* Effect.yieldNow + yield* Scope.close(scope, Exit.void) + + const runExit = yield* Fiber.await(run) + const idleExit = yield* Fiber.await(idle) + expect(Exit.isFailure(runExit) && Cause.hasInterruptsOnly(runExit.cause)).toBeTrue() + expect(Exit.isSuccess(idleExit)).toBeTrue() + }), + ) + + it.effect("does not start work after its owning scope closes", () => + Effect.gen(function* () { + const scope = yield* Scope.make() + let runs = 0 + const coordinator = yield* SessionRunCoordinator.make({ + drain: () => Effect.sync(() => runs++), + }).pipe(Scope.provide(scope)) + yield* Scope.close(scope, Exit.void) + + yield* coordinator.wake("session") + yield* coordinator.awaitIdle("session") + const runExit = yield* coordinator.run("session").pipe(Effect.exit) + + expect(Exit.isFailure(runExit) && Cause.hasInterruptsOnly(runExit.cause)).toBeTrue() + expect(runs).toBe(0) + }), + ) + + it.effect("does not cancel the owner when one joined waiter is interrupted", () => + Effect.scoped( + Effect.gen(function* () { + const gate = yield* Deferred.make() + let runs = 0 + const coordinator = yield* SessionRunCoordinator.make({ + drain: () => Effect.sync(() => runs++).pipe(Effect.andThen(Deferred.await(gate))), + }) + + const first = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Effect.yieldNow + const second = yield* coordinator.run("session").pipe(Effect.forkChild) + yield* Fiber.interrupt(second) + yield* Deferred.succeed(gate, undefined) + yield* Fiber.join(first) + + expect(runs).toBe(1) + }), + ), + ) + + it.effect("runs different keys concurrently", () => + Effect.scoped( + Effect.gen(function* () { + const gate = yield* Deferred.make() + const bothStarted = yield* Deferred.make() + let active = 0 + const coordinator = yield* SessionRunCoordinator.make({ + drain: () => + Effect.sync(() => ++active).pipe( + Effect.tap(() => (active === 2 ? Deferred.succeed(bothStarted, undefined) : Effect.void)), + Effect.andThen(Deferred.await(gate)), + ), + }) + + const first = yield* coordinator.run("first").pipe(Effect.forkChild) + const second = yield* coordinator.run("second").pipe(Effect.forkChild) + yield* Deferred.await(bothStarted) + yield* Deferred.succeed(gate, undefined) + yield* Fiber.join(first) + yield* Fiber.join(second) + }), + ), + ) +}) diff --git a/packages/core/test/session-runner-message.test.ts b/packages/core/test/session-runner-message.test.ts new file mode 100644 index 0000000000..c6dd230505 --- /dev/null +++ b/packages/core/test/session-runner-message.test.ts @@ -0,0 +1,360 @@ +import { describe, expect, test } from "bun:test" +import { Message, Model } from "@opencode-ai/llm" +import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat" +import { EventV2 } from "@opencode-ai/core/event" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { SessionMessage } from "@opencode-ai/core/session/message" +import { AgentAttachment, FileAttachment, ReferenceAttachment } from "@opencode-ai/core/session/prompt" +import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolOutput } from "@opencode-ai/core/tool-output" +import { DateTime } from "effect" + +const created = DateTime.makeUnsafe(0) +const id = (value: string) => EventV2.ID.make(`evt_${value}`) +const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.route }) + +describe("toLLMMessages", () => { + test("maps every top-level V2 Session message type", () => { + const file = new FileAttachment({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" }) + const reference = new ReferenceAttachment({ name: "docs", kind: "local", uri: "file:///docs" }) + const messages = toLLMMessages([ + new SessionMessage.AgentSwitched({ id: id("agent"), type: "agent-switched", agent: "build", time: { created } }), + new SessionMessage.ModelSwitched({ + id: id("model"), + type: "model-switched", + model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + time: { created }, + }), + new SessionMessage.User({ + id: id("user"), + type: "user", + text: "Inspect this image", + files: [file], + agents: [new AgentAttachment({ name: "build" })], + references: [reference], + time: { created }, + }), + new SessionMessage.Synthetic({ + id: id("synthetic"), + type: "synthetic", + sessionID: SessionV2.ID.make("ses_translate"), + text: "Synthetic context", + time: { created }, + }), + new SessionMessage.Shell({ + id: id("shell"), + type: "shell", + callID: "shell-1", + command: "pwd", + output: "/project", + time: { created, completed: created }, + }), + new SessionMessage.Compaction({ + id: id("compaction"), + type: "compaction", + reason: "auto", + summary: "Earlier work", + time: { created }, + }), + ], model) + + expect(messages.map((message) => message.role)).toEqual(["user", "user", "user", "user"]) + expect(messages[0]).toEqual( + Message.make({ + id: id("user"), + role: "user", + content: [ + { type: "text", text: "Inspect this image" }, + { type: "media", mediaType: "image/png", data: "data:image/png;base64,aGVsbG8=", filename: "hello.png" }, + ], + metadata: { agents: [{ name: "build" }], references: [reference] }, + }), + ) + expect(messages.slice(1).map((message) => message.content)).toEqual([ + [{ type: "text", text: "Synthetic context" }], + [{ type: "text", text: "Shell command: pwd\n\n/project" }], + [{ type: "text", text: "Summary of earlier conversation:\nEarlier work" }], + ]) + }) + + test("expands assistant tool calls and settled outcomes into canonical tool messages", () => { + const messages = toLLMMessages([ + new SessionMessage.Assistant({ + id: id("assistant"), + type: "assistant", + agent: "build", + model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + content: [ + new SessionMessage.AssistantText({ type: "text", id: "text-1", text: "Checking" }), + new SessionMessage.AssistantReasoning({ + type: "reasoning", + id: "reasoning-1", + text: "Think", + providerMetadata: { anthropic: { signature: "sig_1" } }, + }), + new SessionMessage.AssistantTool({ + type: "tool", + id: "pending", + name: "read", + state: new SessionMessage.ToolStatePending({ status: "pending", input: '{"path":"README.md"}' }), + time: { created }, + }), + new SessionMessage.AssistantTool({ + type: "tool", + id: "running", + name: "read", + state: new SessionMessage.ToolStateRunning({ + status: "running", + input: { path: "README.md" }, + content: [], + structured: {}, + }), + time: { created }, + }), + new SessionMessage.AssistantTool({ + type: "tool", + id: "completed", + name: "read", + state: new SessionMessage.ToolStateCompleted({ + status: "completed", + input: { path: "README.md" }, + content: [ + new ToolOutput.TextContent({ type: "text", text: "Hello" }), + new ToolOutput.FileContent({ + type: "file", + source: { type: "data", data: "aGVsbG8=" }, + mime: "image/png", + name: "hello.png", + }), + ], + structured: {}, + }), + time: { created, completed: created }, + }), + new SessionMessage.AssistantTool({ + type: "tool", + id: "hosted", + name: "web_search", + provider: { + executed: true, + metadata: { fake: { continuation: "hosted-call" } }, + resultMetadata: { fake: { continuation: "hosted-result" } }, + }, + state: new SessionMessage.ToolStateCompleted({ + status: "completed", + input: { query: "Effect" }, + content: [new ToolOutput.TextContent({ type: "text", text: "Found it" })], + structured: {}, + }), + time: { created, completed: created }, + }), + new SessionMessage.AssistantTool({ + type: "tool", + id: "hosted-failed", + name: "write", + provider: { executed: true, metadata: { fake: { continuation: "failed" } } }, + state: new SessionMessage.ToolStateError({ + status: "error", + input: { path: "README.md" }, + content: [], + structured: {}, + error: { type: "unknown", message: "Denied" }, + }), + time: { created, completed: created }, + }), + ], + time: { created, completed: created }, + }), + ], model) + + expect(messages.map((message) => message.role)).toEqual(["assistant", "tool"]) + expect(messages[0]?.content).toEqual([ + { type: "text", text: "Checking" }, + { type: "reasoning", text: "Think", providerMetadata: { anthropic: { signature: "sig_1" } } }, + { type: "tool-call", id: "pending", name: "read", input: { path: "README.md" } }, + { type: "tool-call", id: "running", name: "read", input: { path: "README.md" } }, + { + type: "tool-call", + id: "completed", + name: "read", + input: { path: "README.md" }, + }, + { + type: "tool-call", + id: "hosted", + name: "web_search", + input: { query: "Effect" }, + providerExecuted: true, + providerMetadata: { fake: { continuation: "hosted-call" } }, + }, + { + type: "tool-result", + id: "hosted", + name: "web_search", + providerExecuted: true, + providerMetadata: { fake: { continuation: "hosted-result" } }, + result: { type: "text", value: "Found it" }, + }, + { + type: "tool-call", + id: "hosted-failed", + name: "write", + input: { path: "README.md" }, + providerExecuted: true, + providerMetadata: { fake: { continuation: "failed" } }, + }, + { + type: "tool-result", + id: "hosted-failed", + name: "write", + providerExecuted: true, + providerMetadata: { fake: { continuation: "failed" } }, + result: { type: "error", value: { error: { type: "unknown", message: "Denied" }, content: [], structured: {} } }, + }, + ]) + expect(messages[1]?.content).toEqual([ + { + type: "tool-result", + id: "completed", + name: "read", + result: { + type: "content", + value: [ + { type: "text", text: "Hello" }, + { type: "media", mediaType: "image/png", data: "aGVsbG8=", filename: "hello.png" }, + ], + }, + }, + ]) + }) + + test("restores OpenAI encrypted reasoning metadata", () => { + const messages = toLLMMessages([ + new SessionMessage.Assistant({ + id: id("assistant-openai-reasoning"), + type: "assistant", + agent: "build", + model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + content: [ + new SessionMessage.AssistantReasoning({ + type: "reasoning", + id: "reasoning-openai", + text: "Think", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, + }), + ], + time: { created, completed: created }, + }), + ], model) + + expect(messages[0]?.content).toEqual([ + { + type: "reasoning", + text: "Think", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, + }, + ]) + }) + + test("drops provider-native continuation metadata after a model switch", () => { + const messages = toLLMMessages( + [ + new SessionMessage.Assistant({ + id: id("assistant-old-model"), + type: "assistant", + agent: "build", + model: { id: ModelV2.ID.make("old-model"), providerID: ProviderV2.ID.make("provider") }, + content: [ + new SessionMessage.AssistantReasoning({ + type: "reasoning", + id: "reasoning-old-model", + text: "Visible thought", + providerMetadata: { anthropic: { signature: "sig_old" } }, + }), + new SessionMessage.AssistantTool({ + type: "tool", + id: "hosted-old-model", + name: "web_search", + provider: { + executed: true, + metadata: { openai: { itemId: "hosted-old-model" } }, + resultMetadata: { openai: { itemId: "hosted-old-model" } }, + }, + state: new SessionMessage.ToolStateCompleted({ + status: "completed", + input: { query: "Effect" }, + content: [], + structured: {}, + result: { type: "json", value: { status: "completed" } }, + }), + time: { created, completed: created }, + }), + new SessionMessage.AssistantTool({ + type: "tool", + id: "local-old-model", + name: "read", + provider: { + executed: false, + metadata: { fake: { call: "old" } }, + resultMetadata: { fake: { result: "old" } }, + }, + state: new SessionMessage.ToolStateCompleted({ + status: "completed", + input: { path: "README.md" }, + content: [], + structured: { text: "Hello" }, + }), + time: { created, completed: created }, + }), + ], + time: { created, completed: created }, + }), + ], + model, + ) + + expect(messages[0]?.content).toEqual([ + { type: "text", text: "Visible thought" }, + { + type: "tool-call", + id: "hosted-old-model", + name: "web_search", + input: { query: "Effect" }, + providerExecuted: true, + providerMetadata: undefined, + }, + { + type: "tool-result", + id: "hosted-old-model", + name: "web_search", + result: { type: "json", value: { status: "completed" } }, + providerExecuted: true, + cache: undefined, + metadata: undefined, + providerMetadata: undefined, + }, + { + type: "tool-call", + id: "local-old-model", + name: "read", + input: { path: "README.md" }, + providerExecuted: false, + providerMetadata: undefined, + }, + ]) + expect(messages[1]?.content).toEqual([ + { + type: "tool-result", + id: "local-old-model", + name: "read", + result: { type: "json", value: { text: "Hello" } }, + providerExecuted: false, + cache: undefined, + metadata: undefined, + providerMetadata: undefined, + }, + ]) + }) +}) diff --git a/packages/core/test/session-runner-model.test.ts b/packages/core/test/session-runner-model.test.ts new file mode 100644 index 0000000000..fbb7b65f7e --- /dev/null +++ b/packages/core/test/session-runner-model.test.ts @@ -0,0 +1,213 @@ +import { describe, expect } from "bun:test" +import { LLM } from "@opencode-ai/llm" +import { LLMClient } from "@opencode-ai/llm/route" +import { ConfigProvider, DateTime, Effect } from "effect" +import { Headers } from "effect/unstable/http" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { ProjectV2 } from "@opencode-ai/core/project" +import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" +import { SessionV2 } from "@opencode-ai/core/session" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { it } from "./lib/effect" + +type Api = + | { + readonly type: "aisdk" + readonly package: string + readonly url?: string + readonly settings?: Record + } + | { readonly type: "native"; readonly url?: string; readonly settings: Record } + +const model = (api: Api, variants: ModelV2.Info["variants"] = []) => + new ModelV2.Info({ + id: ModelV2.ID.make("test-model"), + providerID: ProviderV2.ID.make("test-provider"), + name: "Test model", + api: { id: ModelV2.ID.make("api-test-model"), ...api }, + capabilities: { tools: true, input: ["text"], output: ["text"] }, + request: { + headers: { "x-test": "header" }, + body: { store: false, apiKey: "secret" }, + }, + variants, + time: { released: DateTime.makeUnsafe(0) }, + cost: [], + status: "active", + enabled: true, + limit: { context: 100, output: 20 }, + }) + +const provider = (api: ProviderV2.Info["api"]) => + new ProviderV2.Info({ + id: ProviderV2.ID.make("test-provider"), + name: "Test provider", + enabled: { via: "env", name: "TEST_PROVIDER_API_KEY" }, + env: ["TEST_PROVIDER_API_KEY"], + api, + request: { headers: {}, body: {} }, + }) + +describe("SessionRunnerModel", () => { + it.effect("maps catalog OpenAI AI SDK models into native Responses routes", () => + Effect.gen(function* () { + const resolved = yield* SessionRunnerModel.fromCatalogModel( + model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), + ) + + expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" }) + expect(resolved.route).toMatchObject({ + id: "openai-responses", + endpoint: { baseURL: "https://openai.example/v1" }, + defaults: { + headers: { "x-test": "header" }, + limits: { context: 100, output: 20 }, + http: { body: { store: false } }, + }, + }) + }), + ) + + it.effect("keeps catalog apiKey credentials out of provider JSON", () => + Effect.gen(function* () { + const resolved = yield* SessionRunnerModel.fromCatalogModel( + model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), + ) + const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" })) + + expect(JSON.stringify(prepared.body)).not.toContain("apiKey") + expect(JSON.stringify(prepared.body)).not.toContain("secret") + }), + ) + + it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () => + Effect.gen(function* () { + const resolved = yield* SessionRunnerModel.fromCatalogModel( + new ModelV2.Info({ + ...model({ + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://compatible.example/v1", + settings: { apiKey: "settings-secret", compatibility: "strict" }, + }), + request: { headers: {}, body: {} }, + }), + ) + const request = LLM.request({ model: resolved, prompt: "Hello" }) + const headers = yield* resolved.route.auth.apply({ + request, + method: "POST", + url: "https://compatible.example/v1/chat/completions", + body: "{}", + headers: Headers.empty, + }) + + expect(headers.authorization).toBe("Bearer settings-secret") + expect(resolved.route.defaults.http?.body).toEqual({}) + }), + ) + + it.effect("applies the selected Session variant to request options", () => + Effect.gen(function* () { + const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [ + { + id: ModelV2.VariantID.make("high"), + headers: { "x-variant": "high" }, + body: { reasoningEffort: "high" }, + }, + ]) + const session = SessionV2.Info.make({ + id: SessionV2.ID.make("ses_model_variant"), + projectID: ProjectV2.ID.global, + title: "test", + model: { + id: catalog.id, + providerID: catalog.providerID, + variant: ModelV2.VariantID.make("high"), + }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, + location: { directory: AbsolutePath.make("/project") }, + }) + + const resolved = yield* SessionRunnerModel.resolve(session, catalog) + + expect(resolved.route.defaults).toMatchObject({ + headers: { "x-test": "header", "x-variant": "high" }, + http: { body: { store: false, reasoningEffort: "high" } }, + }) + }), + ) + + it.effect("maps catalog Anthropic AI SDK models into native routes", () => + Effect.gen(function* () { + const resolved = yield* SessionRunnerModel.fromCatalogModel( + model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }), + ) + + expect(resolved.route).toMatchObject({ + id: "anthropic-messages", + endpoint: { baseURL: "https://anthropic.example/v1" }, + }) + }), + ) + + it.effect("preserves environment-backed bearer auth", () => + Effect.gen(function* () { + const resolved = yield* SessionRunnerModel.fromCatalogModel( + new ModelV2.Info({ + ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), + request: { headers: {}, body: {} }, + }), + provider({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), + ) + const request = LLM.request({ model: resolved, prompt: "Hello" }) + const headers = yield* resolved.route.auth + .apply({ + request, + method: "POST", + url: "https://openai.example/v1/responses", + body: "{}", + headers: Headers.empty, + }) + .pipe( + Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { TEST_PROVIDER_API_KEY: "secret" } }))), + ) + + expect(headers.authorization).toBe("Bearer secret") + }), + ) + + it.effect("rejects catalog APIs without a native route", () => + Effect.gen(function* () { + const failure = yield* SessionRunnerModel.fromCatalogModel( + model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }), + ).pipe(Effect.flip) + + expect(failure).toMatchObject({ + _tag: "SessionRunnerModel.UnsupportedApiError", + providerID: "test-provider", + modelID: "test-model", + api: "aisdk:@ai-sdk/google", + }) + }), + ) + + it.effect("reports whether a catalog model has a supported native route", () => + Effect.sync(() => { + expect( + SessionRunnerModel.supported( + model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), + ), + ).toBe(true) + expect( + SessionRunnerModel.supported( + model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }), + ), + ).toBe(false) + expect(SessionRunnerModel.supported(model({ type: "native", settings: {} }))).toBe(false) + }), + ) +}) diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts new file mode 100644 index 0000000000..10f8938fca --- /dev/null +++ b/packages/core/test/session-runner-recorded.test.ts @@ -0,0 +1,155 @@ +import { NodeFileSystem } from "@effect/platform-node" +import { HttpRecorder } from "@opencode-ai/http-recorder" +import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat" +import { Auth, LLMClient, RequestExecutor } from "@opencode-ai/llm/route" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { EventTable } from "@opencode-ai/core/event/sql" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { Project } from "@opencode-ai/core/project" +import { ProjectTable } from "@opencode-ai/core/project/sql" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { Prompt } from "@opencode-ai/core/session/prompt" +import { SessionProjector } from "@opencode-ai/core/session/projector" +import { SessionExecution } from "@opencode-ai/core/session/execution" +import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator" +import { SessionRunner } from "@opencode-ai/core/session/runner" +import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm" +import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { SessionTable } from "@opencode-ai/core/session/sql" +import { SessionStore } from "@opencode-ai/core/session/store" +import { describe, expect } from "bun:test" +import { eq } from "drizzle-orm" +import { Effect, Layer } from "effect" +import path from "node:path" +import { testEffect } from "./lib/effect" + +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database)) +const store = SessionStore.layer.pipe(Layer.provide(database)) +const cassette = HttpRecorder.cassetteLayer("session-runner/openai-chat-streams-text", { + directory: path.resolve(import.meta.dir, "fixtures/recordings"), + mode: process.env.RECORD === "true" ? "record" : "replay", +}).pipe(Layer.provide(NodeFileSystem.layer)) +const executor = RequestExecutor.layer.pipe(Layer.provide(cassette)) +const client = LLMClient.layer.pipe(Layer.provide(executor)) +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: () => Effect.die("unused"), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) +const model = OpenAIChat.route + .with({ + endpoint: { baseURL: "https://api.openai.com/v1" }, + auth: Auth.bearer(process.env.OPENAI_API_KEY ?? "fixture"), + generation: { maxTokens: 20, temperature: 0 }, + }) + .model({ id: "gpt-4o-mini" }) +const models = SessionRunnerModel.layerWith(() => Effect.succeed(model)) +const runner = SessionRunnerLLM.defaultLayer.pipe( + Layer.provide(database), + Layer.provide(store), + Layer.provide(events), + Layer.provide(client), + Layer.provide(registry), + Layer.provide(models), +) +const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner)) +const execution = Layer.effect( + SessionExecution.Service, + SessionRunCoordinator.Service.pipe( + Effect.map((coordinator) => SessionExecution.Service.of({ resume: coordinator.run, wake: coordinator.wake })), + ), +).pipe(Layer.provide(coordinator)) +const sessions = SessionV2.layer.pipe( + Layer.provide(events), + Layer.provide(database), + Layer.provide(store), + Layer.provide(Project.defaultLayer), + Layer.provide(execution), +) +const it = testEffect( + Layer.mergeAll( + database, + events, + projector, + store, + executor, + client, + permission, + registry, + models, + runner, + coordinator, + execution, + sessions, + ), +) +const sessionID = SessionV2.ID.make("ses_runner_recorded") + +describe("SessionRunnerLLM recorded", () => { + it.effect("executes one recorded V2 prompt through the recorded HTTP transport", () => + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "test", + directory: "/project", + title: "test", + version: "test", + }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + const session = yield* SessionV2.Service + const prompt = yield* session.prompt({ + sessionID, + prompt: new Prompt({ text: "Say hello in one short sentence." }), + resume: false, + }) + + yield* session.resume(sessionID) + + const messages = yield* session.context(sessionID) + expect(messages).toHaveLength(2) + expect(messages[0]).toEqual(prompt) + expect(messages[1]).toMatchObject({ type: "assistant", agent: "build", finish: "stop" }) + expect(messages[1]?.type === "assistant" ? messages[1].content : []).toMatchObject([ + { type: "text", text: "Hello!" }, + ]) + expect( + (yield* db + .select({ type: EventTable.type }) + .from(EventTable) + .where(eq(EventTable.aggregate_id, sessionID)) + .orderBy(EventTable.seq) + .all()).map((event) => event.type), + ).toEqual([ + "session.next.prompted.1", + "session.next.step.started.1", + "session.next.text.started.1", + "session.next.text.ended.1", + "session.next.step.ended.2", + ]) + }), + ) +}) diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts new file mode 100644 index 0000000000..68d851861a --- /dev/null +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -0,0 +1,209 @@ +import { describe, expect } from "bun:test" +import { Tool, ToolFailure } from "@opencode-ai/llm" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { Effect, Exit, Layer, Schema, Scope } from "effect" +import { testEffect } from "./lib/effect" + +const assertions: PermissionV2.AssertInput[] = [] +let denyAction: string | undefined +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => assertions.push(input)).pipe( + Effect.andThen(input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void), + ), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) +const it = testEffect(Layer.mergeAll(permission, registry)) + +const echo = Tool.make({ + description: "Echo text", + parameters: Schema.Struct({ text: Schema.String }), + success: Schema.Struct({ text: Schema.String }), + execute: ({ text }) => Effect.succeed({ text }), +}) + +describe("ToolRegistry", () => { + it.effect("rebuilds advertised definitions when a scoped transform closes", () => + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const scope = yield* Scope.make() + const transform = yield* registry.transform().pipe(Scope.provide(scope)) + + yield* transform((editor) => editor.set("echo", { tool: echo, authorize: () => Effect.void })) + expect(yield* registry.definitions()).toMatchObject([{ name: "echo", description: "Echo text" }]) + + yield* Scope.close(scope, Exit.void) + expect(yield* registry.definitions()).toEqual([]) + }), + ) + + it.effect("returns an error result for an unknown tool", () => + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + + expect( + yield* registry.execute({ + sessionID: SessionV2.ID.make("ses_registry_test"), + call: { type: "tool-call", id: "call-missing", name: "missing", input: {} }, + }), + ).toEqual({ type: "error", value: "Unknown tool: missing" }) + }), + ) + + it.effect("does not execute a tool when authorization fails", () => + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + let executed = false + const transform = yield* registry.transform() + + yield* transform((editor) => + editor.set("denied", { + authorize: () => Effect.fail(new ToolFailure({ message: "Denied" })), + tool: Tool.make({ + description: "Denied tool", + parameters: Schema.Struct({}), + success: Schema.Struct({ ok: Schema.Boolean }), + execute: () => + Effect.sync(() => { + executed = true + return { ok: true } + }), + }), + }), + ) + + expect( + yield* registry.execute({ + sessionID: SessionV2.ID.make("ses_registry_test"), + call: { type: "tool-call", id: "call-denied", name: "denied", input: {} }, + }), + ).toEqual({ type: "error", value: "Denied" }) + expect(executed).toBe(false) + }), + ) + + it.effect("binds invocation identity while preserving leaf-owned permission inputs", () => + Effect.gen(function* () { + assertions.length = 0 + denyAction = undefined + const registry = yield* ToolRegistry.Service + const transform = yield* registry.transform() + const sessionID = SessionV2.ID.make("ses_registry_context") + + yield* transform((editor) => + editor.set("context", { + tool: Tool.make({ + description: "Context tool", + parameters: Schema.Struct({}), + success: Schema.Struct({ ok: Schema.Boolean }), + }), + execute: ({ assertPermission, call, source }) => + assertPermission({ + action: "inspect", + resources: [call.id], + save: ["*"], + metadata: { tool: call.name }, + }).pipe( + Effect.as({ ok: source === undefined }), + Effect.catch(() => Effect.fail(new ToolFailure({ message: "Denied" }))), + ), + }), + ) + + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-context", name: "context", input: {} }, + }), + ).toEqual({ type: "json", value: { ok: true } }) + expect(assertions).toEqual([ + { + sessionID, + action: "inspect", + resources: ["call-context"], + save: ["*"], + metadata: { tool: "context" }, + }, + ]) + expect(assertions[0]).not.toHaveProperty("source") + }), + ) + + it.effect("keeps ordered multi-assert policy flow in the leaf and stops on denial", () => + Effect.gen(function* () { + assertions.length = 0 + denyAction = "execute" + let executed = false + const registry = yield* ToolRegistry.Service + const transform = yield* registry.transform() + + yield* transform((editor) => + editor.set("ordered", { + tool: Tool.make({ + description: "Ordered policy tool", + parameters: Schema.Struct({}), + success: Schema.Struct({ ok: Schema.Boolean }), + }), + execute: ({ assertPermission }) => + Effect.gen(function* () { + yield* assertPermission({ action: "external_directory", resources: ["/outside/*"] }) + yield* assertPermission({ action: "execute", resources: ["pwd"] }) + executed = true + return { ok: true } + }).pipe(Effect.catch(() => Effect.fail(new ToolFailure({ message: "Denied" })))), + }), + ) + + expect( + yield* registry.execute({ + sessionID: SessionV2.ID.make("ses_registry_context"), + call: { type: "tool-call", id: "call-ordered", name: "ordered", input: {} }, + }), + ).toEqual({ type: "error", value: "Denied" }) + expect(assertions.map((input) => input.action)).toEqual(["external_directory", "execute"]) + expect(executed).toBe(false) + denyAction = undefined + }), + ) + + it.effect("settles encoded structured output with canonical projected content", () => + Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + const transform = yield* registry.transform() + + yield* transform((editor) => + editor.set("projected", { + tool: Tool.make({ + description: "Projected tool", + parameters: Schema.Struct({ prefix: Schema.String }), + success: Schema.Struct({ count: Schema.NumberFromString }), + execute: () => Effect.succeed({ count: 2 }), + toModelOutput: ({ callID, parameters, output }) => [ + { type: "text", text: `${callID}:${parameters.prefix}:${output.count}` }, + ], + }), + }), + ) + + expect( + yield* registry.settle({ + sessionID: SessionV2.ID.make("ses_registry_test"), + call: { type: "tool-call", id: "call-projected", name: "projected", input: { prefix: "count" } }, + }), + ).toEqual({ + result: { type: "text", value: "call-projected:count:2" }, + output: { structured: { count: "2" }, content: [{ type: "text", text: "call-projected:count:2" }] }, + }) + }), + ) +}) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts new file mode 100644 index 0000000000..daa24403d4 --- /dev/null +++ b/packages/core/test/session-runner.test.ts @@ -0,0 +1,2117 @@ +import { describe, expect } from "bun:test" +import { + LLMClient, + LLMError, + LLMEvent, + Model, + Tool, + TransportReason, + type LLMClientShape, + type LLMRequest, +} from "@opencode-ai/llm" +import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { EventTable } from "@opencode-ai/core/event/sql" +import { Project } from "@opencode-ai/core/project" +import { ProjectTable } from "@opencode-ai/core/project/sql" +import { QuestionV2 } from "@opencode-ai/core/question" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { SessionEvent } from "@opencode-ai/core/session/event" +import { SessionInput } from "@opencode-ai/core/session/input" +import { Prompt } from "@opencode-ai/core/session/prompt" +import { SessionProjector } from "@opencode-ai/core/session/projector" +import { SessionExecution } from "@opencode-ai/core/session/execution" +import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator" +import { SessionRunner } from "@opencode-ai/core/session/runner" +import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm" +import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql" +import { SessionStore } from "@opencode-ai/core/session/store" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { Cause, DateTime, Deferred, Effect, Fiber, Layer, Schema, Stream } from "effect" +import { asc, eq } from "drizzle-orm" +import { testEffect } from "./lib/effect" + +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const questions = QuestionV2.layer.pipe(Layer.provide(events)) +const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database)) +const store = SessionStore.layer.pipe(Layer.provide(database)) +const requests: LLMRequest[] = [] +let response: LLMEvent[] = [] +let responses: LLMEvent[][] | undefined +let responseStream: Stream.Stream | undefined +let streamGate: Deferred.Deferred | undefined +let streamStarted: Deferred.Deferred | undefined +let streamFailure: LLMError | undefined +let toolExecutionGate: Deferred.Deferred | undefined +let toolExecutionsStarted: Deferred.Deferred | undefined +let activeToolExecutions = 0 +let maxActiveToolExecutions = 0 +const client = Layer.succeed( + LLMClient.Service, + LLMClient.Service.of({ + prepare: () => Effect.die("unused"), + stream: ((request: LLMRequest) => { + requests.push(request) + if (responseStream) { + const stream = responseStream + responseStream = undefined + return stream + } + const events = streamFailure + ? Stream.fail(streamFailure) + : Stream.fromIterable(responses === undefined ? response : (responses.shift() ?? [])) + if (!streamGate) return events + return Stream.unwrap( + (streamStarted ? Deferred.succeed(streamStarted, undefined) : Effect.void).pipe( + Effect.andThen(Deferred.await(streamGate)), + Effect.as(events), + ), + ) + }) as unknown as LLMClientShape["stream"], + generate: () => Effect.die("unused"), + }), +) +const model = Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route }) +const authorizations: ToolRegistry.AuthorizeInput[] = [] +const executions: string[] = [] +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: () => Effect.die("unused"), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) +const echo = Layer.effectDiscard( + ToolRegistry.Service.use((registry) => + registry.contribute((editor) => { + ;(editor.set("echo", { + authorize: (input) => + Effect.sync(() => { + authorizations.push(input) + }), + tool: Tool.make({ + description: "Echo text", + parameters: Schema.Struct({ text: Schema.String }), + success: Schema.Struct({ text: Schema.String }), + toModelOutput: ({ output }) => [{ type: "text", text: output.text }], + execute: ({ text }) => + Effect.gen(function* () { + executions.push(text) + activeToolExecutions++ + maxActiveToolExecutions = Math.max(maxActiveToolExecutions, activeToolExecutions) + if (activeToolExecutions === 5 && toolExecutionsStarted) { + yield* Deferred.succeed(toolExecutionsStarted, undefined) + } + if (toolExecutionGate) yield* Deferred.await(toolExecutionGate) + return { text } + }).pipe(Effect.ensuring(Effect.sync(() => activeToolExecutions--))), + }), + }), + editor.set("defect", { + tool: Tool.make({ + description: "Fail unexpectedly", + parameters: Schema.Struct({}), + success: Schema.Struct({}), + execute: () => Effect.die("unexpected tool defect"), + }), + })) + }), + ), +).pipe(Layer.provide(registry)) +const models = SessionRunnerModel.layerWith(() => Effect.succeed(model)) +const runner = SessionRunnerLLM.layer.pipe( + Layer.provide(database), + Layer.provide(store), + Layer.provide(events), + Layer.provide(client), + Layer.provide(registry), + Layer.provide(models), +) +const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner)) +const execution = Layer.effect( + SessionExecution.Service, + SessionRunCoordinator.Service.pipe( + Effect.map((coordinator) => SessionExecution.Service.of({ resume: coordinator.run, wake: coordinator.wake })), + ), +).pipe(Layer.provide(coordinator)) +const sessions = SessionV2.layer.pipe( + Layer.provide(events), + Layer.provide(database), + Layer.provide(store), + Layer.provide(Project.defaultLayer), + Layer.provide(execution), +) +const it = testEffect( + Layer.mergeAll( + database, + events, + questions, + projector, + store, + client, + permission, + registry, + echo, + models, + runner, + coordinator, + execution, + sessions, + ), +) +const sessionID = SessionV2.ID.make("ses_runner_test") +const otherSessionID = SessionV2.ID.make("ses_runner_other") + +const insertSession = (id: SessionV2.ID) => + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(SessionTable) + .values({ + id, + project_id: Project.ID.global, + slug: id, + directory: "/project", + title: "test", + version: "test", + }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + }) + +const setup = Effect.gen(function* () { + const { db } = yield* Database.Service + response = [] + responses = undefined + streamFailure = undefined + responseStream = undefined + streamGate = undefined + streamStarted = undefined + toolExecutionGate = undefined + toolExecutionsStarted = undefined + activeToolExecutions = 0 + maxActiveToolExecutions = 0 + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + yield* insertSession(sessionID) +}) + +const providerUnavailable = () => + new LLMError({ + module: "test", + method: "stream", + reason: new TransportReason({ message: "Provider unavailable" }), + }) + +const userTexts = (request: LLMRequest) => + request.messages.flatMap((message) => + message.role === "user" + ? message.content.flatMap((content) => (content.type === "text" ? [content.text] : [])) + : [], + ) + +const replaySessionProjection = (id: SessionV2.ID) => + Effect.gen(function* () { + const { db } = yield* Database.Service + const events = yield* EventV2.Service + const recorded = yield* db + .select() + .from(EventTable) + .where(eq(EventTable.aggregate_id, id)) + .orderBy(asc(EventTable.seq)) + .all() + .pipe(Effect.orDie) + + yield* events.remove(id) + yield* db.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, id)).run().pipe(Effect.orDie) + yield* events.replayAll( + recorded.map((event) => ({ + id: event.id, + aggregateID: event.aggregate_id, + seq: event.seq, + type: event.type, + data: event.data, + })), + ) + }) + +type FragmentKind = "text" | "reasoning" | "tool input" + +type FragmentFixture = { + readonly delta: EventV2.Definition + readonly completeEvents: LLMEvent[] + readonly partialEvents: LLMEvent[] + readonly expectedAssistant: unknown + readonly expectedContent: unknown +} + +const fragmentKinds: readonly FragmentKind[] = ["text", "reasoning", "tool input"] + +const fragmentID = (kind: FragmentKind, suffix: string) => `${kind === "tool input" ? "call" : kind}-${suffix}` + +const fragmentFixture = (kind: FragmentKind, id: string, chunks: readonly string[]): FragmentFixture => { + const text = chunks.join("") + switch (kind) { + case "text": { + const partialEvents = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.textStart({ id }), + ...chunks.map((text) => LLMEvent.textDelta({ id, text })), + ] + const expectedContent = { type: "text", id, text } + return { + delta: SessionEvent.Text.Delta, + partialEvents, + completeEvents: [ + ...partialEvents, + LLMEvent.textEnd({ id }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + expectedAssistant: { type: "assistant", finish: "stop", content: [expectedContent] }, + expectedContent, + } + } + case "reasoning": { + const partialEvents = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.reasoningStart({ id }), + ...chunks.map((text) => LLMEvent.reasoningDelta({ id, text })), + ] + const expectedContent = { type: "reasoning", id, text } + return { + delta: SessionEvent.Reasoning.Delta, + partialEvents, + completeEvents: [ + ...partialEvents, + LLMEvent.reasoningEnd({ id }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + expectedAssistant: { type: "assistant", finish: "stop", content: [expectedContent] }, + expectedContent, + } + } + case "tool input": { + const partialEvents = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolInputStart({ id, name: "echo" }), + ...chunks.map((text) => LLMEvent.toolInputDelta({ id, name: "echo", text })), + ] + const expectedContent = { type: "tool", id, state: { status: "pending", input: text } } + return { + delta: SessionEvent.Tool.Input.Delta, + partialEvents, + completeEvents: [...partialEvents, LLMEvent.toolInputEnd({ id, name: "echo" })], + expectedAssistant: { type: "assistant", content: [expectedContent] }, + expectedContent, + } + } + } +} + +const verifyEphemeralDeltas = (kind: FragmentKind) => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const prompt = `Stream ${kind}` + const chunks = Array.from({ length: 32 }, (_, index) => `${index},`) + const fixture = fragmentFixture(kind, fragmentID(kind, "many"), chunks) + const expectedContext = [{ type: "user", text: prompt }, fixture.expectedAssistant] + yield* session.prompt({ sessionID, prompt: new Prompt({ text: prompt }), resume: false }) + const events = yield* EventV2.Service + const live = yield* events.subscribe(fixture.delta).pipe(Stream.take(32), Stream.runCollect, Effect.forkScoped) + yield* Effect.yieldNow + response = fixture.completeEvents + + yield* session.resume(sessionID) + + const { db } = yield* Database.Service + const deltas = yield* db + .select({ type: EventTable.type }) + .from(EventTable) + .where(eq(EventTable.type, EventV2.versionedType(fixture.delta.type, 1))) + .all() + .pipe(Effect.orDie) + expect(Array.from(yield* Fiber.join(live))).toHaveLength(32) + expect(deltas).toHaveLength(0) + expect(yield* session.context(sessionID)).toMatchObject(expectedContext) + + yield* replaySessionProjection(sessionID) + + expect(yield* session.context(sessionID)).toMatchObject(expectedContext) + }) + +const verifyPartialFlushOnFailure = (kind: FragmentKind) => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const prompt = `Fail after ${kind}` + const fixture = fragmentFixture(kind, fragmentID(kind, "partial"), ["Partial"]) + const failure = providerUnavailable() + yield* session.prompt({ sessionID, prompt: new Prompt({ text: prompt }), resume: false }) + responseStream = Stream.concat(Stream.fromIterable(fixture.partialEvents), Stream.fail(failure)) + + expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: prompt }, + { + type: "assistant", + finish: "error", + error: { type: "unknown", message: "Provider unavailable" }, + content: [fixture.expectedContent], + }, + ]) + }) + +const verifyPartialFlushOnInterruption = (kind: FragmentKind) => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const prompt = `Interrupt after ${kind}` + const fixture = fragmentFixture(kind, fragmentID(kind, "interrupted"), ["Partial"]) + const streamed = yield* Deferred.make() + yield* session.prompt({ sessionID, prompt: new Prompt({ text: prompt }), resume: false }) + responseStream = Stream.concat( + Stream.fromIterable(fixture.partialEvents), + Stream.fromEffect(Deferred.succeed(streamed, undefined)).pipe(Stream.flatMap(() => Stream.never)), + ) + + const runner = yield* SessionRunner.Service + const fiber = yield* runner.run({ sessionID, force: true }).pipe(Effect.forkChild) + yield* Deferred.await(streamed) + yield* Fiber.interrupt(fiber) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: prompt }, + { + type: "assistant", + content: [ + kind === "tool input" + ? { type: "tool", id: fragmentID(kind, "interrupted"), state: { status: "error" } } + : fixture.expectedContent, + ], + }, + ]) + }) + +describe("SessionRunnerLLM", () => { + it.effect("starts a real runner turn after default prompt recording", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + requests.length = 0 + responses = undefined + streamGate = undefined + streamStarted = undefined + response = [] + + const message = yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run automatically" }) }) + + expect(requests).toHaveLength(1) + expect(yield* session.messages({ sessionID })).toEqual([message]) + }), + ) + + it.effect("streams one request with registry definitions from chronological V2 user history", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + + requests.length = 0 + responses = undefined + streamGate = undefined + streamStarted = undefined + response = [] + yield* session.resume(sessionID) + + expect(requests).toHaveLength(1) + expect(requests[0]?.model).toBe(model) + expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["echo", "defect"]) + expect(requests[0]?.messages.map((message) => ({ role: message.role, content: message.content }))).toEqual([ + { role: "user", content: [{ type: "text", text: "First" }] }, + { role: "user", content: [{ type: "text", text: "Second" }] }, + ]) + expect(yield* session.messages({ sessionID })).toHaveLength(2) + }), + ) + + it.effect("projects reasoning and tool events without executing or continuing tools", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Use tools" }), resume: false }) + + requests.length = 0 + responses = undefined + streamGate = undefined + streamStarted = undefined + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.reasoningStart({ id: "reasoning-1" }), + LLMEvent.reasoningDelta({ id: "reasoning-1", text: "Think" }), + LLMEvent.reasoningEnd({ id: "reasoning-1" }), + LLMEvent.toolInputStart({ id: "call-error", name: "write" }), + LLMEvent.toolInputDelta({ id: "call-error", name: "write", text: '{"path":"README.md"}' }), + LLMEvent.toolInputEnd({ id: "call-error", name: "write" }), + LLMEvent.toolCall({ id: "call-error", name: "write", input: { path: "README.md" }, providerExecuted: true }), + LLMEvent.toolError({ id: "call-error", name: "write", message: "Denied" }), + LLMEvent.toolResult({ id: "call-error", name: "write", result: { type: "error", value: "Denied" } }), + LLMEvent.toolCall({ + id: "call-provider", + name: "web_search", + input: { query: "hello" }, + providerExecuted: true, + providerMetadata: { fake: { source: "provider" } }, + }), + LLMEvent.toolResult({ + id: "call-provider", + name: "web_search", + result: { + type: "content", + value: [ + { type: "text", text: "Hello" }, + { type: "media", mediaType: "image/png", data: "data:image/png;base64,aGVsbG8=", filename: "hello.png" }, + ], + }, + providerExecuted: true, + providerMetadata: { fake: { source: "provider" } }, + }), + LLMEvent.stepFinish({ + index: 0, + reason: "tool-calls", + usage: { + inputTokens: 10, + nonCachedInputTokens: 8, + outputTokens: 4, + reasoningTokens: 1, + cacheReadInputTokens: 2, + }, + }), + LLMEvent.finish({ reason: "tool-calls" }), + ] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(1) + expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["echo", "defect"]) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Use tools" }, + { + type: "assistant", + finish: "tool-calls", + tokens: { input: 8, output: 3, reasoning: 1, cache: { read: 2, write: 0 } }, + content: [ + { type: "reasoning", id: "reasoning-1", text: "Think" }, + { + type: "tool", + id: "call-error", + name: "write", + state: { + status: "error", + input: { path: "README.md" }, + error: { type: "unknown", message: "Denied" }, + }, + }, + { + type: "tool", + id: "call-provider", + name: "web_search", + provider: { executed: true, metadata: { fake: { source: "provider" } } }, + state: { + status: "completed", + input: { query: "hello" }, + structured: {}, + content: [ + { type: "text", text: "Hello" }, + { type: "file", mime: "image/png", source: { type: "data", data: "aGVsbG8=" }, name: "hello.png" }, + ], + }, + }, + ], + }, + ]) + }), + ) + + it.effect("continues with reloaded history after durably settling one local tool call", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo this" }), resume: false }) + + requests.length = 0 + authorizations.length = 0 + executions.length = 0 + streamGate = undefined + streamStarted = undefined + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-echo", name: "echo", input: { text: "hello" } }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.textStart({ id: "text-final" }), + LLMEvent.textDelta({ id: "text-final", text: "Done" }), + LLMEvent.textEnd({ id: "text-final" }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(2) + expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "assistant", "tool"]) + expect(authorizations).toMatchObject([{ sessionID, call: { id: "call-echo", name: "echo" } }]) + expect(executions).toEqual(["hello"]) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Echo this" }, + { + type: "assistant", + finish: "tool-calls", + content: [ + { + type: "tool", + id: "call-echo", + name: "echo", + state: { + status: "completed", + input: { text: "hello" }, + structured: { text: "hello" }, + content: [{ type: "text", text: "hello" }], + }, + }, + ], + }, + { type: "assistant", finish: "stop", content: [{ type: "text", id: "text-final", text: "Done" }] }, + ]) + }), + ) + + it.effect("restores durable reasoning provider metadata in a second-turn request", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Think first" }), resume: false }) + + requests.length = 0 + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.reasoningStart({ id: "reasoning-anthropic" }), + LLMEvent.reasoningDelta({ id: "reasoning-anthropic", text: "Signed thought" }), + LLMEvent.reasoningEnd({ id: "reasoning-anthropic", providerMetadata: { anthropic: { signature: "sig_1" } } }), + LLMEvent.reasoningStart({ + id: "reasoning-openai", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } }, + }), + LLMEvent.reasoningDelta({ id: "reasoning-openai", text: "Encrypted thought" }), + LLMEvent.reasoningEnd({ + id: "reasoning-openai", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, + }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ] + yield* session.resume(sessionID) + yield* replaySessionProjection(sessionID) + + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Think first" }, + { + type: "assistant", + content: [ + { type: "reasoning", text: "Signed thought", providerMetadata: { anthropic: { signature: "sig_1" } } }, + { + type: "reasoning", + text: "Encrypted thought", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, + }, + ], + }, + ]) + + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false }) + response = [] + yield* session.resume(sessionID) + + expect(requests[1]?.messages[1]?.content).toEqual([ + { type: "reasoning", text: "Signed thought", providerMetadata: { anthropic: { signature: "sig_1" } } }, + { + type: "reasoning", + text: "Encrypted thought", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, + }, + ]) + }), + ) + + it.effect("replays durable provider-executed tool results inline in a second-turn request", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Search first" }), resume: false }) + + requests.length = 0 + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ + id: "hosted-search", + name: "web_search", + input: { query: "Effect" }, + providerExecuted: true, + providerMetadata: { openai: { itemId: "hosted-search" } }, + }), + LLMEvent.toolResult({ + id: "hosted-search", + name: "web_search", + result: { type: "json", value: [{ title: "Effect" }] }, + providerExecuted: true, + providerMetadata: { anthropic: { blockType: "web_search_tool_result" } }, + }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ] + yield* session.resume(sessionID) + yield* replaySessionProjection(sessionID) + + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false }) + response = [] + yield* session.resume(sessionID) + + expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "assistant", "user"]) + expect(requests[1]?.messages[1]?.content).toMatchObject([ + { + type: "tool-call", + id: "hosted-search", + name: "web_search", + input: { query: "Effect" }, + providerExecuted: true, + providerMetadata: { openai: { itemId: "hosted-search" } }, + }, + { + type: "tool-result", + id: "hosted-search", + name: "web_search", + result: { type: "json", value: [{ title: "Effect" }] }, + providerExecuted: true, + providerMetadata: { anthropic: { blockType: "web_search_tool_result" } }, + }, + ]) + }), + ) + + it.effect("starts recorded local tools eagerly and awaits settlement before continuing", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo five times" }), resume: false }) + + requests.length = 0 + executions.length = 0 + toolExecutionGate = yield* Deferred.make() + toolExecutionsStarted = yield* Deferred.make() + const providerGate = yield* Deferred.make() + response = [] + responses = undefined + const initial = Stream.fromIterable([ + LLMEvent.stepStart({ index: 0 }), + ...Array.from({ length: 5 }, (_, index) => + LLMEvent.toolCall({ id: `call-echo-${index}`, name: "echo", input: { text: `${index}` } }), + ), + ]) + const final = Stream.fromIterable([ + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ]) + streamGate = undefined + responseStream = Stream.concat( + initial, + Stream.fromEffect(Deferred.await(providerGate)).pipe(Stream.flatMap(() => final)), + ) + + const run = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Deferred.await(toolExecutionsStarted) + + expect(executions).toHaveLength(5) + expect(maxActiveToolExecutions).toBe(5) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Echo five times" }, + { + type: "assistant", + content: Array.from({ length: 5 }, (_, index) => ({ + type: "tool", + id: `call-echo-${index}`, + state: { status: "running", input: { text: `${index}` } }, + })), + }, + ]) + + yield* Deferred.succeed(providerGate, undefined) + yield* Effect.yieldNow + expect(requests).toHaveLength(1) + + yield* Deferred.succeed(toolExecutionGate, undefined) + yield* Fiber.join(run) + toolExecutionGate = undefined + toolExecutionsStarted = undefined + + expect(executions).toHaveLength(5) + expect(maxActiveToolExecutions).toBe(5) + expect(requests).toHaveLength(2) + }), + ) + + it.effect("settles repeated provider-local tool call IDs against their owning assistant messages", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo twice" }), resume: false }) + + requests.length = 0 + executions.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "tool_0", name: "echo", input: { text: "first" } }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "tool_0", name: "echo", input: { text: "second" } }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ], + [], + ] + + yield* session.resume(sessionID) + + expect(executions).toEqual(["first", "second"]) + expect(requests).toHaveLength(3) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Echo twice" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "tool_0", + state: { status: "completed", structured: { text: "first" }, content: [{ type: "text", text: "first" }] }, + }, + ], + }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "tool_0", + state: { + status: "completed", + structured: { text: "second" }, + content: [{ type: "text", text: "second" }], + }, + }, + ], + }, + ]) + + yield* replaySessionProjection(sessionID) + + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Echo twice" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "tool_0", + state: { status: "completed", structured: { text: "first" }, content: [{ type: "text", text: "first" }] }, + }, + ], + }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "tool_0", + state: { + status: "completed", + structured: { text: "second" }, + content: [{ type: "text", text: "second" }], + }, + }, + ], + }, + ]) + }), + ) + + it.effect("joins concurrent resume calls into one active provider run", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run once" }), resume: false }) + + requests.length = 0 + responses = undefined + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.textStart({ id: "text-once" }), + LLMEvent.textDelta({ id: "text-once", text: "Once" }), + LLMEvent.textEnd({ id: "text-once" }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ] + streamGate = yield* Deferred.make() + streamStarted = yield* Deferred.make() + + const first = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Deferred.await(streamStarted) + const second = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Effect.yieldNow + + expect(requests).toHaveLength(1) + yield* Deferred.succeed(streamGate, undefined) + yield* Fiber.join(first) + yield* Fiber.join(second) + streamGate = undefined + streamStarted = undefined + + expect(requests).toHaveLength(1) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Run once" }, + { type: "assistant", finish: "stop", content: [{ type: "text", id: "text-once", text: "Once" }] }, + ]) + }), + ) + + it.effect("steers an active provider turn with newly recorded prompts", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + streamGate = yield* Deferred.make() + streamStarted = yield* Deferred.make() + + const first = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Deferred.await(streamStarted) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Change direction" }) }) + yield* Deferred.succeed(streamGate, undefined) + yield* Fiber.join(first) + streamGate = undefined + streamStarted = undefined + yield* Effect.yieldNow + + expect(requests).toHaveLength(2) + expect(userTexts(requests[0]!)).toEqual(["Start working"]) + expect(userTexts(requests[1]!)).toEqual(["Start working", "Change direction"]) + expect((yield* session.context(sessionID)).map((message) => message.type)).toEqual([ + "user", + "assistant", + "user", + "assistant", + ]) + }), + ) + + it.effect("starts queued input after the active activity settles", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-echo", name: "echo", input: { text: "hello" } }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + streamGate = yield* Deferred.make() + streamStarted = yield* Deferred.make() + + const first = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Deferred.await(streamStarted) + yield* session.prompt({ + sessionID, + prompt: new Prompt({ text: "Wait until the next activity" }), + delivery: "queue", + }) + yield* Deferred.succeed(streamGate, undefined) + yield* Fiber.join(first) + streamGate = undefined + streamStarted = undefined + + expect(requests).toHaveLength(3) + expect(userTexts(requests[0]!)).toEqual(["Start working"]) + expect(userTexts(requests[1]!)).toEqual(["Start working"]) + expect(userTexts(requests[2]!)).toEqual(["Start working", "Wait until the next activity"]) + }), + ) + + it.effect("runs queued active inputs as separate FIFO activities", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + streamGate = yield* Deferred.make() + streamStarted = yield* Deferred.make() + + const first = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Deferred.await(streamStarted) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue first" }), delivery: "queue" }) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue second" }), delivery: "queue" }) + yield* Deferred.succeed(streamGate, undefined) + yield* Fiber.join(first) + streamGate = undefined + streamStarted = undefined + + expect(requests).toHaveLength(3) + expect(userTexts(requests[0]!)).toEqual(["Start working"]) + expect(userTexts(requests[1]!)).toEqual(["Start working", "Queue first"]) + expect(userTexts(requests[2]!)).toEqual(["Start working", "Queue first", "Queue second"]) + }), + ) + + it.effect("opens queued input after idle steering activity settles", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start steering activity" }), resume: false }) + yield* session.prompt({ + sessionID, + prompt: new Prompt({ text: "Queue later activity" }), + delivery: "queue", + resume: false, + }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(2) + expect(userTexts(requests[0]!)).toEqual(["Start steering activity"]) + expect(userTexts(requests[1]!)).toEqual(["Start steering activity", "Queue later activity"]) + }), + ) + + it.effect("coalesces steers into the active queued activity before starting the next queued activity", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + const firstGate = yield* Deferred.make() + const secondGate = yield* Deferred.make() + streamGate = firstGate + + const first = yield* session.resume(sessionID).pipe(Effect.forkChild) + while (requests.length < 1) yield* Effect.yieldNow + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue first" }), delivery: "queue" }) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue second" }), delivery: "queue" }) + streamGate = secondGate + yield* Deferred.succeed(firstGate, undefined) + while (requests.length < 2) yield* Effect.yieldNow + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Steer first queued activity" }) }) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Also steer first queued activity" }) }) + yield* Deferred.succeed(secondGate, undefined) + yield* Fiber.join(first) + streamGate = undefined + + expect(requests).toHaveLength(4) + expect(userTexts(requests[0]!)).toEqual(["Start working"]) + expect(userTexts(requests[1]!)).toEqual(["Start working", "Queue first"]) + expect(userTexts(requests[2]!)).toEqual([ + "Start working", + "Queue first", + "Steer first queued activity", + "Also steer first queued activity", + ]) + expect(userTexts(requests[3]!)).toEqual([ + "Start working", + "Queue first", + "Steer first queued activity", + "Also steer first queued activity", + "Queue second", + ]) + }), + ) + + it.effect("coalesces multiple active steering prompts into one continuation turn", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + streamGate = yield* Deferred.make() + streamStarted = yield* Deferred.make() + + const first = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Deferred.await(streamStarted) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First steer" }) }) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second steer" }) }) + yield* Deferred.succeed(streamGate, undefined) + yield* Fiber.join(first) + streamGate = undefined + streamStarted = undefined + yield* Effect.yieldNow + + expect(requests).toHaveLength(2) + expect(userTexts(requests[1]!)).toEqual(["Start working", "First steer", "Second steer"]) + yield* (yield* SessionRunCoordinator.Service).wake(sessionID) + yield* Effect.yieldNow + expect(requests).toHaveLength(2) + }), + ) + + it.effect("runs steering input accepted while the active provider turn fails", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + + requests.length = 0 + responses = undefined + response = [] + streamFailure = providerUnavailable() + streamGate = yield* Deferred.make() + streamStarted = yield* Deferred.make() + + const first = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Deferred.await(streamStarted) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover with this" }) }) + yield* Deferred.succeed(streamGate, undefined) + expect(yield* Fiber.join(first).pipe(Effect.flip)).toBe(streamFailure) + + streamFailure = undefined + streamGate = undefined + streamStarted = undefined + yield* Effect.yieldNow + + expect(requests).toHaveLength(2) + expect(userTexts(requests[1]!)).toEqual(["Start working", "Recover with this"]) + }), + ) + + it.effect("durably fails local tools left running by a prior process before continuing", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover interrupted tool" }), resume: false }) + yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID) + const assistant = yield* events.publish(SessionEvent.Step.Started, { + sessionID, + timestamp: yield* DateTime.now, + agent: "build", + model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, + }) + yield* events.publish(SessionEvent.Tool.Input.Started, { + sessionID, + timestamp: yield* DateTime.now, + assistantMessageID: assistant.id, + callID: "call-interrupted", + name: "echo", + }) + yield* events.publish(SessionEvent.Tool.Input.Ended, { + sessionID, + timestamp: yield* DateTime.now, + assistantMessageID: assistant.id, + callID: "call-interrupted", + text: '{"text":"stale"}', + }) + yield* events.publish(SessionEvent.Tool.Called, { + sessionID, + timestamp: yield* DateTime.now, + assistantMessageID: assistant.id, + callID: "call-interrupted", + tool: "echo", + input: { text: "stale" }, + provider: { executed: false }, + }) + requests.length = 0 + response = [] + yield* session.resume(sessionID) + + expect(requests).toHaveLength(1) + expect(requests[0]?.messages.map((message) => message.role)).toEqual(["user", "assistant", "tool"]) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Recover interrupted tool" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "call-interrupted", + state: { status: "error", error: { type: "unknown", message: "Tool execution interrupted" } }, + }, + ], + }, + ]) + }), + ) + + it.effect("durably fails hosted tools left running by a prior process before continuing inline", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + yield* session.prompt({ + sessionID, + prompt: new Prompt({ text: "Recover interrupted hosted tool" }), + resume: false, + }) + yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID) + const assistant = yield* events.publish(SessionEvent.Step.Started, { + sessionID, + timestamp: yield* DateTime.now, + agent: "build", + model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, + }) + yield* events.publish(SessionEvent.Tool.Input.Started, { + sessionID, + timestamp: yield* DateTime.now, + assistantMessageID: assistant.id, + callID: "call-hosted-interrupted", + name: "web_search", + }) + yield* events.publish(SessionEvent.Tool.Input.Ended, { + sessionID, + timestamp: yield* DateTime.now, + assistantMessageID: assistant.id, + callID: "call-hosted-interrupted", + text: '{"query":"stale"}', + }) + yield* events.publish(SessionEvent.Tool.Called, { + sessionID, + timestamp: yield* DateTime.now, + assistantMessageID: assistant.id, + callID: "call-hosted-interrupted", + tool: "web_search", + input: { query: "stale" }, + provider: { executed: true, metadata: { openai: { itemId: "call-hosted-interrupted" } } }, + }) + requests.length = 0 + response = [] + yield* session.resume(sessionID) + + expect(requests).toHaveLength(1) + expect(requests[0]?.messages.map((message) => message.role)).toEqual(["user", "assistant"]) + expect(requests[0]?.messages[1]?.content).toMatchObject([ + { + type: "tool-call", + id: "call-hosted-interrupted", + providerExecuted: true, + providerMetadata: { openai: { itemId: "call-hosted-interrupted" } }, + }, + { type: "tool-result", id: "call-hosted-interrupted", providerExecuted: true, result: { type: "error" } }, + ]) + }), + ) + + it.effect("durably fails pending tool input left by a prior process before continuing", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + yield* session.prompt({ + sessionID, + prompt: new Prompt({ text: "Recover interrupted tool input" }), + resume: false, + }) + yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID) + const assistant = yield* events.publish(SessionEvent.Step.Started, { + sessionID, + timestamp: yield* DateTime.now, + agent: "build", + model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, + }) + yield* events.publish(SessionEvent.Tool.Input.Started, { + sessionID, + timestamp: yield* DateTime.now, + assistantMessageID: assistant.id, + callID: "call-pending-interrupted", + name: "echo", + }) + requests.length = 0 + response = [] + yield* session.resume(sessionID) + + expect(requests).toHaveLength(1) + expect(requests[0]?.messages.map((message) => message.role)).toEqual(["user", "assistant", "tool"]) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Recover interrupted tool input" }, + { type: "assistant", content: [{ type: "tool", id: "call-pending-interrupted", state: { status: "error" } }] }, + ]) + }), + ) + + it.effect("starts the first queued activity when woken while idle", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ + sessionID, + prompt: new Prompt({ text: "Wait for fresh activity" }), + delivery: "queue", + resume: false, + }) + + requests.length = 0 + yield* (yield* SessionRunCoordinator.Service).wake(sessionID) + yield* Effect.yieldNow + + expect(requests).toHaveLength(1) + expect(userTexts(requests[0]!)).toEqual(["Wait for fresh activity"]) + }), + ) + + it.effect("does not spend one activity step budget across queued activities", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const queued = Array.from({ length: 26 }, (_, index) => `Queued activity ${index + 1}`) + for (const text of queued) { + yield* session.prompt({ sessionID, prompt: new Prompt({ text }), delivery: "queue", resume: false }) + } + + requests.length = 0 + responses = queued.map(() => [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ]) + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(queued.length) + expect(userTexts(requests.at(-1)!)).toEqual(queued) + }), + ) + + it.effect("retries inbox input after prompt projection rolls back", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const events = yield* EventV2.Service + const defect = new Error("fail after prompt promotion") + let fail = true + yield* events.project(SessionEvent.Prompted, () => (fail ? Effect.die(defect) : Effect.void)) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover promoted input" }), resume: false }) + + expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect) + fail = false + requests.length = 0 + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ] + + yield* (yield* SessionRunCoordinator.Service).wake(sessionID) + while (requests.length === 0) yield* Effect.yieldNow + + expect(userTexts(requests[0]!)).toEqual(["Recover promoted input"]) + }), + ) + + it.effect("runs different sessions concurrently", () => + Effect.gen(function* () { + yield* setup + yield* insertSession(otherSessionID) + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run first" }), resume: false }) + yield* session.prompt({ sessionID: otherSessionID, prompt: new Prompt({ text: "Run second" }), resume: false }) + + requests.length = 0 + responses = undefined + response = [] + streamGate = yield* Deferred.make() + streamStarted = yield* Deferred.make() + + const first = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Deferred.await(streamStarted) + const second = yield* session.resume(otherSessionID).pipe(Effect.forkChild) + yield* Effect.yieldNow + + expect(requests).toHaveLength(2) + yield* Deferred.succeed(streamGate, undefined) + yield* Fiber.join(first) + yield* Fiber.join(second) + streamGate = undefined + streamStarted = undefined + }), + ) + + it.effect("fans out one failed run and allows a later retry", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Retry after failure" }), resume: false }) + + requests.length = 0 + responses = undefined + response = [] + streamFailure = providerUnavailable() + streamGate = yield* Deferred.make() + streamStarted = yield* Deferred.make() + + const first = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Deferred.await(streamStarted) + const second = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Effect.yieldNow + + expect(requests).toHaveLength(1) + yield* Deferred.succeed(streamGate, undefined) + const [firstExit, secondExit] = yield* Effect.all([Fiber.await(first), Fiber.await(second)]) + expect(secondExit).toEqual(firstExit) + + streamFailure = undefined + streamGate = undefined + streamStarted = undefined + yield* session.resume(sessionID) + expect(requests).toHaveLength(2) + }), + ) + + it.effect("durably settles local tool failures before continuing", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Call missing" }), resume: false }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-missing", name: "missing", input: {} }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.textStart({ id: "text-after-error" }), + LLMEvent.textDelta({ id: "text-after-error", text: "Recovered" }), + LLMEvent.textEnd({ id: "text-after-error" }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + streamGate = undefined + streamStarted = undefined + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(2) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Call missing" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "call-missing", + state: { status: "error", error: { message: "Unknown tool: missing" } }, + }, + ], + }, + { type: "assistant", finish: "stop", content: [{ type: "text", id: "text-after-error", text: "Recovered" }] }, + ]) + }), + ) + + it.effect("durably settles unexpected local tool defects before continuing", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Call defect" }), resume: false }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-defect", name: "defect", input: {} }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ], + [], + ] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(2) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Call defect" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "call-defect", + state: { status: "error", error: { message: "unexpected tool defect" } }, + }, + ], + }, + ]) + }), + ) + + it.effect("interrupts runner continuation when a question is dismissed", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const registry = yield* ToolRegistry.Service + const questions = yield* QuestionV2.Service + const transform = yield* registry.transform() + yield* transform((editor) => + editor.set("question", { + tool: Tool.make({ + description: "Ask the user", + parameters: Schema.Struct({}), + success: Schema.Struct({}), + }), + execute: ({ sessionID }) => questions.ask({ sessionID, questions: [] }).pipe(Effect.as({}), Effect.orDie), + }), + ) + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Ask then stop" }), resume: false }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-question", name: "question", input: {} }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ], + [], + ] + + const run = yield* session.resume(sessionID).pipe(Effect.exit, Effect.forkChild) + let pending = yield* questions.list() + while (pending.length === 0) { + yield* Effect.yieldNow + pending = yield* questions.list() + } + yield* questions.reject(pending[0]!.id) + const exit = yield* Fiber.join(run) + + expect(exit._tag).toBe("Failure") + if (exit._tag === "Failure") expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true) + expect(requests).toHaveLength(1) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Ask then stop" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "call-question", + state: { status: "error", error: { type: "unknown", message: "Tool execution interrupted" } }, + }, + ], + }, + ]) + }), + ) + + it.effect("awaits started local tools before surfacing provider stream failure", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Settle before failing" }), resume: false }) + const failure = providerUnavailable() + toolExecutionGate = yield* Deferred.make() + responseStream = Stream.concat( + Stream.fromIterable([ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-before-failure", name: "echo", input: { text: "settle" } }), + ]), + Stream.fail(failure), + ) + + const run = yield* session.resume(sessionID).pipe(Effect.forkChild) + while (executions.length === 0) yield* Effect.yieldNow + yield* Effect.yieldNow + yield* Deferred.succeed(toolExecutionGate, undefined) + expect(yield* Fiber.join(run).pipe(Effect.flip)).toBe(failure) + toolExecutionGate = undefined + + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Settle before failing" }, + { + type: "assistant", + content: [ + { type: "tool", id: "call-before-failure", state: { status: "completed", structured: { text: "settle" } } }, + ], + }, + ]) + }), + ) + + it.effect("durably fails blocked local tools when a provider turn is interrupted", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt blocked tool" }), resume: false }) + executions.length = 0 + toolExecutionGate = yield* Deferred.make() + responseStream = Stream.concat( + Stream.fromIterable([ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-before-interrupt", name: "echo", input: { text: "blocked" } }), + ]), + Stream.never, + ) + + const runner = yield* SessionRunner.Service + const run = yield* runner.run({ sessionID, force: true }).pipe(Effect.forkChild) + while (executions.length === 0) yield* Effect.yieldNow + yield* Fiber.interrupt(run) + toolExecutionGate = undefined + + expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" }) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Interrupt blocked tool" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "call-before-interrupt", + state: { status: "error", error: { type: "unknown", message: "Tool execution interrupted" } }, + }, + ], + }, + ]) + + yield* replaySessionProjection(sessionID) + + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Interrupt blocked tool" }, + { type: "assistant", content: [{ type: "tool", id: "call-before-interrupt", state: { status: "error" } }] }, + ]) + requests.length = 0 + responseStream = undefined + response = [] + yield* session.resume(sessionID) + expect(requests[0]?.messages.map((message) => message.role)).toEqual(["user", "assistant", "tool"]) + }), + ) + + it.effect("durably fails blocked local tools when interrupted while awaiting settlement", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt tool settlement" }), resume: false }) + executions.length = 0 + toolExecutionGate = yield* Deferred.make() + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-await-interrupt", name: "echo", input: { text: "blocked" } }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ] + + const runner = yield* SessionRunner.Service + const run = yield* runner.run({ sessionID, force: true }).pipe(Effect.forkChild) + while (executions.length === 0) yield* Effect.yieldNow + yield* Fiber.interrupt(run) + toolExecutionGate = undefined + + expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" }) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Interrupt tool settlement" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "call-await-interrupt", + state: { status: "error", error: { type: "unknown", message: "Tool execution interrupted" } }, + }, + ], + }, + ]) + }), + ) + + it.effect("fails after the bounded number of local tool continuation steps", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Loop forever" }), resume: false }) + + requests.length = 0 + authorizations.length = 0 + executions.length = 0 + streamGate = undefined + streamStarted = undefined + responses = Array.from({ length: 25 }, (_, index) => [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: `call-echo-${index}`, name: "echo", input: { text: `${index}` } }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ]) + + const failure = yield* session.resume(sessionID).pipe(Effect.flip) + + expect(failure).toMatchObject({ _tag: "SessionRunner.StepLimitExceededError", sessionID, limit: 25 }) + expect(requests).toHaveLength(25) + expect(executions).toHaveLength(25) + }), + ) + + it.effect("does not restart a capped tool loop for a coalesced stale wake", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const coordinator = yield* SessionRunCoordinator.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Loop forever" }), resume: false }) + + requests.length = 0 + responses = Array.from({ length: 25 }, (_, index) => [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: `call-capped-${index}`, name: "echo", input: { text: `${index}` } }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ]) + streamGate = yield* Deferred.make() + streamStarted = yield* Deferred.make() + + const run = yield* session.resume(sessionID).pipe(Effect.forkChild) + yield* Deferred.await(streamStarted) + yield* coordinator.wake(sessionID) + yield* Deferred.succeed(streamGate, undefined) + expect(yield* Fiber.join(run).pipe(Effect.flip)).toMatchObject({ _tag: "SessionRunner.StepLimitExceededError" }) + streamGate = undefined + streamStarted = undefined + yield* Effect.yieldNow + + expect(requests).toHaveLength(25) + }), + ) + + it.effect("accepts a terminal response on the final bounded provider turn", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Finish at the limit" }), resume: false }) + + requests.length = 0 + responses = [ + ...Array.from({ length: 24 }, (_, index) => [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: `call-terminal-${index}`, name: "echo", input: { text: `${index}` } }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ]), + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(25) + }), + ) + + it.effect("projects provider errors as terminal assistant step failures", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail durably" }), resume: false }) + + requests.length = 0 + responses = undefined + streamGate = undefined + streamStarted = undefined + response = [LLMEvent.stepStart({ index: 0 }), LLMEvent.providerError({ message: "Provider unavailable" })] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(1) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Fail durably" }, + { type: "assistant", finish: "error", error: { type: "unknown", message: "Provider unavailable" } }, + ]) + }), + ) + + it.effect("projects provider errors emitted before assistant step start", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail before step" }), resume: false }) + + requests.length = 0 + response = [LLMEvent.providerError({ message: "Provider unavailable" })] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(1) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Fail before step" }, + { type: "assistant", finish: "error", error: { type: "unknown", message: "Provider unavailable" } }, + ]) + }), + ) + + it.effect("projects raw provider stream failures as terminal assistant step failures", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail raw stream durably" }), resume: false }) + const failure = providerUnavailable() + responseStream = Stream.fail(failure) + + expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure) + yield* replaySessionProjection(sessionID) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Fail raw stream durably" }, + { type: "assistant", finish: "error", error: { type: "unknown", message: "Provider unavailable" } }, + ]) + }), + ) + + it.effect("does not continue automatically after a provider error follows a local tool call", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ + sessionID, + prompt: new Prompt({ text: "Do not continue failed provider" }), + resume: false, + }) + + requests.length = 0 + const executionCount = executions.length + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-before-provider-error", name: "echo", input: { text: "settled" } }), + LLMEvent.providerError({ message: "Provider unavailable" }), + ] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(1) + expect(executions.slice(executionCount)).toEqual(["settled"]) + }), + ) + + it.effect("durably fails a hosted tool when its provider errors before returning a result", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail hosted tool durably" }), resume: false }) + + requests.length = 0 + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ + id: "call-hosted-provider-error", + name: "web_search", + input: { query: "effect" }, + providerExecuted: true, + }), + LLMEvent.providerError({ message: "Provider unavailable" }), + ] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(1) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Fail hosted tool durably" }, + { + type: "assistant", + content: [{ type: "tool", id: "call-hosted-provider-error", state: { status: "error" } }], + }, + ]) + }), + ) + + it.effect("durably fails a hosted tool left unresolved at normal provider EOF", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail hosted tool at EOF" }), resume: false }) + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ + id: "call-hosted-eof", + name: "web_search", + input: { query: "effect" }, + providerExecuted: true, + }), + ] + + yield* session.resume(sessionID) + yield* replaySessionProjection(sessionID) + + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Fail hosted tool at EOF" }, + { type: "assistant", content: [{ type: "tool", id: "call-hosted-eof", state: { status: "error" } }] }, + ]) + }), + ) + + it.effect("durably fails a hosted tool left unresolved by a raw provider stream failure", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail hosted tool on raw failure" }), resume: false }) + const failure = providerUnavailable() + responseStream = Stream.concat( + Stream.fromIterable([ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ + id: "call-hosted-raw-failure", + name: "web_search", + input: { query: "effect" }, + providerExecuted: true, + }), + ]), + Stream.fail(failure), + ) + + expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure) + yield* replaySessionProjection(sessionID) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Fail hosted tool on raw failure" }, + { + type: "assistant", + finish: "error", + error: { type: "unknown", message: "Provider unavailable" }, + content: [{ type: "tool", id: "call-hosted-raw-failure", state: { status: "error" } }], + }, + ]) + }), + ) + + it.effect("keeps interleaved assistant text blocks separate", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Two blocks" }), resume: false }) + + responses = undefined + streamGate = undefined + streamStarted = undefined + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.textStart({ id: "text-1" }), + LLMEvent.textStart({ id: "text-2" }), + LLMEvent.textDelta({ id: "text-1", text: "First" }), + LLMEvent.textDelta({ id: "text-2", text: "Second" }), + LLMEvent.textEnd({ id: "text-1" }), + LLMEvent.textEnd({ id: "text-2" }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ] + + yield* session.resume(sessionID) + + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Two blocks" }, + { + type: "assistant", + content: [ + { type: "text", id: "text-1", text: "First" }, + { type: "text", id: "text-2", text: "Second" }, + ], + }, + ]) + }), + ) + + for (const kind of fragmentKinds) { + it.effect(`broadcasts provider ${kind} deltas without storing projection rewrites`, () => + verifyEphemeralDeltas(kind), + ) + + it.effect(`durably closes partial ${kind} when the provider stream fails`, () => verifyPartialFlushOnFailure(kind)) + + it.effect(`durably closes partial ${kind} when the provider stream is interrupted`, () => + verifyPartialFlushOnInterruption(kind), + ) + } + + it.effect("rejects duplicate streamed text starts", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + responses = undefined + streamGate = undefined + streamStarted = undefined + response = [LLMEvent.textStart({ id: "text-1" }), LLMEvent.textStart({ id: "text-1" })] + + expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe( + "Duplicate text start: text-1", + ) + }), + ) + + it.effect("transitions streamed raw tool input to parsed called input", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Call provider tool" }), resume: false }) + + responses = undefined + streamGate = undefined + streamStarted = undefined + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolInputStart({ id: "call-parsed", name: "web_search" }), + LLMEvent.toolInputDelta({ id: "call-parsed", name: "web_search", text: '{"query":"hello"}' }), + LLMEvent.toolInputEnd({ id: "call-parsed", name: "web_search" }), + LLMEvent.toolCall({ id: "call-parsed", name: "web_search", input: { query: "hello" }, providerExecuted: true }), + ] + + yield* session.resume(sessionID) + + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Call provider tool" }, + { + type: "assistant", + content: [{ type: "tool", id: "call-parsed", state: { status: "error", input: { query: "hello" } } }], + }, + ]) + }), + ) + + it.effect("rejects malformed streamed tool input ordering", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + responses = undefined + streamGate = undefined + streamStarted = undefined + response = [LLMEvent.toolInputDelta({ id: "call-1", name: "read", text: "{}" })] + + expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe( + "Tool input delta before start: call-1", + ) + }), + ) +}) diff --git a/packages/core/test/session-system-context.test.ts b/packages/core/test/session-system-context.test.ts new file mode 100644 index 0000000000..a2c979d069 --- /dev/null +++ b/packages/core/test/session-system-context.test.ts @@ -0,0 +1,70 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import * as TestClock from "effect/testing/TestClock" +import { Location } from "@opencode-ai/core/location" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionSystemContext } from "@opencode-ai/core/session-system-context" +import { SystemContext } from "@opencode-ai/core/system-context" +import { location } from "./fixture/location" +import { testEffect } from "./lib/effect" + +const directory = AbsolutePath.make("/repo/packages/core") +const projectDirectory = AbsolutePath.make("/repo") +const timestamp = Date.parse("2026-06-03T12:00:00.000Z") +const localDate = (time: number) => new Date(time).toDateString() +const it = testEffect( + SessionSystemContext.locationLayer.pipe( + Layer.provide( + Layer.succeed( + Location.Service, + Location.Service.of( + location({ directory }, { projectDirectory, vcs: { type: "git", store: AbsolutePath.make("/repo/.git") } }), + ), + ), + ), + ), +) + +describe("SessionSystemContext", () => { + it.effect("loads location-scoped environment and host-local date context", () => + Effect.gen(function* () { + yield* TestClock.setTime(timestamp) + const context = yield* SessionSystemContext.Service + const initialized = SystemContext.initialize(yield* context.load()) + + expect(initialized.baseline).toEqual([ + { + key: SystemContext.Key.make("core/environment"), + text: [ + "Here is some useful information about the environment you are running in:", + "", + ` Working directory: ${directory}`, + ` Workspace root folder: ${projectDirectory}`, + " Is directory a git repo: yes", + ` Platform: ${process.platform}`, + "", + ].join("\n"), + }, + { key: SystemContext.Key.make("core/date"), text: `Today's date: ${localDate(timestamp)}` }, + ]) + }), + ) + + it.effect("refreshes the date without repeating unchanged environment context", () => + Effect.gen(function* () { + yield* TestClock.setTime(timestamp) + const context = yield* SessionSystemContext.Service + const initialized = SystemContext.initialize(yield* context.load()) + + yield* TestClock.setTime(timestamp + 24 * 60 * 60 * 1000) + const refreshed = SystemContext.refresh(yield* context.load(), initialized.checkpoint) + + expect(refreshed.changes).toEqual([ + { + key: SystemContext.Key.make("core/date"), + text: `Today's date is now: ${localDate(timestamp + 24 * 60 * 60 * 1000)}`, + }, + ]) + }), + ) +}) diff --git a/packages/core/test/session-todo.test.ts b/packages/core/test/session-todo.test.ts new file mode 100644 index 0000000000..d1d656af38 --- /dev/null +++ b/packages/core/test/session-todo.test.ts @@ -0,0 +1,95 @@ +import { describe, expect } from "bun:test" +import { asc } from "drizzle-orm" +import { Effect, Layer } from "effect" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { Project } from "@opencode-ai/core/project" +import { ProjectTable } from "@opencode-ai/core/project/sql" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { SessionTable, TodoTable } from "@opencode-ai/core/session/sql" +import { SessionTodo } from "@opencode-ai/core/session/todo" +import { testEffect } from "./lib/effect" + +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const todos = SessionTodo.layer.pipe(Layer.provide(database), Layer.provide(events)) +const it = testEffect(Layer.mergeAll(database, events, todos)) +const sessionID = SessionV2.ID.make("ses_todo_test") + +const setup = Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "todo", + directory: "/project", + title: "todo", + version: "test", + }) + .run() + .pipe(Effect.orDie) +}) + +describe("SessionTodo", () => { + it.effect("replaces persisted todos in order and publishes updates", () => + Effect.gen(function* () { + yield* setup + const { db } = yield* Database.Service + const events = yield* EventV2.Service + const todos = yield* SessionTodo.Service + const published = new Array() + const unsubscribe = yield* events.listen((event) => + Effect.sync(() => { + if (event.type === SessionTodo.Event.Updated.type) published.push(event) + }), + ) + yield* Effect.addFinalizer(() => unsubscribe) + + yield* todos.update({ + sessionID, + todos: [ + { content: "second", status: "pending", priority: "low" }, + { content: "first", status: "in_progress", priority: "high" }, + ], + }) + expect(yield* todos.get(sessionID)).toEqual([ + { content: "second", status: "pending", priority: "low" }, + { content: "first", status: "in_progress", priority: "high" }, + ]) + expect( + (yield* db.select().from(TodoTable).orderBy(asc(TodoTable.position)).all().pipe(Effect.orDie)).map((row) => ({ + content: row.content, + position: row.position, + })), + ).toEqual([ + { content: "second", position: 0 }, + { content: "first", position: 1 }, + ]) + + yield* todos.update({ sessionID, todos: [{ content: "replacement", status: "completed", priority: "medium" }] }) + expect(yield* todos.get(sessionID)).toEqual([{ content: "replacement", status: "completed", priority: "medium" }]) + + yield* todos.update({ sessionID, todos: [] }) + expect(yield* todos.get(sessionID)).toEqual([]) + expect(published.map((event) => event.data)).toEqual([ + { + sessionID, + todos: [ + { content: "second", status: "pending", priority: "low" }, + { content: "first", status: "in_progress", priority: "high" }, + ], + }, + { sessionID, todos: [{ content: "replacement", status: "completed", priority: "medium" }] }, + { sessionID, todos: [] }, + ]) + }), + ) +}) diff --git a/packages/core/test/session-tool-progress.test.ts b/packages/core/test/session-tool-progress.test.ts new file mode 100644 index 0000000000..0e9e0f872d --- /dev/null +++ b/packages/core/test/session-tool-progress.test.ts @@ -0,0 +1,70 @@ +import { describe, expect } from "bun:test" +import { asc, eq } from "drizzle-orm" +import { DateTime, Effect, Layer, Schema } from "effect" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { EventTable } from "@opencode-ai/core/event/sql" +import { ModelV2 } from "@opencode-ai/core/model" +import { Project } from "@opencode-ai/core/project" +import { ProjectTable } from "@opencode-ai/core/project/sql" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { SessionEvent } from "@opencode-ai/core/session/event" +import { SessionMessage } from "@opencode-ai/core/session/message" +import { SessionProjector } from "@opencode-ai/core/session/projector" +import { SessionTable, SessionMessageTable } from "@opencode-ai/core/session/sql" +import { ToolOutput } from "@opencode-ai/core/tool-output" +import { testEffect } from "./lib/effect" + +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database)) +const it = testEffect(Layer.mergeAll(database, events, projector)) +const timestamp = DateTime.makeUnsafe(1) +const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") } + +const content = (text: string) => [ToolOutput.text({ type: "text", text })] + +describe("Tool.Progress", () => { + it.effect("projects durable progress and keeps final settlements durable", () => + Effect.gen(function* () { + const { db } = yield* Database.Service + const service = yield* EventV2.Service + const sessionID = SessionV2.ID.make("ses_tool_progress_projector") + yield* db.insert(ProjectTable).values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }).onConflictDoNothing().run().pipe(Effect.orDie) + yield* db.insert(SessionTable).values({ id: sessionID, project_id: Project.ID.global, slug: "progress", directory: "/project", title: "progress", version: "test" }).run().pipe(Effect.orDie) + const assistantMessageID = (yield* service.publish(SessionEvent.Step.Started, { sessionID, timestamp, agent: "build", model })).id + const readAssistant = Effect.gen(function* () { + const row = yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.id, assistantMessageID)).get().pipe(Effect.orDie) + if (!row) return yield* Effect.die("Missing projected assistant") + return Schema.decodeUnknownSync(SessionMessage.Assistant)({ ...row.data, id: row.id, type: row.type }) + }) + const start = (callID: string) => Effect.gen(function* () { + yield* service.publish(SessionEvent.Tool.Input.Started, { sessionID, timestamp, assistantMessageID, callID, name: "bash" }) + yield* service.publish(SessionEvent.Tool.Called, { sessionID, timestamp, assistantMessageID, callID, tool: "bash", input: { command: "pwd" }, provider: { executed: false } }) + }) + + yield* start("call-success") + expect((yield* readAssistant).content[0]).toMatchObject({ state: { status: "running", structured: {}, content: [] } }) + + yield* service.publish(SessionEvent.Tool.Progress, { sessionID, timestamp, assistantMessageID, callID: "call-success", structured: { phase: "checkpoint" }, content: content("saved") }) + expect((yield* readAssistant).content[0]).toMatchObject({ state: { status: "running", structured: { phase: "checkpoint" }, content: content("saved") } }) + + const success = yield* service.publish(SessionEvent.Tool.Success, { sessionID, timestamp, assistantMessageID, callID: "call-success", structured: { phase: "done" }, content: content("complete"), provider: { executed: false } }) + expect((yield* readAssistant).content[0]).toMatchObject({ state: { status: "completed", structured: { phase: "done" }, content: content("complete") } }) + + yield* start("call-failed") + yield* service.publish(SessionEvent.Tool.Progress, { sessionID, timestamp, assistantMessageID, callID: "call-failed", structured: { phase: "checkpoint" }, content: content("before failure") }) + const failed = yield* service.publish(SessionEvent.Tool.Failed, { sessionID, timestamp, assistantMessageID, callID: "call-failed", error: { type: "unknown", message: "boom" }, provider: { executed: false } }) + expect((yield* readAssistant).content[1]).toMatchObject({ state: { status: "error", structured: { phase: "checkpoint" }, content: content("before failure"), error: { type: "unknown", message: "boom" } } }) + expect(Schema.is(SessionEvent.Durable)(success)).toBe(true) + expect(Schema.is(SessionEvent.Durable)(failed)).toBe(true) + + const rows = yield* db.select({ type: EventTable.type }).from(EventTable).where(eq(EventTable.aggregate_id, sessionID)).orderBy(asc(EventTable.seq)).all().pipe(Effect.orDie) + expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Progress.type, 1)) + expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Success.type, 1)) + expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Failed.type, 1)) + }), + ) +}) diff --git a/packages/core/test/skill-discovery.test.ts b/packages/core/test/skill-discovery.test.ts new file mode 100644 index 0000000000..fc83de19c7 --- /dev/null +++ b/packages/core/test/skill-discovery.test.ts @@ -0,0 +1,107 @@ +import fs from "fs/promises" +import path from "path" +import { describe, expect, test } from "bun:test" +import { Effect, Layer } from "effect" +import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Global } from "@opencode-ai/core/global" +import { SkillDiscovery } from "@opencode-ai/core/skill/discovery" +import { tmpdir } from "./fixture/tmpdir" + +const base = "https://skills.example.test/catalog/" + +async function pull(skills: unknown[], files: Record = {}) { + const tmp = await tmpdir() + const requests: string[] = [] + const http = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.sync(() => requests.push(request.url)).pipe( + Effect.map(() => { + const body = request.url === `${base}index.json` ? JSON.stringify({ skills }) : files[request.url] + return HttpClientResponse.fromWeb( + request, + new Response(body ?? "Not Found", { status: body === undefined ? 404 : 200 }), + ) + }), + ), + ), + ) + const layer = SkillDiscovery.layer.pipe( + Layer.provide(http), + Layer.provide(FSUtil.defaultLayer), + Layer.provide(Global.layerWith({ cache: tmp.path })), + ) + const directories = await Effect.runPromise( + Effect.gen(function* () { + return yield* (yield* SkillDiscovery.Service).pull(base) + }).pipe(Effect.provide(layer)), + ) + return { tmp, requests, directories } +} + +describe("SkillDiscovery.pull", () => { + test("rejects skill name traversal without fetching files", async () => { + const result = await pull([{ name: "../outside", files: ["SKILL.md"] }]) + try { + expect(result.directories).toEqual([]) + expect(result.requests).toEqual([`${base}index.json`]) + expect(await fs.readdir(result.tmp.path)).toEqual([]) + } finally { + await result.tmp[Symbol.asyncDispose]() + } + }) + + test("rejects file traversal without fetching files", async () => { + const result = await pull([{ name: "deploy", files: ["SKILL.md", "../outside.md"] }]) + try { + expect(result.directories).toEqual([]) + expect(result.requests).toEqual([`${base}index.json`]) + expect(await fs.readdir(result.tmp.path)).toEqual([]) + } finally { + await result.tmp[Symbol.asyncDispose]() + } + }) + + test("rejects absolute file paths without fetching files", async () => { + const result = await pull([{ name: "deploy", files: ["SKILL.md", "/tmp/outside.md"] }]) + try { + expect(result.directories).toEqual([]) + expect(result.requests).toEqual([`${base}index.json`]) + expect(await fs.readdir(result.tmp.path)).toEqual([]) + } finally { + await result.tmp[Symbol.asyncDispose]() + } + }) + + test("rejects cross-origin file URLs without fetching files", async () => { + const result = await pull([{ name: "deploy", files: ["SKILL.md", "https://evil.example.test/outside.md"] }]) + try { + expect(result.directories).toEqual([]) + expect(result.requests).toEqual([`${base}index.json`]) + expect(await fs.readdir(result.tmp.path)).toEqual([]) + } finally { + await result.tmp[Symbol.asyncDispose]() + } + }) + + test("downloads safe nested files under the skill root", async () => { + const result = await pull( + [{ name: "deploy", files: ["SKILL.md", "references/guide.md"] }], + { + [`${base}deploy/SKILL.md`]: "# Deploy", + [`${base}deploy/references/guide.md`]: "# Guide", + }, + ) + try { + expect(result.directories).toHaveLength(1) + expect(result.requests.toSorted()).toEqual( + [`${base}index.json`, `${base}deploy/SKILL.md`, `${base}deploy/references/guide.md`].toSorted(), + ) + expect(await fs.readFile(path.join(result.directories[0], "SKILL.md"), "utf8")).toBe("# Deploy") + expect(await fs.readFile(path.join(result.directories[0], "references", "guide.md"), "utf8")).toBe("# Guide") + } finally { + await result.tmp[Symbol.asyncDispose]() + } + }) +}) diff --git a/packages/core/test/system-context.test.ts b/packages/core/test/system-context.test.ts new file mode 100644 index 0000000000..e15843e63b --- /dev/null +++ b/packages/core/test/system-context.test.ts @@ -0,0 +1,188 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Schema } from "effect" +import { SystemContext } from "@opencode-ai/core/system-context" +import { Hash } from "@opencode-ai/core/util/hash" + +const key = SystemContext.Key.make + +describe("SystemContext", () => { + test("loads one coherent sample and initializes a deterministic baseline", async () => { + let loads = 0 + const context = SystemContext.struct({ + date: SystemContext.value({ + key: key("core/date"), + load: Effect.sync(() => { + loads++ + return { baseline: "Today's date is 2026-06-03.", update: "The current date is 2026-06-03." } + }), + }), + location: SystemContext.value({ + key: key("core/location"), + load: Effect.succeed({ baseline: "Working directory: /repo", update: "The working directory is /repo." }), + }), + }) + + const initialized = SystemContext.initialize(await Effect.runPromise(SystemContext.load(context))) + + expect(loads).toBe(1) + expect(initialized).toEqual({ + baseline: [ + { key: key("core/date"), text: "Today's date is 2026-06-03." }, + { key: key("core/location"), text: "Working directory: /repo" }, + ], + checkpoint: { + "core/date": Hash.sha256("The current date is 2026-06-03."), + "core/location": Hash.sha256("The working directory is /repo."), + }, + }) + }) + + test("emits changed and newly registered components in declaration order", async () => { + const context = SystemContext.struct({ + date: SystemContext.value({ + key: key("core/date"), + load: Effect.succeed({ baseline: "Today's date is 2026-06-04.", update: "The current date is 2026-06-04." }), + }), + location: SystemContext.value({ + key: key("core/location"), + load: Effect.succeed({ baseline: "Working directory: /repo", update: "The working directory is /repo." }), + }), + skills: SystemContext.value({ + key: key("core/skills"), + load: Effect.succeed({ baseline: "Available skills: effect", update: "Available skills: effect" }), + }), + }) + + const refreshed = SystemContext.refresh(await Effect.runPromise(SystemContext.load(context)), { + "core/date": Hash.sha256("The current date is 2026-06-03."), + "core/location": Hash.sha256("The working directory is /repo."), + }) + + expect(refreshed).toEqual({ + changes: [ + { key: key("core/date"), text: "The current date is 2026-06-04." }, + { key: key("core/skills"), text: "Available skills: effect" }, + ], + checkpoint: { + "core/date": Hash.sha256("The current date is 2026-06-04."), + "core/location": Hash.sha256("The working directory is /repo."), + "core/skills": Hash.sha256("Available skills: effect"), + }, + }) + expect(SystemContext.render(refreshed.changes)).toBe("The current date is 2026-06-04.\n\nAvailable skills: effect") + }) + + test("omits unavailable initial context and admits it after its first successful load", async () => { + let available = false + const context = SystemContext.struct({ + remote: SystemContext.value({ + key: key("core/remote-instructions"), + load: Effect.sync(() => + available + ? { baseline: "Remote instructions: available", update: "Remote instructions are now available." } + : SystemContext.unavailable, + ), + }), + }) + + const initialized = SystemContext.initialize(await Effect.runPromise(SystemContext.load(context))) + available = true + const refreshed = SystemContext.refresh( + await Effect.runPromise(SystemContext.load(context)), + initialized.checkpoint, + ) + + expect(initialized).toEqual({ baseline: [], checkpoint: {} }) + expect(refreshed.changes).toEqual([ + { key: key("core/remote-instructions"), text: "Remote instructions are now available." }, + ]) + }) + + test("retains an existing checkpoint while context is unavailable", async () => { + const previous = { "core/remote-instructions": Hash.sha256("Remote instructions: old") } + const context = SystemContext.struct({ + remote: SystemContext.value({ + key: key("core/remote-instructions"), + load: Effect.succeed(SystemContext.unavailable), + }), + }) + + const refreshed = SystemContext.refresh(await Effect.runPromise(SystemContext.load(context)), previous) + + expect(refreshed).toEqual({ changes: [], checkpoint: previous }) + }) + + test("drops checkpoints for removed components", async () => { + const context = SystemContext.struct({ + date: SystemContext.value({ + key: key("core/date"), + load: Effect.succeed({ baseline: "Today's date is 2026-06-03.", update: "The current date is 2026-06-03." }), + }), + }) + + const refreshed = SystemContext.refresh(await Effect.runPromise(SystemContext.load(context)), { + "core/date": Hash.sha256("The current date is 2026-06-03."), + "plugin/removed": Hash.sha256("Removed plugin context"), + }) + + expect(refreshed).toEqual({ + changes: [], + checkpoint: { "core/date": Hash.sha256("The current date is 2026-06-03.") }, + }) + }) + + test("ignores inherited checkpoint properties", async () => { + const context = SystemContext.struct({ + date: SystemContext.value({ + key: key("core/date"), + load: Effect.succeed({ baseline: "Today's date is 2026-06-03.", update: "The current date is 2026-06-03." }), + }), + }) + const previous = Object.create({ + "core/date": Hash.sha256("The current date is 2026-06-03."), + }) as SystemContext.Checkpoint + + const refreshed = SystemContext.refresh(await Effect.runPromise(SystemContext.load(context)), previous) + + expect(refreshed.changes).toEqual([{ key: key("core/date"), text: "The current date is 2026-06-03." }]) + expect(Object.hasOwn(refreshed.checkpoint, "core/date")).toBe(true) + }) + + test("preserves unexpected loader failures", async () => { + const context = SystemContext.struct({ + broken: SystemContext.value({ + key: key("plugin/broken"), + load: Effect.fail("broken loader"), + }), + }) + + await expect(Effect.runPromise(SystemContext.load(context))).rejects.toBe("broken loader") + }) + + test("rejects duplicate component keys", () => { + expect(() => + SystemContext.struct({ + one: SystemContext.value({ key: key("core/date"), load: Effect.succeed({ baseline: "one", update: "one" }) }), + two: SystemContext.value({ key: key("core/date"), load: Effect.succeed({ baseline: "two", update: "two" }) }), + }), + ).toThrow(new SystemContext.DuplicateKeyError({ key: key("core/date") })) + }) + + test("rejects duplicate component keys at the interpreter boundary", async () => { + const component = SystemContext.value({ + key: key("core/date"), + load: Effect.succeed({ baseline: "date", update: "date" }), + }) + const context: SystemContext.SystemContext = { components: [component, component] } + + await expect(Effect.runPromise(SystemContext.load(context))).rejects.toBeInstanceOf(SystemContext.DuplicateKeyError) + }) + + test("requires namespaced component keys", () => { + const decode = Schema.decodeUnknownSync(SystemContext.Key) + + expect(decode("core/date")).toBe(key("core/date")) + expect(() => decode("date")).toThrow() + expect(() => decode("core/")).toThrow() + }) +}) diff --git a/packages/core/test/tool-apply-patch.test.ts b/packages/core/test/tool-apply-patch.test.ts new file mode 100644 index 0000000000..6d070fe152 --- /dev/null +++ b/packages/core/test/tool-apply-patch.test.ts @@ -0,0 +1,320 @@ +import fs from "fs/promises" +import path from "path" +import { describe, expect } from "bun:test" +import { Deferred, Effect, Fiber, Layer } from "effect" +import { FileMutation } from "@opencode-ai/core/file-mutation" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Location } from "@opencode-ai/core/location" +import { LocationMutation } from "@opencode-ai/core/location-mutation" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { ApplyPatchTool } from "@opencode-ai/core/tool/apply-patch" +import { location } from "./fixture/location" +import { tmpdir } from "./fixture/tmpdir" +import { testEffect } from "./lib/effect" + +const sessionID = SessionV2.ID.make("ses_apply_patch_tool_test") +const assertions: PermissionV2.AssertInput[] = [] +let denyAction: string | undefined +let failRemoveTarget: string | undefined +let readsBeforeEditApproval = 0 +let editApproved = false +let blockRemoveTarget: string | undefined +let removeStarted: Deferred.Deferred | undefined +let releaseRemove: Deferred.Deferred | undefined + +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => { + assertions.push(input) + if (input.action === "edit") editApproved = true + }).pipe( + Effect.andThen(input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void), + ), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) + +const reset = () => { + assertions.length = 0 + denyAction = undefined + failRemoveTarget = undefined + readsBeforeEditApproval = 0 + editApproved = false + blockRemoveTarget = undefined + removeStarted = undefined + releaseRemove = undefined +} + +const filesystem = Layer.effect( + FSUtil.Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + return FSUtil.Service.of({ + ...fs, + readFile: (target) => + Effect.sync(() => { + if (!editApproved) readsBeforeEditApproval++ + }).pipe(Effect.andThen(fs.readFile(target))), + remove: (target, options) => { + if (failRemoveTarget && path.basename(target) === failRemoveTarget) return Effect.die("forced remove failure") + if (blockRemoveTarget && path.basename(target) === blockRemoveTarget && removeStarted && releaseRemove) + return Deferred.succeed(removeStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseRemove)), Effect.andThen(fs.remove(target, options))) + return fs.remove(target, options) + }, + }) + }), +).pipe(Layer.provide(FSUtil.defaultLayer)) + +const withTool = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => { + const activeLocation = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make(directory) })), + ) + const planning = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation)) + const commits = FileMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(planning)) + const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) + const patch = ApplyPatchTool.layer.pipe( + Layer.provide(registry), + Layer.provide(planning), + Layer.provide(commits), + Layer.provide(filesystem), + ) + return Effect.gen(function* () { + return yield* body(yield* ToolRegistry.Service) + }).pipe(Effect.provide(Layer.mergeAll(registry, planning, commits, patch))) +} + +const call = (patchText: string, id = "call-apply-patch") => ({ + sessionID, + call: { type: "tool-call" as const, id, name: "apply_patch", input: { patchText } }, +}) + +const exists = (target: string) => Effect.promise(() => fs.stat(target).then(() => true, () => false)) +const it = testEffect(Layer.empty) + +describe("ApplyPatchTool", () => { + it.live("registers and sequentially applies add, update, and delete hunks", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const update = path.join(tmp.path, "update.txt") + const remove = path.join(tmp.path, "remove.txt") + return Effect.promise(() => Promise.all([fs.writeFile(update, "before\n"), fs.writeFile(remove, "remove\n")])).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + Effect.gen(function* () { + expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["apply_patch"]) + const settled = yield* registry.settle( + call("*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Update File: update.txt\n@@\n-before\n+after\n*** Delete File: remove.txt\n*** End Patch"), + ) + expect(settled.result).toEqual({ + type: "text", + value: "Applied patch sequentially:\nA nested/new.txt\nM update.txt\nD remove.txt", + }) + expect(settled.output?.structured).toMatchObject({ + applied: [ + { type: "add", resource: "nested/new.txt" }, + { type: "update", resource: "update.txt" }, + { type: "delete", resource: "remove.txt" }, + ], + }) + expect(assertions).toEqual([ + { sessionID, action: "edit", resources: ["nested/new.txt", "update.txt", "remove.txt"], save: ["*"] }, + ]) + expect(readsBeforeEditApproval).toBe(0) + expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "nested/new.txt"), "utf8"))).toBe("created\n") + expect(yield* Effect.promise(() => fs.readFile(update, "utf8"))).toBe("after\n") + expect(yield* exists(remove)).toBe(false) + }), + ), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("rejects moves before applying any hunk", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const source = path.join(tmp.path, "old.txt") + return Effect.promise(() => fs.writeFile(source, "before\n")).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + Effect.gen(function* () { + expect( + yield* registry.execute(call("*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: old.txt\n*** Move to: moved.txt\n@@\n-before\n+after\n*** End Patch")), + ).toEqual({ type: "error", value: "apply_patch moves are not supported yet" }) + expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false) + expect(assertions).toEqual([]) + }), + ), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("approves an external directory and the batch before reading external update content", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + reset() + const target = path.join(outside.path, "external.txt") + return Effect.promise(() => fs.writeFile(target, "before\n")).pipe( + Effect.andThen( + withTool(active.path, (registry) => + Effect.gen(function* () { + expect( + yield* registry.execute(call(`*** Begin Patch\n*** Update File: ${target}\n@@\n-before\n+after\n*** End Patch`)), + ).toMatchObject({ type: "text" }) + expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"]) + expect(readsBeforeEditApproval).toBe(0) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after\n") + }), + ), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined)), + ), + ) + + it.live("approves one external directory scope for multiple files under the same parent", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + reset() + const first = path.join(outside.path, "first.txt") + const second = path.join(outside.path, "second.txt") + return Effect.promise(() => Promise.all([fs.writeFile(first, "before\n"), fs.writeFile(second, "before\n")])).pipe( + Effect.andThen( + withTool(active.path, (registry) => + Effect.gen(function* () { + expect( + yield* registry.execute(call(`*** Begin Patch\n*** Update File: ${first}\n@@\n-before\n+after\n*** Update File: ${second}\n@@\n-before\n+after\n*** End Patch`)), + ).toMatchObject({ type: "text" }) + expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"]) + expect(assertions[0]?.resources).toEqual([ + path.join(yield* Effect.promise(() => fs.realpath(outside.path)), "*").replaceAll("\\", "/"), + ]) + }), + ), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined)), + ), + ) + + it.live("rejects invalid later update before applying an earlier add", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + return withTool(tmp.path, (registry) => + Effect.gen(function* () { + expect( + yield* registry.execute(call("*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: missing.txt\n@@\n-before\n+after\n*** End Patch")), + ).toEqual({ type: "error", value: "Unable to apply patch at missing.txt" }) + expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false) + }), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("rejects add hunks targeting an existing file without replacing it", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const target = path.join(tmp.path, "existing.txt") + return Effect.promise(() => fs.writeFile(target, "sentinel\n")).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + Effect.gen(function* () { + expect( + yield* registry.execute(call("*** Begin Patch\n*** Add File: existing.txt\n+replacement\n*** End Patch")), + ).toEqual({ type: "error", value: "Unable to apply patch at existing.txt" }) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("sentinel\n") + }), + ), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("reports earlier sequential applications when a later commit fails", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const first = path.join(tmp.path, "first.txt") + const second = path.join(tmp.path, "second.txt") + failRemoveTarget = path.basename(second) + return Effect.promise(() => Promise.all([fs.writeFile(first, "first"), fs.writeFile(second, "second")])).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + Effect.gen(function* () { + expect( + yield* registry.execute(call("*** Begin Patch\n*** Delete File: first.txt\n*** Delete File: second.txt\n*** End Patch")), + ).toEqual({ type: "error", value: "Patch partially applied before failing at second.txt. Applied: first.txt" }) + expect(yield* exists(first)).toBe(false) + expect(yield* exists(second)).toBe(true) + }), + ), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("finishes the sequential commit phase when interrupted after the first mutation", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const first = path.join(tmp.path, "first.txt") + const second = path.join(tmp.path, "second.txt") + blockRemoveTarget = path.basename(second) + return Effect.gen(function* () { + removeStarted = yield* Deferred.make() + releaseRemove = yield* Deferred.make() + yield* Effect.promise(() => Promise.all([fs.writeFile(first, "first"), fs.writeFile(second, "second")])) + yield* withTool(tmp.path, (registry) => + Effect.gen(function* () { + const run = yield* registry.execute(call("*** Begin Patch\n*** Delete File: first.txt\n*** Delete File: second.txt\n*** End Patch")).pipe(Effect.forkChild) + yield* Deferred.await(removeStarted!) + const interrupt = yield* Fiber.interrupt(run).pipe(Effect.forkChild) + yield* Deferred.succeed(releaseRemove!, undefined) + yield* Fiber.join(interrupt) + expect(yield* exists(first)).toBe(false) + expect(yield* exists(second)).toBe(false) + }), + ) + }) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) +}) diff --git a/packages/core/test/tool-bash.test.ts b/packages/core/test/tool-bash.test.ts new file mode 100644 index 0000000000..950debc82a --- /dev/null +++ b/packages/core/test/tool-bash.test.ts @@ -0,0 +1,406 @@ +import fs from "fs/promises" +import { realpathSync } from "node:fs" +import path from "path" +import { describe, expect, test } from "bun:test" +import { Effect, Layer } from "effect" +import { ChildProcess } from "effect/unstable/process" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Config } from "@opencode-ai/core/config" +import { Location } from "@opencode-ai/core/location" +import { LocationMutation } from "@opencode-ai/core/location-mutation" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { AppProcess } from "@opencode-ai/core/process" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { BashTool } from "@opencode-ai/core/tool/bash" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { location } from "./fixture/location" +import { tmpdir } from "./fixture/tmpdir" +import { testEffect } from "./lib/effect" + +const sessionID = SessionV2.ID.make("ses_bash_tool_test") +const assertions: PermissionV2.AssertInput[] = [] +const runs: Array<{ + readonly command: string + readonly cwd?: string + readonly shell?: string | boolean + readonly options?: AppProcess.RunOptions +}> = [] +const truncations: ToolOutputStore.TruncateInput[] = [] +let denyAction: string | undefined +let result: AppProcess.RunResult = { + command: "mock", + exitCode: 0, + stdout: Buffer.from("hello\n"), + stderr: Buffer.alloc(0), + stdoutTruncated: false, + stderrTruncated: false, +} +let runFailure: AppProcess.AppProcessError | undefined +let truncate = (input: ToolOutputStore.TruncateInput): Effect.Effect => + Effect.succeed({ content: input.content, truncated: false }) + +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => assertions.push(input)).pipe( + Effect.andThen( + input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void, + ), + ), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const appProcess = Layer.succeed( + AppProcess.Service, + AppProcess.Service.of({ + run: (command: ChildProcess.Command, options?: AppProcess.RunOptions) => + Effect.suspend(() => { + if (command._tag !== "StandardCommand") throw new Error("expected standard command") + runs.push({ command: command.command, cwd: command.options.cwd, shell: command.options.shell, options }) + return runFailure ? Effect.fail(runFailure) : Effect.succeed(result) + }), + } as unknown as AppProcess.Interface), +) +const resources = Layer.succeed( + ToolOutputStore.Service, + ToolOutputStore.Service.of({ + limits: () => Effect.die("unused"), + write: () => Effect.die("unused"), + truncate: (input) => Effect.sync(() => truncations.push(input)).pipe(Effect.andThen(truncate(input))), + read: () => Effect.die("unused"), + cleanup: () => Effect.die("unused"), + }), +) +const config = Layer.succeed( + Config.Service, + Config.Service.of({ + entries: () => Effect.succeed([]), + }), +) + +const reset = () => { + assertions.length = 0 + runs.length = 0 + truncations.length = 0 + denyAction = undefined + runFailure = undefined + result = { + command: "mock", + exitCode: 0, + stdout: Buffer.from("hello\n"), + stderr: Buffer.alloc(0), + stdoutTruncated: false, + stderrTruncated: false, + } + truncate = (input) => Effect.succeed({ content: input.content, truncated: false }) +} + +const withTool = ( + directory: string, + body: (registry: ToolRegistry.Interface) => Effect.Effect, + processLayer: Layer.Layer = appProcess, +) => { + const filesystem = FSUtil.defaultLayer + const activeLocation = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make(directory) })), + ) + const mutation = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation)) + const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) + const bash = BashTool.layer.pipe( + Layer.provide(registry), + Layer.provide(permission), + Layer.provide(mutation), + Layer.provide(processLayer), + Layer.provide(resources), + Layer.provide(config), + ) + return Effect.gen(function* () { + return yield* body(yield* ToolRegistry.Service) + }).pipe(Effect.provide(Layer.mergeAll(registry, bash))) +} + +const call = (input: typeof BashTool.Parameters.Type, id = "call-bash") => ({ + sessionID, + call: { type: "tool-call" as const, id, name: "bash", input }, +}) + +const it = testEffect(Layer.empty) + +describe("BashTool", () => { + it.live("registers and returns structured successful output from the active Location", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + return withTool(tmp.path, (registry) => + Effect.gen(function* () { + const definitions = yield* registry.definitions() + expect(definitions.map((tool) => tool.name)).toEqual(["bash"]) + expect(definitions[0]?.inputSchema).not.toHaveProperty("properties.background") + expect(yield* registry.settle(call({ command: "pwd", description: "Print working directory" }))).toEqual({ + result: { type: "text", value: "hello\n\n\nCommand exited with code 0." }, + output: { + structured: { + command: "pwd", + cwd: realpathSync(tmp.path), + exitCode: 0, + output: "hello\n", + truncated: false, + }, + content: [{ type: "text", text: "hello\n\n\nCommand exited with code 0." }], + }, + }) + expect(runs).toMatchObject([{ command: "pwd", cwd: realpathSync(tmp.path) }]) + expect(runs[0]?.options).toMatchObject({ + maxOutputBytes: BashTool.MAX_CAPTURE_BYTES, + maxErrorBytes: BashTool.MAX_CAPTURE_BYTES, + }) + expect(assertions).toEqual([{ sessionID, action: "bash", resources: ["pwd"], save: ["pwd"] }]) + }), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("resolves a relative workdir from the active Location", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + return Effect.promise(() => fs.mkdir(path.join(tmp.path, "src"))).pipe( + Effect.andThen(withTool(tmp.path, (registry) => registry.execute(call({ command: "pwd", workdir: "src" })))), + Effect.andThen( + Effect.sync(() => expect(runs).toMatchObject([{ cwd: realpathSync(path.join(tmp.path, "src")) }])), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + if (process.platform !== "win32") { + it.live("executes a real shell command through AppProcess", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + return withTool( + tmp.path, + (registry) => registry.settle(call({ command: "printf core-bash" })), + AppProcess.defaultLayer, + ).pipe( + Effect.andThen((settled) => + Effect.sync(() => { + expect(settled.result).toEqual({ type: "text", value: "core-bash\n\nCommand exited with code 0." }) + expect(settled.output?.structured).toMatchObject({ + command: "printf core-bash", + cwd: realpathSync(tmp.path), + exitCode: 0, + output: "core-bash", + }) + }), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + } + + it.live("approves an explicit external workdir before bash execution", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + reset() + return withTool(active.path, (registry) => + registry.execute(call({ command: "pwd", workdir: outside.path })), + ).pipe( + Effect.andThen( + Effect.sync(() => { + expect(assertions.map((item) => item.action)).toEqual(["external_directory", "bash"]) + expect(assertions[0]).toMatchObject({ + resources: [path.join(realpathSync(outside.path), "*").replaceAll("\\", "/")], + }) + expect(runs).toHaveLength(1) + }), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + + it.live("does not execute after external-directory or bash denial", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => + Effect.gen(function* () { + reset() + denyAction = "external_directory" + yield* withTool(active.path, (registry) => registry.execute(call({ command: "pwd", workdir: outside.path }))) + expect(assertions.map((item) => item.action)).toEqual(["external_directory"]) + expect(runs).toEqual([]) + + reset() + denyAction = "bash" + yield* withTool(active.path, (registry) => registry.execute(call({ command: "pwd" }))) + expect(assertions.map((item) => item.action)).toEqual(["bash"]) + expect(runs).toEqual([]) + }), + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + + it.live("reports external command arguments as advisory warnings without enforcing approval", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + reset() + denyAction = "external_directory" + const target = path.join(outside.path, "secret.txt") + return withTool(active.path, (registry) => registry.settle(call({ command: `cat ${target}` }))).pipe( + Effect.andThen((settled) => + Effect.sync(() => { + expect(assertions.map((item) => item.action)).toEqual(["bash"]) + expect(runs).toHaveLength(1) + expect(settled.output?.structured).toMatchObject({ + warnings: [ + `Command argument references external directory ${path.join(realpathSync(outside.path), "*").replaceAll("\\", "/")}. Bash runs with host-user filesystem, process, and network authority; this scan is advisory only.`, + ], + }) + expect(settled.result).toMatchObject({ type: "text", value: expect.stringContaining("Warnings:") }) + }), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + + it.live("keeps non-zero exits useful and exposes managed overflow by opaque URI", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + result = { ...result, exitCode: 7, stdout: Buffer.from("HEAD full output TAIL") } + truncate = (input) => + Effect.succeed({ + content: "HEAD\n\n... output truncated; full content available as tool-output://opaque ...\n\nTAIL", + truncated: true, + resource: new ToolOutputStore.Resource({ + uri: "tool-output://opaque", + mime: "text/plain", + size: input.content.length, + }), + }) + return withTool(tmp.path, (registry) => registry.settle(call({ command: "false" }, "call-overflow"))).pipe( + Effect.andThen((settled) => + Effect.sync(() => { + expect(settled.result).toMatchObject({ + type: "text", + value: expect.stringContaining("Command exited with code 7"), + }) + expect(settled.output?.structured).toMatchObject({ + command: "false", + cwd: realpathSync(tmp.path), + exitCode: 7, + truncated: true, + resource: { uri: "tool-output://opaque" }, + }) + expect(truncations).toMatchObject([ + { sessionID, toolCallID: "call-overflow", content: "HEAD full output TAIL" }, + ]) + expect(JSON.stringify(settled)).not.toContain(tmp.path + path.sep + "tool-output") + }), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("surfaces bounded process-capture truncation", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + result = { ...result, stdoutTruncated: true } + return withTool(tmp.path, (registry) => registry.settle(call({ command: "verbose" }))).pipe( + Effect.andThen((settled) => + Effect.sync(() => { + expect(settled.output?.structured).toMatchObject({ truncated: true, stdoutTruncated: true }) + expect(settled.result).toMatchObject({ + type: "text", + value: expect.stringContaining("stdout capture truncated"), + }) + expect(settled.output?.structured).not.toHaveProperty("resource") + }), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("returns a useful timeout settlement", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + runFailure = new AppProcess.AppProcessError({ command: "sleep", cause: new Error("Timed out") }) + return withTool(tmp.path, (registry) => registry.settle(call({ command: "sleep 60", timeout: 10 }))).pipe( + Effect.andThen((settled) => + Effect.sync(() => { + expect(settled.result).toMatchObject({ + type: "text", + value: expect.stringContaining("Command timed out"), + }) + expect(settled.output?.structured).toMatchObject({ + command: "sleep 60", + timedOut: true, + truncated: false, + }) + }), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) +}) + +test("keeps locked deferred parity TODOs visible", async () => { + const source = await fs.readFile(new URL("../src/tool/bash.ts", import.meta.url), "utf8") + for (const todo of [ + "Port tree-sitter bash / PowerShell parser-based approval reduction.", + "Port BashArity reusable command-prefix approvals.", + "Replace token-based command-argument external-directory advisories with parser-based detection.", + "Restore PowerShell and cmd-specific invocation/path handling on Windows.", + "Add plugin shell.env environment augmentation once V2 plugin hooks exist.", + "Add durable/live progress metadata streaming for long-running commands once V2 tool invocation progress context is wired.", + "Persist background job status and define restart recovery before exposing remote observation.", + "Revisit process-group cleanup and platform coverage with shell-specific tests if current AppProcess semantics do not fully cover it.", + "Revisit binary output handling if stdout/stderr decoding is text-only.", + "Stream full shell output into managed storage while retaining only a bounded in-memory preview.", + ]) { + expect(source).toContain(`TODO: ${todo}`) + } +}) diff --git a/packages/core/test/tool-edit.test.ts b/packages/core/test/tool-edit.test.ts new file mode 100644 index 0000000000..558e1f0b10 --- /dev/null +++ b/packages/core/test/tool-edit.test.ts @@ -0,0 +1,453 @@ +import fs from "fs/promises" +import path from "path" +import { fileURLToPath } from "url" +import { describe, expect, test } from "bun:test" +import { Effect, Layer } from "effect" +import { FileMutation } from "@opencode-ai/core/file-mutation" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Location } from "@opencode-ai/core/location" +import { LocationMutation } from "@opencode-ai/core/location-mutation" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { EditTool } from "@opencode-ai/core/tool/edit" +import { location } from "./fixture/location" +import { tmpdir } from "./fixture/tmpdir" +import { testEffect } from "./lib/effect" + +const sessionID = SessionV2.ID.make("ses_edit_tool_test") +const assertions: PermissionV2.AssertInput[] = [] +const writes: string[] = [] +let reads = 0 +let denyAction: string | undefined +let afterAssertion = (_input: PermissionV2.AssertInput): Effect.Effect => Effect.void +let afterRead = (_target: string, _content: Uint8Array): Effect.Effect => Effect.void + +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => assertions.push(input)).pipe( + Effect.andThen( + input.action === denyAction + ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) + : afterAssertion(input), + ), + ), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) + +const reset = () => { + assertions.length = 0 + writes.length = 0 + reads = 0 + denyAction = undefined + afterAssertion = () => Effect.void + afterRead = () => Effect.void +} + +const filesystem = Layer.effect( + FSUtil.Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + return FSUtil.Service.of({ + ...fs, + readFile: (target) => + fs.readFile(target).pipe( + Effect.tap((content) => + Effect.sync(() => reads++).pipe(Effect.andThen(Effect.suspend(() => afterRead(target, content)))), + ), + ), + writeWithDirs: (target, content, mode) => + Effect.sync(() => writes.push(target)).pipe(Effect.andThen(fs.writeWithDirs(target, content, mode))), + }) + }), +).pipe(Layer.provide(FSUtil.defaultLayer)) + +const withTool = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => { + const activeLocation = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make(directory) })), + ) + const planning = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation)) + const commits = FileMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(planning)) + const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) + const edit = EditTool.layer.pipe( + Layer.provide(registry), + Layer.provide(planning), + Layer.provide(commits), + Layer.provide(filesystem), + ) + return Effect.gen(function* () { + return yield* body(yield* ToolRegistry.Service) + }).pipe(Effect.provide(Layer.mergeAll(registry, planning, commits, edit))) +} + +const call = (input: typeof EditTool.Parameters.Type, id = "call-edit") => ({ + sessionID, + call: { type: "tool-call" as const, id, name: "edit", input }, +}) + +const it = testEffect(Layer.empty) + +describe("EditTool", () => { + it.live("registers and replaces relative exact text through FileMutation once", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const target = path.join(tmp.path, "hello.txt") + return Effect.promise(() => fs.writeFile(target, "before\nrest\n")).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + Effect.gen(function* () { + expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["edit"]) + const settled = yield* registry.settle( + call({ path: "hello.txt", oldString: "before", newString: "after" }), + ) + expect(settled.result).toEqual({ + type: "text", + value: "Edited file successfully: hello.txt\nReplacements: 1\n```diff\n-before\n+after\n```", + }) + expect(settled.output?.structured).toEqual({ + operation: "write", + target: yield* Effect.promise(() => fs.realpath(target)), + resource: "hello.txt", + existed: true, + replacements: 1, + }) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after\nrest\n") + expect(assertions).toEqual([{ sessionID, action: "edit", resources: ["hello.txt"], save: ["*"] }]) + expect(writes).toEqual([yield* Effect.promise(() => fs.realpath(target))]) + }), + ), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("accepts an absolute file path inside the active Location", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const target = path.join(tmp.path, "absolute.txt") + return Effect.promise(() => fs.writeFile(target, "before")).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + registry.execute(call({ path: target, oldString: "before", newString: "after" })), + ), + ), + Effect.andThen((result) => + Effect.gen(function* () { + expect(result.type).toBe("text") + expect(assertions.map((input) => input.action)).toEqual(["edit"]) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after") + }), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("approves an explicit external absolute path before edit", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + reset() + const target = path.join(outside.path, "external.txt") + return Effect.promise(() => fs.writeFile(target, "before")).pipe( + Effect.andThen( + withTool(active.path, (registry) => + registry.execute(call({ path: target, oldString: "before", newString: "after" })), + ), + ), + Effect.andThen((result) => + Effect.gen(function* () { + expect(result.type).toBe("text") + expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"]) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after") + expect(writes).toHaveLength(1) + }), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + + it.live("does not write when external_directory or edit approval is denied", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => + Effect.gen(function* () { + const external = path.join(outside.path, "denied.txt") + yield* Effect.promise(() => fs.writeFile(external, "before")) + reset() + denyAction = "external_directory" + expect( + yield* withTool(active.path, (registry) => + registry.execute(call({ path: external, oldString: "before", newString: "after" })), + ), + ).toEqual({ + type: "error", + value: `Unable to edit ${external}`, + }) + expect(assertions.map((input) => input.action)).toEqual(["external_directory"]) + expect(reads).toBe(0) + expect(writes).toEqual([]) + + reset() + denyAction = "edit" + expect( + yield* withTool(active.path, (registry) => + registry.execute(call({ path: external, oldString: "before", newString: "after" })), + ), + ).toEqual({ + type: "error", + value: `Unable to edit ${external}`, + }) + expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"]) + expect(reads).toBe(0) + expect(writes).toEqual([]) + expect(yield* Effect.promise(() => fs.readFile(external, "utf8"))).toBe("before") + }), + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + + it.live("denied edit reads no target content and does not disclose whether oldString matches", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + denyAction = "edit" + const target = path.join(tmp.path, "secret.txt") + return Effect.promise(() => fs.writeFile(target, "secret content")).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + Effect.gen(function* () { + const matching = yield* registry.execute( + call({ path: "secret.txt", oldString: "secret content", newString: "replacement" }), + ) + const missing = yield* registry.execute( + call({ path: "secret.txt", oldString: "not present", newString: "replacement" }), + ) + + expect(matching).toEqual({ type: "error", value: "Unable to edit secret.txt" }) + expect(missing).toEqual(matching) + expect(assertions.map((input) => input.action)).toEqual(["edit", "edit"]) + expect(reads).toBe(0) + expect(writes).toEqual([]) + }), + ), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("rejects no-op, empty, missing, and ambiguous exact replacements", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const target = path.join(tmp.path, "matches.txt") + return Effect.promise(() => fs.writeFile(target, "same same")).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + Effect.gen(function* () { + expect( + yield* registry.execute(call({ path: "matches.txt", oldString: "same", newString: "same" })), + ).toEqual({ + type: "error", + value: "No changes to apply: oldString and newString are identical.", + }) + expect( + yield* registry.execute(call({ path: "matches.txt", oldString: "", newString: "after" })), + ).toEqual({ + type: "error", + value: "oldString must not be empty. Use write to create or overwrite a file.", + }) + expect( + yield* registry.execute(call({ path: "matches.txt", oldString: "missing", newString: "after" })), + ).toEqual({ + type: "error", + value: + "Could not find oldString in the file. It must match exactly, including whitespace and indentation.", + }) + expect( + yield* registry.execute(call({ path: "matches.txt", oldString: "same", newString: "after" })), + ).toEqual({ + type: "error", + value: + "Found multiple exact matches for oldString. Provide more surrounding context or set replaceAll to true.", + }) + expect(writes).toEqual([]) + }), + ), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("replaces every exact occurrence when replaceAll is true", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const target = path.join(tmp.path, "all.txt") + return Effect.promise(() => fs.writeFile(target, "same same same")).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + registry.settle(call({ path: "all.txt", oldString: "same", newString: "after", replaceAll: true })), + ), + ), + Effect.andThen((settled) => + Effect.gen(function* () { + expect(settled.output?.structured).toMatchObject({ replacements: 3 }) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after after after") + expect(writes).toHaveLength(1) + }), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("preserves BOM and CRLF line endings", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const target = path.join(tmp.path, "windows.txt") + return Effect.promise(() => fs.writeFile(target, "\uFEFFbefore\r\nrest\r\n")).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + registry.execute(call({ path: "windows.txt", oldString: "before\nrest", newString: "after\nrest" })), + ), + ), + Effect.andThen(() => Effect.promise(() => fs.readFile(target, "utf8"))), + Effect.tap((content) => Effect.sync(() => expect(content).toBe("\uFEFFafter\r\nrest\r\n"))), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("rejects an in-place content change after matching but before conditional commit", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const target = path.join(tmp.path, "concurrent.txt") + afterRead = () => (reads === 1 ? Effect.promise(() => fs.writeFile(target, "newer\n")) : Effect.void) + return Effect.promise(() => fs.writeFile(target, "before\n")).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + registry.execute(call({ path: "concurrent.txt", oldString: "before", newString: "after" })), + ), + ), + Effect.andThen((result) => + Effect.gen(function* () { + expect(result).toEqual({ type: "error", value: "File changed after permission approval. Read it again before editing." }) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("newer\n") + expect(writes).toEqual([]) + }), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + if (process.platform !== "win32") { + it.live("delegates post-approval revalidation to FileMutation before writing", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + reset() + const parent = path.join(active.path, "parent") + const detached = path.join(active.path, "detached") + afterAssertion = (input) => + input.action === "edit" + ? Effect.promise(async () => { + await fs.rename(parent, detached) + await fs.symlink(outside.path, parent) + }) + : Effect.void + return Effect.promise(async () => { + await fs.mkdir(parent) + await fs.writeFile(path.join(parent, "escape.txt"), "before") + }).pipe( + Effect.andThen( + withTool(active.path, (registry) => + registry.execute(call({ path: "parent/escape.txt", oldString: "before", newString: "after" })), + ), + ), + Effect.andThen((result) => + Effect.gen(function* () { + expect(result).toEqual({ type: "error", value: "Unable to edit parent/escape.txt" }) + expect(assertions.map((input) => input.action)).toEqual(["edit"]) + expect(writes).toEqual([]) + expect( + yield* Effect.promise(() => + fs.stat(path.join(outside.path, "escape.txt")).then( + () => true, + () => false, + ), + ), + ).toBe(false) + }), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + } +}) + +test("keeps the locked edit schema, semantics docstring, and deferred TODOs visible", async () => { + const source = (await fs.readFile(new URL("../src/tool/edit.ts", import.meta.url), "utf8")).replaceAll("\r\n", "\n") + const definition = await Effect.runPromise( + withTool(path.dirname(fileURLToPath(import.meta.url)), (registry) => registry.definitions()), + ) + const schema = definition[0]?.inputSchema as { readonly properties?: Record } + + expect(Object.keys(schema.properties ?? {}).sort()).toEqual(["newString", "oldString", "path", "replaceAll"]) + expect(source).toContain( + "Named project references\n * are read-oriented and deliberately are not accepted by mutation tools.", + ) + for (const todo of [ + "Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review.", + "Add formatter integration after V2 formatter runtime exists.", + "Publish watcher/file-edit events after V2 watcher integration exists.", + "Add snapshots / undo after design exists.", + "Add LSP notification and diagnostics after V2 LSP runtime exists.", + ]) { + expect(source).toContain(`TODO: ${todo}`) + } +}) diff --git a/packages/core/test/tool-glob.test.ts b/packages/core/test/tool-glob.test.ts new file mode 100644 index 0000000000..8fe24b2d6a --- /dev/null +++ b/packages/core/test/tool-glob.test.ts @@ -0,0 +1,210 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { FileSystem } from "@opencode-ai/core/filesystem" +import { LocationSearch } from "@opencode-ai/core/location-search" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { RelativePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { GlobTool } from "@opencode-ai/core/tool/glob" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { testEffect } from "./lib/effect" + +const sessionID = SessionV2.ID.make("ses_glob_tool_test") +const assertions: PermissionV2.AssertInput[] = [] +const resolutions: FileSystem.ListInput[] = [] +const searches: LocationSearch.FilesInput[] = [] +const roots: FileSystem.RootTarget[] = [] +let allow = true +let result = new LocationSearch.FilesResult({ items: [], truncated: false, partial: false }) + +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => assertions.push(input)).pipe( + Effect.andThen(allow ? Effect.void : Effect.fail(new PermissionV2.DeniedError({ rules: [] }))), + ), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) + +const filesystem = Layer.succeed( + FileSystem.Service, + FileSystem.Service.of({ + read: () => Effect.die("unused"), + resolveReadPath: () => Effect.die("unused"), + resolveRead: () => Effect.die("unused"), + readResolved: () => Effect.die("unused"), + readTextPageResolved: () => Effect.die("unused"), + list: () => Effect.die("unused"), + resolveRoot: (input = {}) => + Effect.sync(() => { + resolutions.push(input) + const relative = input.path ?? RelativePath.make(".") + const resource = input.reference === undefined ? relative : `${input.reference}:${relative}` + return new FileSystem.RootTarget({ + absolute: `/project/${relative}`, + real: `/project/${relative}`, + directory: "/project", + root: "/project", + resource, + reference: input.reference, + type: "directory", + dev: 1, + }) + }), + revalidateRoot: Effect.succeed, + resolveList: () => Effect.die("unused"), + listResolved: () => Effect.die("unused"), + listPage: () => Effect.die("unused"), + listPageResolved: () => Effect.die("unused"), + find: () => Effect.die("unused"), + grep: () => Effect.die("unused"), + isIgnored: () => false, + }), +) + +const search = Layer.succeed( + LocationSearch.Service, + LocationSearch.Service.of({ + files: (input, root) => + Effect.sync(() => { + searches.push(input) + if (root) roots.push(root) + return result + }), + grep: () => Effect.die("unused"), + }), +) + +const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) +const glob = GlobTool.layer.pipe( + Layer.provide(registry), + Layer.provide(permission), + Layer.provide(filesystem), + Layer.provide(search), +) +const it = testEffect(Layer.mergeAll(registry, permission, filesystem, search, glob)) + +const reset = () => { + assertions.length = 0 + resolutions.length = 0 + searches.length = 0 + roots.length = 0 + allow = true + result = new LocationSearch.FilesResult({ items: [], truncated: false, partial: false }) +} + +const call = (input: typeof GlobTool.Parameters.Type, id = "call-glob") => ({ + sessionID, + call: { type: "tool-call" as const, id, name: "glob", input }, +}) + +describe("GlobTool", () => { + it.effect("registers the glob definition", () => + Effect.gen(function* () { + reset() + expect((yield* (yield* ToolRegistry.Service).definitions()).map((tool) => tool.name)).toEqual(["glob"]) + }), + ) + + it.effect("authorizes the active Location pattern and delegates traversal only to LocationSearch.files", () => + Effect.gen(function* () { + reset() + const registry = yield* ToolRegistry.Service + + expect(yield* registry.execute(call({ pattern: "**/*.ts", path: RelativePath.make("src"), limit: 12 }))).toEqual({ + type: "text", + value: "No files found", + }) + expect(assertions).toEqual([ + { + sessionID, + action: "glob", + resources: ["**/*.ts"], + save: ["*"], + metadata: { root: "src", reference: undefined, path: "src", limit: 12 }, + }, + ]) + expect(resolutions).toEqual([{ path: RelativePath.make("src"), reference: undefined }]) + expect(searches).toEqual([{ pattern: "**/*.ts", path: RelativePath.make("src"), limit: 12 }]) + expect(roots).toMatchObject([{ resource: "src" }]) + }), + ) + + it.effect("prevents Location search when permission is denied", () => + Effect.gen(function* () { + reset() + allow = false + + expect(yield* (yield* ToolRegistry.Service).execute(call({ pattern: "*.secret" }))).toEqual({ + type: "error", + value: "Unable to find files matching *.secret", + }) + expect(searches).toEqual([]) + }), + ) + + it.effect("returns active Location glob resources", () => + Effect.gen(function* () { + reset() + result = new LocationSearch.FilesResult({ + items: [new LocationSearch.File({ path: RelativePath.make("src/index.ts"), canonical: "/project/src/index.ts", resource: "src/index.ts", mtime: 1 })], + truncated: false, + partial: false, + }) + + expect(yield* (yield* ToolRegistry.Service).settle(call({ pattern: "*.ts" }))).toEqual({ + result: { type: "text", value: "src/index.ts" }, + output: { + structured: result, + content: [{ type: "text", text: "src/index.ts" }], + }, + }) + }), + ) + + it.effect("searches named references with root and reference metadata", () => + Effect.gen(function* () { + reset() + result = new LocationSearch.FilesResult({ + items: [new LocationSearch.File({ path: RelativePath.make("guide.md"), canonical: "/project/docs/guide.md", resource: "docs:guide.md", mtime: 1 })], + truncated: false, + partial: false, + }) + + expect(yield* (yield* ToolRegistry.Service).execute(call({ pattern: "*.md", reference: "docs" }))).toEqual({ + type: "text", + value: "docs:guide.md", + }) + expect(assertions).toEqual([ + { + sessionID, + action: "glob", + resources: ["*.md"], + save: ["*"], + metadata: { root: "docs:.", reference: "docs", path: undefined, limit: undefined }, + }, + ]) + expect(searches).toEqual([{ pattern: "*.md", reference: "docs" }]) + }), + ) + + it.effect("formats bounded and partial results without discarding structured output", () => + Effect.sync(() => { + const output = new LocationSearch.FilesResult({ + items: [new LocationSearch.File({ path: RelativePath.make("one.ts"), canonical: "/project/one.ts", resource: "one.ts", mtime: 1 })], + truncated: true, + partial: true, + }) + + expect(GlobTool.toModelOutput(output)).toBe( + "one.ts\n\n(Results are truncated: showing first 1 results. Consider using a more specific path or pattern.)\n\n(Results may be incomplete because some discovered files could not be read.)", + ) + }), + ) +}) diff --git a/packages/core/test/tool-grep.test.ts b/packages/core/test/tool-grep.test.ts new file mode 100644 index 0000000000..d4b72acf57 --- /dev/null +++ b/packages/core/test/tool-grep.test.ts @@ -0,0 +1,268 @@ +import fs from "fs/promises" +import path from "path" +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Location } from "@opencode-ai/core/location" +import { FileSystem } from "@opencode-ai/core/filesystem" +import { Ripgrep as FileSystemRipgrep } from "@opencode-ai/core/filesystem/ripgrep" +import { LocationSearch } from "@opencode-ai/core/location-search" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { AppProcess } from "@opencode-ai/core/process" +import { ProjectReference } from "@opencode-ai/core/project-reference" +import { Ripgrep } from "@opencode-ai/core/ripgrep" +import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { GrepTool } from "@opencode-ai/core/tool/grep" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { location } from "./fixture/location" +import { tmpdir } from "./fixture/tmpdir" +import { it as runtimeIt } from "./lib/effect" +import { testEffect } from "./lib/effect" + +const assertions: PermissionV2.AssertInput[] = [] +const searches: LocationSearch.GrepInput[] = [] +const roots: FileSystem.RootTarget[] = [] +let allow = true +let result = new LocationSearch.GrepResult({ items: [], truncated: false, partial: false }) +let searchFailure: Ripgrep.InvalidPatternError | undefined + +const filesystem = Layer.succeed( + FileSystem.Service, + FileSystem.Service.of({ + read: () => Effect.die("unused"), + resolveReadPath: () => Effect.die("unused"), + resolveRead: () => Effect.die("unused"), + readResolved: () => Effect.die("unused"), + readTextPageResolved: () => Effect.die("unused"), + list: () => Effect.die("unused"), + resolveRoot: (input = {}) => + Effect.succeed( + new FileSystem.RootTarget({ + absolute: `/project/${input.path ?? "."}`, + real: `/project/${input.path ?? "."}`, + directory: "/project", + root: "/project", + resource: input.reference === undefined ? input.path ?? "." : `${input.reference}:${input.path ?? "."}`, + reference: input.reference, + type: "directory", + dev: 1, + }), + ), + revalidateRoot: Effect.succeed, + resolveList: () => Effect.die("unused"), + listResolved: () => Effect.die("unused"), + listPage: () => Effect.die("unused"), + listPageResolved: () => Effect.die("unused"), + find: () => Effect.die("unused"), + grep: () => Effect.die("unused"), + isIgnored: () => false, + }), +) +const search = Layer.succeed( + LocationSearch.Service, + LocationSearch.Service.of({ + files: () => Effect.die("unused"), + grep: (input, root) => + Effect.sync(() => { + searches.push(input) + if (root) roots.push(root) + if (searchFailure) throw searchFailure + return result + }), + }), +) +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => { + assertions.push(input) + }).pipe(Effect.andThen(allow ? Effect.void : Effect.fail(new PermissionV2.DeniedError({ rules: [] })))), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) +const grep = GrepTool.layer.pipe(Layer.provide(registry), Layer.provide(filesystem), Layer.provide(search), Layer.provide(permission)) +const it = testEffect(Layer.mergeAll(registry, filesystem, search, permission, grep)) +const sessionID = SessionV2.ID.make("ses_grep_tool_test") + +const execute = (input: Record) => + ToolRegistry.Service.use((registry) => + registry.execute({ sessionID, call: { type: "tool-call", id: "call-grep", name: "grep", input } }), + ) + +const settle = (input: Record) => + ToolRegistry.Service.use((registry) => + registry.settle({ sessionID, call: { type: "tool-call", id: "call-grep", name: "grep", input } }), + ) + +const reset = () => { + assertions.length = 0 + searches.length = 0 + roots.length = 0 + allow = true + searchFailure = undefined + result = new LocationSearch.GrepResult({ items: [], truncated: false, partial: false }) +} + +function references(entries: Record) { + return ProjectReference.Service.of({ + list: () => Effect.succeed(Object.values(entries)), + get: (name) => Effect.succeed(entries[name]), + resolveMention: () => Effect.succeed(undefined), + ensurePath: () => Effect.void, + containsManagedPath: () => Effect.succeed(false), + }) +} + +function provideLive(directory: string, projectReferences = references({})) { + const dependencies = Layer.mergeAll( + FSUtil.defaultLayer, + FileSystemRipgrep.defaultLayer, + AppProcess.defaultLayer, + Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))), + Layer.succeed(ProjectReference.Service, projectReferences), + ) + const filesystem = FileSystem.layer.pipe(Layer.provide(dependencies)) + const search = LocationSearch.layer.pipe( + Layer.provide(filesystem), + Layer.provide(Ripgrep.layer.pipe(Layer.provide(dependencies))), + Layer.provide(FSUtil.defaultLayer), + Layer.provide(dependencies), + ) + const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) + const grep = GrepTool.layer.pipe(Layer.provide(registry), Layer.provide(filesystem), Layer.provide(search), Layer.provide(permission)) + return Layer.mergeAll(registry, filesystem, search, permission, grep) +} + +describe("GrepTool", () => { + it.effect("registers the grep contribution", () => + Effect.gen(function* () { + reset() + expect(yield* (yield* ToolRegistry.Service).definitions()).toMatchObject([{ name: "grep" }]) + }), + ) + + it.effect("authorizes the regex resource and delegates an active Location grep", () => + Effect.gen(function* () { + reset() + const input = { pattern: "needle", path: "src", include: "*.ts", limit: 2 } + + expect(yield* execute(input)).toEqual({ type: "text", value: "No files found" }) + expect(assertions).toEqual([ + { + sessionID, + action: "grep", + resources: ["needle"], + save: ["*"], + metadata: { root: "src", reference: undefined, path: RelativePath.make("src"), include: "*.ts", limit: 2 }, + }, + ]) + expect(searches).toEqual([{ pattern: "needle", path: RelativePath.make("src"), include: "*.ts", limit: 2 }]) + expect(roots).toMatchObject([{ resource: "src" }]) + }), + ) + + it.effect("delegates named reference grep and exposes the canonical selected root in metadata", () => + Effect.gen(function* () { + reset() + + yield* execute({ pattern: "guide", path: "docs", reference: "manual", include: "*.md" }) + + expect(assertions[0]).toMatchObject({ + resources: ["guide"], + metadata: { root: "manual:docs", reference: "manual", path: RelativePath.make("docs"), include: "*.md" }, + }) + expect(searches).toEqual([{ pattern: "guide", path: RelativePath.make("docs"), reference: "manual", include: "*.md" }]) + }), + ) + + it.effect("does not search when permission is denied", () => + Effect.gen(function* () { + reset() + allow = false + + expect(yield* execute({ pattern: "secret" })).toEqual({ type: "error", value: "Unable to grep for secret" }) + expect(assertions).toHaveLength(1) + expect(searches).toEqual([]) + }), + ) + + it.effect("keeps structured results raw while formatting bounded partial previews for models", () => + Effect.gen(function* () { + reset() + result = new LocationSearch.GrepResult({ + items: [ + new LocationSearch.Match({ + path: RelativePath.make("src/index.ts"), + canonical: "/project/src/index.ts", + resource: "src/index.ts", + lines: "needle preview", + linePreviewTruncated: true, + line: 3, + offset: 8, + submatches: [new LocationSearch.Submatch({ text: "needle", start: 0, end: 6 })], + mtime: 1, + }), + ], + truncated: true, + partial: true, + }) + + const settlement = yield* settle({ pattern: "needle" }) + expect(settlement.output?.structured).toEqual(result) + expect(settlement.result).toEqual({ + type: "text", + value: "Found 1 matches\nsrc/index.ts:\n Line 3: needle preview...\n\n(Results are truncated: showing first 1 matches. Consider using a more specific path or pattern.)\n\n(Some paths were inaccessible and skipped)", + }) + }), + ) + + it.effect("returns a useful tool error for an invalid regex", () => + Effect.gen(function* () { + reset() + searchFailure = new Ripgrep.InvalidPatternError({ pattern: "[", message: "regex parse error: unclosed character class" }) + + expect(yield* execute({ pattern: "[" })).toEqual({ + type: "error", + value: 'Invalid grep pattern "[": regex parse error: unclosed character class', + }) + expect(searches).toEqual([{ pattern: "[" }]) + }), + ) + + runtimeIt.live("greps active Location and named-reference files with include globs", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => { + const docs = path.join(tmp.path, "docs") + return Effect.gen(function* () { + reset() + yield* Effect.promise(async () => { + await fs.mkdir(path.join(tmp.path, "src")) + await fs.mkdir(docs) + await fs.writeFile(path.join(tmp.path, "src", "index.ts"), "needle ts\n") + await fs.writeFile(path.join(tmp.path, "src", "notes.txt"), "needle txt\n") + await fs.writeFile(path.join(docs, "guide.md"), "needle docs\n") + }) + + expect(yield* execute({ pattern: "needle", path: "src", include: "*.ts" })).toEqual({ + type: "text", + value: "Found 1 matches\nsrc/index.ts:\n Line 1: needle ts\n", + }) + expect(yield* execute({ pattern: "needle", reference: "docs", include: "*.md" })).toEqual({ + type: "text", + value: "Found 1 matches\ndocs:guide.md:\n Line 1: needle docs\n", + }) + }).pipe(Effect.provide(provideLive(tmp.path, references({ docs: { name: "docs", kind: "local", path: docs } })))) + }), + ), + ) +}) diff --git a/packages/core/test/tool-output-store.test.ts b/packages/core/test/tool-output-store.test.ts new file mode 100644 index 0000000000..3aa4ad9bc4 --- /dev/null +++ b/packages/core/test/tool-output-store.test.ts @@ -0,0 +1,245 @@ +import { describe, expect } from "bun:test" +import path from "path" +import { Effect, Layer } from "effect" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Global } from "@opencode-ai/core/global" +import { Config } from "@opencode-ai/core/config" +import { ConfigToolOutput } from "@opencode-ai/core/config/tool-output" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { testEffect } from "./lib/effect" +import { tmpdir } from "./fixture/tmpdir" + +const sessionID = SessionV2.ID.make("ses_tool_output_store") +const otherSessionID = SessionV2.ID.make("ses_tool_output_store_other") + +const withStore = ( + body: (input: { root: string; store: ToolOutputStore.Interface; fs: FSUtil.Interface }) => Effect.Effect, + config?: Config.Info, +) => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + const global = Global.layerWith({ data: tmp.path }) + const configured = config + ? Layer.succeed( + Config.Service, + Config.Service.of({ + entries: () => Effect.succeed([new Config.Document({ type: "document", info: config })]), + }), + ) + : Layer.empty + const store = ToolOutputStore.layer.pipe( + Layer.provide(FSUtil.defaultLayer), + Layer.provide(global), + Layer.provide(configured), + ) + return Effect.gen(function* () { + return yield* body({ root: tmp.path, store: yield* ToolOutputStore.Service, fs: yield* FSUtil.Service }) + }).pipe(Effect.provide(Layer.mergeAll(store, FSUtil.defaultLayer))) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + +const it = testEffect(Layer.empty) + +describe("ToolOutputStore", () => { + it.live("returns under-limit text unchanged without writing a resource", () => + withStore(({ store }) => + Effect.gen(function* () { + expect( + yield* store.truncate({ sessionID, toolCallID: "call-short", content: "line one\nline two" }), + ).toEqual({ content: "line one\nline two", truncated: false }) + }), + ), + ) + + it.live("stores byte-truncated output and returns an opaque head-tail preview", () => + withStore(({ store }) => + Effect.gen(function* () { + const content = "HEAD-" + "x".repeat(100) + "-TAIL" + const result = yield* store.truncate({ sessionID, toolCallID: "call-bytes", content, maxBytes: 20 }) + + expect(result.truncated).toBe(true) + if (!result.truncated) throw new Error("expected truncation") + expect(result.content).toContain("HEAD-") + expect(result.content).toContain("-TAIL") + expect(result.content).toContain("output truncated") + expect(result.resource.uri).toMatch(/^tool-output:\/\/[0-9A-Za-z]+$/) + expect(result.resource.uri.slice("tool-output://".length)).not.toContain("/") + expect(result.resource.uri).not.toContain("\\") + expect(result.resource).toMatchObject({ mime: "text/plain", size: Buffer.byteLength(content) }) + expect((yield* store.read({ sessionID, uri: result.resource.uri })).content).toBe(content) + }), + ), + ) + + it.live("stores line-truncated output and keeps both ends in the preview", () => + withStore(({ store }) => + Effect.gen(function* () { + const content = Array.from({ length: 10 }, (_, index) => `line-${index}`).join("\n") + const result = yield* store.truncate({ sessionID, toolCallID: "call-lines", content, maxLines: 4 }) + + expect(result.truncated).toBe(true) + if (!result.truncated) throw new Error("expected truncation") + expect(result.content).toContain("line-0\nline-1") + expect(result.content).toContain("line-8\nline-9") + expect(result.content).not.toContain("line-4") + }), + ), + ) + + it.live("keeps one-line previews bounded", () => + withStore(({ store }) => + Effect.gen(function* () { + const result = yield* store.truncate({ sessionID, toolCallID: "call-one-line", content: "one\ntwo\nthree", maxLines: 1 }) + + expect(result.truncated).toBe(true) + if (!result.truncated) throw new Error("expected truncation") + const preview = result.content.split("\n\n... output truncated")[0] + expect(preview).toBe("one") + }), + ), + ) + + it.live("pages reads within the bounded managed-resource limit", () => + withStore(({ root, store, fs }) => + Effect.gen(function* () { + const resource = yield* store.write({ sessionID, toolCallID: "call-page", content: "0123456789", name: "out.txt" }) + const first = yield* store.read({ sessionID, uri: resource.uri, limit: 4 }) + const second = yield* store.read({ sessionID, uri: resource.uri, offset: first.next, limit: 4 }) + const last = yield* store.read({ sessionID, uri: resource.uri, offset: second.next, limit: 4 }) + + expect(first).toMatchObject({ content: "0123", offset: 0, truncated: true, next: 4 }) + expect(second).toMatchObject({ content: "4567", offset: 4, truncated: true, next: 8 }) + expect(last).toMatchObject({ content: "89", offset: 8, truncated: false }) + expect(last.resource).toEqual({ uri: resource.uri, mime: "text/plain", name: "out.txt", size: 10 }) + expect(JSON.parse(yield* fs.readFileString(path.join(root, "tool-output", "managed", `${resource.uri.slice("tool-output://".length)}.json`)))).toMatchObject({ + sessionID, + toolCallID: "call-page", + }) + + const bounded = yield* store.read({ + sessionID, + uri: (yield* store.write({ + sessionID, + toolCallID: "call-bounded", + content: "x".repeat(ToolOutputStore.MAX_READ_BYTES + 10), + })).uri, + limit: ToolOutputStore.MAX_READ_BYTES + 10, + }) + expect(Buffer.byteLength(bounded.content)).toBe(ToolOutputStore.MAX_READ_BYTES) + expect(bounded).toMatchObject({ truncated: true, next: ToolOutputStore.MAX_READ_BYTES }) + }), + ), + ) + + it.live("allows the owning session and denies cross-session reads", () => + withStore(({ store }) => + Effect.gen(function* () { + const resource = yield* store.write({ sessionID, toolCallID: "call-owned", content: "owned" }) + expect((yield* store.read({ sessionID, uri: resource.uri })).content).toBe("owned") + expect(yield* Effect.flip(store.read({ sessionID: otherSessionID, uri: resource.uri }))).toBeInstanceOf( + ToolOutputStore.AccessDeniedError, + ) + }), + ), + ) + + it.live("rejects resources whose payload size no longer matches metadata", () => + withStore(({ root, store, fs }) => + Effect.gen(function* () { + const resource = yield* store.write({ sessionID, toolCallID: "call-modified", content: "original" }) + const id = resource.uri.slice("tool-output://".length) + yield* fs.writeFileString(path.join(root, "tool-output", "managed", `${id}.txt`), "changed payload") + + expect(yield* Effect.flip(store.read({ sessionID, uri: resource.uri }))).toBeInstanceOf( + ToolOutputStore.ResourceNotFoundError, + ) + }), + ), + ) + + it.live("honors configured truncation limits", () => + withStore( + ({ store }) => + Effect.gen(function* () { + expect(yield* store.limits()).toEqual({ maxLines: 2, maxBytes: 1_000 }) + expect((yield* store.truncate({ sessionID, toolCallID: "call-config", content: "one\ntwo\nthree" })).truncated).toBe( + true, + ) + }), + new Config.Info({ tool_output: new ConfigToolOutput.Info({ max_lines: 2, max_bytes: 1_000 }) }), + ), + ) + + it.live("cleans old managed resources while preserving recent and unrelated files", () => + withStore(({ root, store, fs }) => + Effect.gen(function* () { + const old = yield* store.write({ sessionID, toolCallID: "call-old", content: "old" }) + const recent = yield* store.write({ sessionID, toolCallID: "call-recent", content: "recent" }) + const directory = path.join(root, "tool-output", "managed") + const oldID = old.uri.slice("tool-output://".length) + const recentID = recent.uri.slice("tool-output://".length) + const oldMetadata = path.join(directory, `${oldID}.json`) + const unrelated = path.join(root, "tool-output", "unrelated.txt") + const unrelatedManaged = path.join(directory, "unrelated.txt") + const record = JSON.parse(yield* fs.readFileString(oldMetadata)) + + yield* fs.writeFileString(oldMetadata, JSON.stringify({ ...record, created: Date.now() - 8 * 24 * 60 * 60 * 1_000 })) + yield* fs.writeFileString(unrelated, "keep") + yield* fs.writeFileString(unrelatedManaged, "keep") + yield* store.cleanup() + + expect(yield* fs.exists(path.join(directory, `${oldID}.txt`))).toBe(false) + expect(yield* fs.exists(oldMetadata)).toBe(false) + expect(yield* fs.exists(path.join(directory, `${recentID}.txt`))).toBe(true) + expect(yield* fs.exists(unrelated)).toBe(true) + expect(yield* fs.exists(unrelatedManaged)).toBe(true) + }), + ), + ) + + it.live("cleans stale generated orphan payloads and malformed pairs", () => + withStore(({ root, store, fs }) => + Effect.gen(function* () { + const directory = path.join(root, "tool-output", "managed") + yield* fs.ensureDir(directory) + const orphanID = "00000000000000000000000000" + const malformedID = "00000000000000000000000001" + const orphan = path.join(directory, `${orphanID}.txt`) + const malformedPayload = path.join(directory, `${malformedID}.txt`) + const malformedMetadata = path.join(directory, `${malformedID}.json`) + yield* fs.writeFileString(orphan, "orphan") + yield* fs.writeFileString(malformedPayload, "malformed") + yield* fs.writeFileString(malformedMetadata, "not json") + const old = new Date(Date.now() - 8 * 24 * 60 * 60 * 1_000) + yield* Effect.all([fs.utimes(orphan, old, old), fs.utimes(malformedPayload, old, old)]) + + yield* store.cleanup() + + expect(yield* fs.exists(orphan)).toBe(false) + expect(yield* fs.exists(malformedPayload)).toBe(false) + expect(yield* fs.exists(malformedMetadata)).toBe(false) + }), + ), + ) + + it.live("cleans managed resources whose payload size no longer matches metadata", () => + withStore(({ root, store, fs }) => + Effect.gen(function* () { + const resource = yield* store.write({ sessionID, toolCallID: "call-modified", content: "original" }) + const directory = path.join(root, "tool-output", "managed") + const id = resource.uri.slice("tool-output://".length) + const payload = path.join(directory, `${id}.txt`) + const metadata = path.join(directory, `${id}.json`) + yield* fs.writeFileString(payload, "changed payload") + + yield* store.cleanup() + + expect(yield* fs.exists(payload)).toBe(false) + expect(yield* fs.exists(metadata)).toBe(false) + }), + ), + ) +}) diff --git a/packages/core/test/tool-question.test.ts b/packages/core/test/tool-question.test.ts new file mode 100644 index 0000000000..5a01a29a87 --- /dev/null +++ b/packages/core/test/tool-question.test.ts @@ -0,0 +1,119 @@ +import { describe, expect } from "bun:test" +import { Effect, Exit, Fiber, Layer } from "effect" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { QuestionV2 } from "@opencode-ai/core/question" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { QuestionTool } from "@opencode-ai/core/tool/question" +import { testEffect } from "./lib/effect" + +const sessionID = SessionV2.ID.make("ses_question_tool_test") +const assertions: PermissionV2.AssertInput[] = [] +let captured: QuestionV2.AskInput | undefined +let reject = false +const capturedInput = () => captured +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => Effect.sync(() => assertions.push(input)), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) +const question = Layer.succeed( + QuestionV2.Service, + QuestionV2.Service.of({ + ask: (input: QuestionV2.AskInput) => + Effect.sync(() => { + captured = input + }).pipe(Effect.andThen(reject ? Effect.fail(new QuestionV2.RejectedError()) : Effect.succeed([["Build"], []]))), + reply: () => Effect.die("unused"), + reject: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const tool = QuestionTool.layer.pipe(Layer.provide(registry), Layer.provide(question)) +const it = testEffect(Layer.mergeAll(permission, registry, question, tool)) + +describe("QuestionTool", () => { + it.effect("registers question and projects user answers without a permission assertion", () => + Effect.gen(function* () { + assertions.length = 0 + captured = undefined + reject = false + const registry = yield* ToolRegistry.Service + const questions = [ + { + question: "What should happen?", + header: "Action", + options: [{ label: "Build", description: "Build it" }], + }, + { + question: "Which environment?", + header: "Environment", + options: [{ label: "Dev", description: "Development" }], + }, + ] + + expect((yield* registry.definitions()).map((definition) => definition.name)).toEqual(["question"]) + expect( + yield* registry.settle({ + sessionID, + call: { type: "tool-call", id: "call-question", name: "question", input: { questions } }, + }), + ).toEqual({ + result: { + type: "text", + value: + 'User has answered your questions: "What should happen?"="Build", "Which environment?"="Unanswered". You can now continue with the user\'s answers in mind.', + }, + output: { + structured: { answers: [["Build"], []] }, + content: [ + { + type: "text", + text: 'User has answered your questions: "What should happen?"="Build", "Which environment?"="Unanswered". You can now continue with the user\'s answers in mind.', + }, + ], + }, + }) + expect(assertions).toEqual([]) + expect(capturedInput()).toEqual({ sessionID, questions, tool: undefined }) + }), + ) + + it.effect("does not invent tool ownership metadata without a durable registry source", () => + Effect.gen(function* () { + captured = undefined + reject = false + const registryService = yield* ToolRegistry.Service + + yield* registryService.execute({ + sessionID, + call: { type: "tool-call", id: "call-question", name: "question", input: { questions: [] } }, + }) + expect(capturedInput()).toEqual({ sessionID, questions: [], tool: undefined }) + }), + ) + + it.effect("keeps dismissed questions out of model-facing output", () => + Effect.gen(function* () { + captured = undefined + reject = true + const registryService = yield* ToolRegistry.Service + const fiber = yield* registryService + .execute({ + sessionID, + call: { type: "tool-call", id: "call-question", name: "question", input: { questions: [] } }, + }) + .pipe(Effect.forkScoped) + + const exit = yield* Fiber.await(fiber) + expect(Exit.isFailure(exit)).toBe(true) + }), + ) +}) diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts new file mode 100644 index 0000000000..4b8ca69a53 --- /dev/null +++ b/packages/core/test/tool-read.test.ts @@ -0,0 +1,397 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { FileSystem } from "@opencode-ai/core/filesystem" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { ReadTool } from "@opencode-ai/core/tool/read" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { RelativePath } from "@opencode-ai/core/schema" +import { testEffect } from "./lib/effect" + +const assertions: PermissionV2.AssertInput[] = [] +const reads: FileSystem.ReadInput[] = [] +const textPageInputs: FileSystem.TextPageInput[] = [] +const pages: FileSystem.ListTarget[] = [] +const pageInputs: Pick[] = [] +let resolvedInput: FileSystem.ReadInput | undefined +let resolveFailure: unknown +let listResolveFailure: unknown = new Error("not a directory") +let listReal = "/project/src" +let size = 5 +let real = "/project/README.md" +let afterApproval = () => {} +const resourceReads: ToolOutputStore.ReadInput[] = [] +const filesystem = Layer.succeed( + FileSystem.Service, + FileSystem.Service.of({ + read: () => Effect.die("unused"), + resolveReadPath: (input) => + resolveFailure === undefined + ? Effect.succeed({ + type: "file" as const, + target: new FileSystem.ReadTarget({ + real, + resource: input.reference === undefined ? "README.md" : `${input.reference}:README.md`, + size, + dev: 1, + }), + }) + : listResolveFailure === undefined + ? Effect.succeed({ + type: "directory" as const, + target: new FileSystem.ListTarget({ + absolute: `/project/${input.path ?? "."}`, + real: listReal, + directory: "/project", + root: "/project", + resource: input.path ?? ".", + }), + }) + : Effect.die(resolveFailure), + resolveRead: (input) => + Effect.sync(() => { + resolvedInput = input + }).pipe( + Effect.andThen( + resolveFailure === undefined + ? Effect.succeed( + new FileSystem.ReadTarget({ + real, + resource: input.reference === undefined ? "README.md" : `${input.reference}:README.md`, + size, + dev: 1, + }), + ) + : Effect.die(resolveFailure), + ), + ), + readResolved: () => + Effect.sync(() => { + reads.push({ path: RelativePath.make("README.md") }) + return new FileSystem.TextContent({ type: "text", content: "hello", mime: "text/plain" }) + }), + readTextPageResolved: (_target, page = {}) => + Effect.sync(() => { + textPageInputs.push(page) + return new FileSystem.TextPage({ + type: "text-page", + content: "hello", + mime: "text/plain", + offset: page.offset ?? 1, + truncated: true, + next: (page.offset ?? 1) + 1, + }) + }), + resolveRoot: () => Effect.die("unused"), + revalidateRoot: Effect.succeed, + list: () => Effect.die("unused"), + resolveList: (input = {}) => + listResolveFailure === undefined + ? Effect.succeed( + new FileSystem.ListTarget({ + absolute: `/project/${input.path ?? "."}`, + real: listReal, + directory: "/project", + root: "/project", + resource: input.path ?? ".", + }), + ) + : Effect.die(listResolveFailure), + listResolved: () => Effect.die("unused"), + listPage: () => Effect.die("unused"), + listPageResolved: (target, page = {}) => + Effect.sync(() => { + pages.push(target) + pageInputs.push(page) + return new FileSystem.ListPage({ entries: [], truncated: false }) + }), + find: () => Effect.die("unused"), + grep: () => Effect.die("unused"), + isIgnored: () => false, + }), +) +let allow = true +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => { + assertions.push(input) + if (allow) afterApproval() + }).pipe(Effect.andThen(allow ? Effect.void : Effect.fail(new PermissionV2.DeniedError({ rules: [] })))), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) +const resources = Layer.succeed( + ToolOutputStore.Service, + ToolOutputStore.Service.of({ + limits: () => Effect.die("unused"), + write: () => Effect.die("unused"), + truncate: () => Effect.die("unused"), + cleanup: () => Effect.die("unused"), + read: (input) => + Effect.sync(() => { + resourceReads.push(input) + return new ToolOutputStore.Page({ + resource: new ToolOutputStore.Resource({ uri: input.uri, mime: "text/plain", size: 5 }), + content: "hello", + offset: input.offset ?? 0, + truncated: false, + }) + }), + }), +) +const read = ReadTool.layer.pipe( + Layer.provide(registry), + Layer.provide(filesystem), + Layer.provide(permission), + Layer.provide(resources), +) +const it = testEffect(Layer.mergeAll(registry, filesystem, permission, resources, read)) +const sessionID = SessionV2.ID.make("ses_read_tool_test") + +describe("ReadTool", () => { + it.effect("registers, authorizes, and reads through the location filesystem", () => + Effect.gen(function* () { + assertions.length = 0 + reads.length = 0 + allow = true + resolveFailure = undefined + listResolveFailure = new Error("not a directory") + size = 5 + real = "/project/README.md" + afterApproval = () => {} + resolvedInput = undefined + const registry = yield* ToolRegistry.Service + + expect(yield* registry.definitions()).toMatchObject([{ name: "read" }]) + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md" } }, + }), + ).toEqual({ type: "json", value: { type: "text", content: "hello", mime: "text/plain" } }) + expect(assertions).toMatchObject([{ sessionID, action: "read", resources: ["README.md"], save: ["*"] }]) + expect(reads).toEqual([{ path: RelativePath.make("README.md") }]) + }), + ) + + it.effect("does not read when permission is denied", () => + Effect.gen(function* () { + assertions.length = 0 + reads.length = 0 + allow = false + resolveFailure = undefined + listResolveFailure = new Error("not a directory") + size = 5 + real = "/project/README.md" + afterApproval = () => {} + resolvedInput = undefined + const registry = yield* ToolRegistry.Service + + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md" } }, + }), + ).toEqual({ type: "error", value: "Unable to read README.md" }) + expect(reads).toEqual([]) + }), + ) + + it.effect("reads an opaque managed resource without treating it as a path", () => + Effect.gen(function* () { + resourceReads.length = 0 + assertions.length = 0 + const registry = yield* ToolRegistry.Service + + expect( + yield* registry.execute({ + sessionID, + call: { + type: "tool-call", + id: "call-read-resource", + name: "read", + input: { resource: "tool-output://opaque", offset: 2, limit: 10 }, + }, + }), + ).toEqual({ + type: "json", + value: { + resource: { uri: "tool-output://opaque", mime: "text/plain", size: 5 }, + content: "hello", + offset: 2, + truncated: false, + }, + }) + expect(resourceReads).toEqual([{ sessionID, uri: "tool-output://opaque", offset: 2, limit: 10 }]) + expect(assertions).toEqual([]) + }), + ) + + it.effect("lists a bounded directory page through read", () => + Effect.gen(function* () { + assertions.length = 0 + pages.length = 0 + pageInputs.length = 0 + allow = true + resolveFailure = new Error("Path is not a file") + listResolveFailure = undefined + listReal = "/project/src" + afterApproval = () => {} + const registry = yield* ToolRegistry.Service + + expect( + yield* registry.execute({ + sessionID, + call: { + type: "tool-call", + id: "call-read-directory", + name: "read", + input: { path: "src", offset: 2, limit: 10 }, + }, + }), + ).toEqual({ type: "json", value: { entries: [], truncated: false } }) + expect(assertions).toMatchObject([{ sessionID, action: "read", resources: ["src"], save: ["*"] }]) + expect(pageInputs).toEqual([{ offset: 2, limit: 10 }]) + }), + ) + + it.effect("does not list a directory when permission is denied", () => + Effect.gen(function* () { + pages.length = 0 + allow = false + resolveFailure = new Error("Path is not a file") + listResolveFailure = undefined + listReal = "/project/src" + afterApproval = () => {} + const registry = yield* ToolRegistry.Service + + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-read-directory-denied", name: "read", input: { path: "src" } }, + }), + ).toEqual({ type: "error", value: "Unable to read src" }) + expect(pages).toEqual([]) + }), + ) + + it.effect("does not list when the directory changes after permission approval", () => + Effect.gen(function* () { + pages.length = 0 + allow = true + resolveFailure = new Error("Path is not a file") + listResolveFailure = undefined + listReal = "/project/src" + afterApproval = () => { + listReal = "/outside/src" + } + const registry = yield* ToolRegistry.Service + + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-read-directory-swapped", name: "read", input: { path: "src" } }, + }), + ).toEqual({ type: "error", value: "Unable to read src" }) + expect(pages).toEqual([]) + }), + ) + + it.effect("authorizes project references with their canonical identity", () => + Effect.gen(function* () { + assertions.length = 0 + reads.length = 0 + allow = true + resolveFailure = undefined + listResolveFailure = new Error("not a directory") + size = 5 + real = "/project/README.md" + afterApproval = () => {} + resolvedInput = undefined + const registry = yield* ToolRegistry.Service + + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md", reference: "docs" } }, + }) + + expect(assertions).toMatchObject([{ resources: ["docs:README.md"] }]) + }), + ) + + it.effect("settles missing files as typed tool errors", () => + Effect.gen(function* () { + allow = true + reads.length = 0 + real = "/project/README.md" + afterApproval = () => {} + const registry = yield* ToolRegistry.Service + + resolveFailure = new Error("missing") + listResolveFailure = new Error("missing") + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-missing", name: "read", input: { path: "missing.txt" } }, + }), + ).toEqual({ type: "error", value: "Unable to read missing.txt" }) + + expect(reads).toEqual([]) + }), + ) + + it.effect("reads large UTF-8 text files as bounded pages with continuation", () => + Effect.gen(function* () { + textPageInputs.length = 0 + allow = true + resolveFailure = undefined + listResolveFailure = new Error("not a directory") + size = FileSystem.MAX_READ_BYTES + 1 + real = "/project/large.txt" + afterApproval = () => {} + const registry = yield* ToolRegistry.Service + + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-large", name: "read", input: { path: "large.txt", offset: 2, limit: 1 } }, + }), + ).toEqual({ + type: "json", + value: { type: "text-page", content: "hello", mime: "text/plain", offset: 2, truncated: true, next: 3 }, + }) + expect(textPageInputs).toEqual([{ offset: 2, limit: 1 }]) + }), + ) + + it.effect("does not read when the file changes after permission approval", () => + Effect.gen(function* () { + assertions.length = 0 + reads.length = 0 + allow = true + resolveFailure = undefined + listResolveFailure = new Error("not a directory") + size = 5 + real = "/project/README.md" + afterApproval = () => { + real = "/outside/README.md" + } + const registry = yield* ToolRegistry.Service + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-swapped", name: "read", input: { path: "README.md" } }, + }), + ).toEqual({ type: "error", value: "Unable to read README.md" }) + expect(reads).toEqual([]) + }), + ) +}) diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts new file mode 100644 index 0000000000..a63ae25315 --- /dev/null +++ b/packages/core/test/tool-skill.test.ts @@ -0,0 +1,145 @@ +import fs from "fs/promises" +import path from "path" +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { PluginBoot } from "@opencode-ai/core/plugin/boot" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { SkillV2 } from "@opencode-ai/core/skill" +import { SkillTool } from "@opencode-ai/core/tool/skill" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { tmpdir } from "./fixture/tmpdir" +import { it } from "./lib/effect" + +const sessionID = SessionV2.ID.make("ses_skill_tool_test") + +describe("SkillTool", () => { + it.live("lists available skills, authorizes the selected name, and loads model-facing content", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + const directory = path.join(tmp.path, "effect") + const location = path.join(directory, "SKILL.md") + const reference = path.join(directory, "reference.md") + yield* Effect.promise(() => fs.mkdir(directory, { recursive: true })) + yield* Effect.promise(() => + Promise.all([fs.writeFile(location, "unused"), fs.writeFile(reference, "reference")]), + ) + + const info: SkillV2.Info = { + name: "effect", + description: "Use Effect", + location: AbsolutePath.make(location), + content: "# Effect\n\nGuidance", + } + const assertions: PermissionV2.AssertInput[] = [] + const truncations: ToolOutputStore.TruncateInput[] = [] + let truncate = (input: ToolOutputStore.TruncateInput): Effect.Effect => + Effect.succeed({ content: input.content, truncated: false }) + let bootWaited = false + const boot = Layer.succeed( + PluginBoot.Service, + PluginBoot.Service.of({ wait: () => Effect.sync(() => { bootWaited = true }) }), + ) + const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => Effect.sync(() => assertions.push(input)), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), + ) + const skills = Layer.succeed( + SkillV2.Service, + SkillV2.Service.of({ + transform: () => Effect.die("unused"), + sources: () => Effect.die("unused"), + list: () => Effect.succeed([info]), + forAgent: () => Effect.die("unused"), + }), + ) + const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) + const resources = Layer.succeed( + ToolOutputStore.Service, + ToolOutputStore.Service.of({ + limits: () => Effect.die("unused"), + write: () => Effect.die("unused"), + truncate: (input) => Effect.sync(() => truncations.push(input)).pipe(Effect.andThen(truncate(input))), + read: () => Effect.die("unused"), + cleanup: () => Effect.die("unused"), + }), + ) + const tool = SkillTool.layer.pipe( + Layer.provide(registry), + Layer.provide(FSUtil.defaultLayer), + Layer.provide(boot), + Layer.provide(skills), + Layer.provide(resources), + ) + const layer = Layer.mergeAll(permission, skills, registry, boot, resources, tool) + + return yield* Effect.gen(function* () { + const registry = yield* ToolRegistry.Service + expect(bootWaited).toBe(true) + expect((yield* registry.definitions())[0]).toMatchObject({ + name: "skill", + description: expect.stringContaining("**effect**: Use Effect"), + }) + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-skill", name: "skill", input: { name: "effect" } }, + }), + ).toEqual({ + type: "text", + value: SkillTool.toModelOutput(info, [reference]), + }) + expect(truncations).toEqual([ + { sessionID, toolCallID: "call-skill", content: SkillTool.toModelOutput(info, [reference]) }, + ]) + truncate = (input) => + Effect.succeed({ + content: "HEAD\n\n... output truncated; full content available as tool-output://opaque ...\n\nTAIL", + truncated: true, + resource: new ToolOutputStore.Resource({ + uri: "tool-output://opaque", + mime: "text/plain", + size: input.content.length, + }), + }) + expect( + yield* registry.settle({ + sessionID, + call: { type: "tool-call", id: "call-skill-overflow", name: "skill", input: { name: "effect" } }, + }), + ).toMatchObject({ + result: { type: "text", value: expect.stringContaining("tool-output://opaque") }, + output: { + structured: { truncated: true, resource: { uri: "tool-output://opaque" } }, + }, + }) + expect(assertions).toEqual([ + { sessionID, action: "skill", resources: ["effect"], save: ["effect"] }, + { sessionID, action: "skill", resources: ["effect"], save: ["effect"] }, + ]) + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-missing-skill", name: "skill", input: { name: "missing" } }, + }), + ).toEqual({ type: "error", value: 'Skill "missing" not found. Available skills: effect' }) + }).pipe(Effect.provide(layer)) + }), + ), + ), + ) +}) diff --git a/packages/core/test/tool-todowrite.test.ts b/packages/core/test/tool-todowrite.test.ts new file mode 100644 index 0000000000..09ab1f2d12 --- /dev/null +++ b/packages/core/test/tool-todowrite.test.ts @@ -0,0 +1,106 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { Project } from "@opencode-ai/core/project" +import { ProjectTable } from "@opencode-ai/core/project/sql" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { SessionTable } from "@opencode-ai/core/session/sql" +import { SessionTodo } from "@opencode-ai/core/session/todo" +import { TodoWriteTool } from "@opencode-ai/core/tool/todowrite" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { testEffect } from "./lib/effect" + +const sessionID = SessionV2.ID.make("ses_todowrite_tool_test") +const assertions: PermissionV2.AssertInput[] = [] +let deny = false + +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => assertions.push(input)).pipe( + Effect.andThen(deny ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void), + ), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const todos = SessionTodo.layer.pipe(Layer.provide(database), Layer.provide(events)) +const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) +const tool = TodoWriteTool.layer.pipe(Layer.provide(registry), Layer.provide(todos)) +const it = testEffect(Layer.mergeAll(database, events, todos, permission, registry, tool)) + +const setup = Effect.gen(function* () { + assertions.length = 0 + deny = false + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "todowrite", + directory: "/project", + title: "todowrite", + version: "test", + }) + .run() + .pipe(Effect.orDie) +}) + +const call = (todos: ReadonlyArray, id = "call-todowrite") => ({ + sessionID, + call: { type: "tool-call" as const, id, name: TodoWriteTool.name, input: { todos } }, +}) + +describe("TodoWriteTool", () => { + it.effect("registers, approves the wildcard resource, persists todos, and returns typed output", () => + Effect.gen(function* () { + yield* setup + const registry = yield* ToolRegistry.Service + const service = yield* SessionTodo.Service + const todoList = [{ content: "Implement slice", status: "in_progress", priority: "high" }] + + expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual([TodoWriteTool.name]) + expect(yield* registry.settle(call(todoList))).toEqual({ + result: { type: "text", value: JSON.stringify(todoList, null, 2) }, + output: { + structured: { todos: todoList }, + content: [{ type: "text", text: JSON.stringify(todoList, null, 2) }], + }, + }) + expect(assertions).toEqual([{ sessionID, action: "todowrite", resources: ["*"], save: ["*"] }]) + expect(yield* service.get(sessionID)).toEqual(todoList) + }), + ) + + it.effect("does not update persisted todos when permission is denied", () => + Effect.gen(function* () { + yield* setup + const registry = yield* ToolRegistry.Service + const service = yield* SessionTodo.Service + yield* service.update({ sessionID, todos: [{ content: "keep", status: "pending", priority: "low" }] }) + deny = true + + expect(yield* registry.execute(call([{ content: "blocked", status: "completed", priority: "high" }]))).toEqual({ + type: "error", + value: "Unable to update todos", + }) + expect(yield* service.get(sessionID)).toEqual([{ content: "keep", status: "pending", priority: "low" }]) + expect(assertions).toEqual([{ sessionID, action: "todowrite", resources: ["*"], save: ["*"] }]) + }), + ) +}) diff --git a/packages/core/test/tool-webfetch.test.ts b/packages/core/test/tool-webfetch.test.ts new file mode 100644 index 0000000000..01d36ec7b1 --- /dev/null +++ b/packages/core/test/tool-webfetch.test.ts @@ -0,0 +1,295 @@ +import { describe, expect, test } from "bun:test" +import { Duration, Effect, Fiber, Layer, Schema } from "effect" +import * as TestClock from "effect/testing/TestClock" +import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { WebFetchTool } from "@opencode-ai/core/tool/webfetch" +import { testEffect } from "./lib/effect" + +const sessionID = SessionV2.ID.make("ses_webfetch_test") +const requests: Array<{ readonly url: string; readonly headers: Record }> = [] +const assertions: PermissionV2.AssertInput[] = [] +const truncations: ToolOutputStore.TruncateInput[] = [] +let respond = (_request: HttpClientRequest.HttpClientRequest) => + Effect.succeed(new Response("hello", { headers: { "content-type": "text/plain" } })) +let truncate = (input: ToolOutputStore.TruncateInput): Effect.Effect => + Effect.succeed({ content: input.content, truncated: false }) + +const http = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.sync(() => requests.push({ url: request.url, headers: request.headers })).pipe( + Effect.andThen(respond(request)), + Effect.map((response) => HttpClientResponse.fromWeb(request, response)), + ), + ), +) +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => Effect.sync(() => assertions.push(input)), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const resources = Layer.succeed( + ToolOutputStore.Service, + ToolOutputStore.Service.of({ + limits: () => Effect.die("unused"), + write: () => Effect.die("unused"), + truncate: (input) => Effect.sync(() => truncations.push(input)).pipe(Effect.andThen(truncate(input))), + read: () => Effect.die("unused"), + cleanup: () => Effect.die("unused"), + }), +) +const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) +const webfetch = WebFetchTool.layer.pipe(Layer.provide(registry), Layer.provide(http), Layer.provide(resources)) +const it = testEffect(Layer.mergeAll(registry, permission, http, resources, webfetch)) +const fetchWebfetch = WebFetchTool.layer.pipe( + Layer.provide(registry), + Layer.provide(FetchHttpClient.layer), + Layer.provide(resources), +) +const live = testEffect(Layer.mergeAll(registry, permission, FetchHttpClient.layer, resources, fetchWebfetch)) + +const reset = () => { + requests.length = 0 + assertions.length = 0 + truncations.length = 0 + respond = () => Effect.succeed(new Response("hello", { headers: { "content-type": "text/plain" } })) + truncate = (input) => Effect.succeed({ content: input.content, truncated: false }) +} + +const call = (input: typeof WebFetchTool.Parameters.Type, id = "call-webfetch") => ({ + sessionID, + call: { type: "tool-call" as const, id, name: "webfetch", input }, +}) + +describe("WebFetchTool helpers", () => { + test("defaults format and rejects invalid timeout controls", () => { + const decode = Schema.decodeUnknownSync(WebFetchTool.Parameters) + expect(decode({ url: "https://example.com" })).toEqual({ url: "https://example.com", format: "markdown" }) + expect(() => decode({ url: "https://example.com", timeout: 0 })).toThrow() + expect(() => decode({ url: "https://example.com", timeout: WebFetchTool.MAX_TIMEOUT_SECONDS + 1 })).toThrow() + }) + + test("ports HTML text and markdown conversions without active content", () => { + const html = "

Hello

world wide

" + expect(WebFetchTool.extractTextFromHTML(html)).toBe("Helloworld wide") + expect(WebFetchTool.convertHTMLToMarkdown(html)).toBe("# Hello\n\nworld **wide**") + }) +}) + +describe("WebFetchTool contribution", () => { + it.effect("registers and fetches an ordinary hostname HTTP URL without rewriting it", () => + Effect.gen(function* () { + reset() + const registry = yield* ToolRegistry.Service + const url = "http://example.com/public" + + expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["webfetch"]) + expect(yield* registry.settle(call({ url, format: "text", timeout: 4 }))).toEqual({ + result: { type: "text", value: "hello" }, + output: { + structured: { url, contentType: "text/plain", format: "text", output: "hello", truncated: false }, + content: [{ type: "text", text: "hello" }], + }, + }) + expect(assertions).toEqual([ + { sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text", timeout: 4 } }, + ]) + expect(requests).toMatchObject([{ url, headers: { accept: expect.stringContaining("text/plain;q=1.0") } }]) + }), + ) + + it.effect("accepts localhost URLs with the same requested-URL permission check", () => + Effect.gen(function* () { + reset() + const registry = yield* ToolRegistry.Service + const url = "http://localhost/private" + + expect(yield* registry.execute(call({ url, format: "text" }))).toEqual({ + type: "text", + value: "hello", + }) + expect(assertions).toEqual([ + { sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text" } }, + ]) + expect(requests.map((request) => request.url)).toEqual([url]) + }), + ) + + live.effect("follows redirects while approving only the requested URL", () => + Effect.acquireUseRelease( + Effect.sync(() => + Bun.serve({ + port: 0, + fetch: (request) => + new URL(request.url).pathname === "/redirect" + ? new Response("", { status: 302, headers: { location: "/target" } }) + : new Response("redirected", { headers: { "content-type": "text/plain" } }), + }), + ), + (server) => + Effect.gen(function* () { + reset() + const registry = yield* ToolRegistry.Service + const url = new URL("/redirect", server.url).toString() + + expect(yield* registry.execute(call({ url, format: "text" }))).toEqual({ type: "text", value: "redirected" }) + expect(assertions).toEqual([ + { sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text" } }, + ]) + }), + (server) => Effect.promise(() => server.stop(true)), + ), + ) + + it.effect("rejects non-HTTP schemes before permission or transport", () => + Effect.gen(function* () { + reset() + const registry = yield* ToolRegistry.Service + + expect(yield* registry.execute(call({ url: "file:///etc/passwd", format: "text" }))).toEqual({ + type: "error", + value: "Unable to fetch file:///etc/passwd", + }) + expect(assertions).toEqual([]) + expect(requests).toEqual([]) + }), + ) + + it.effect("converts HTML to requested markdown and text", () => + Effect.gen(function* () { + reset() + respond = () => + Effect.succeed( + new Response("

Hello

world

", { + headers: { "content-type": "text/html; charset=utf-8" }, + }), + ) + const registry = yield* ToolRegistry.Service + + expect(yield* registry.execute(call({ url: "https://1.1.1.1", format: "markdown" }))).toEqual({ + type: "text", + value: "# Hello\n\nworld", + }) + expect(yield* registry.execute(call({ url: "https://1.1.1.1", format: "text" }))).toEqual({ + type: "text", + value: "Helloworld", + }) + }), + ) + + it.effect("exposes managed overflow through an opaque resource URI", () => + Effect.gen(function* () { + reset() + truncate = (input) => + Effect.succeed({ + content: "HEAD\n\n... output truncated; full content available as tool-output://opaque ...\n\nTAIL", + truncated: true, + resource: new ToolOutputStore.Resource({ + uri: "tool-output://opaque", + mime: input.mime ?? "text/plain", + size: input.content.length, + }), + }) + const registry = yield* ToolRegistry.Service + const settled = yield* registry.settle(call({ url: "https://1.1.1.1", format: "html" }, "call-overflow")) + + expect(settled.result).toMatchObject({ type: "text", value: expect.stringContaining("tool-output://opaque") }) + expect(settled.output?.structured).toMatchObject({ + truncated: true, + resource: { uri: "tool-output://opaque", mime: "text/html" }, + }) + expect(truncations).toEqual([{ sessionID, toolCallID: "call-overflow", content: "hello", mime: "text/html" }]) + }), + ) + + it.effect("rejects declared and streamed oversized bodies", () => + Effect.gen(function* () { + reset() + const registry = yield* ToolRegistry.Service + respond = () => + Effect.succeed( + new Response("small", { + headers: { "content-type": "text/plain", "content-length": String(WebFetchTool.MAX_RESPONSE_BYTES + 1) }, + }), + ) + expect(yield* registry.execute(call({ url: "https://1.1.1.1/declared", format: "text" }))).toEqual({ + type: "error", + value: "Unable to fetch https://1.1.1.1/declared", + }) + + respond = () => + Effect.succeed( + new Response("x".repeat(WebFetchTool.MAX_RESPONSE_BYTES + 1), { headers: { "content-type": "text/plain" } }), + ) + expect(yield* registry.execute(call({ url: "https://1.1.1.1/streamed", format: "text" }))).toEqual({ + type: "error", + value: "Unable to fetch https://1.1.1.1/streamed", + }) + }), + ) + + it.effect("keeps images and files unsupported until typed settlement can carry attachments", () => + Effect.gen(function* () { + reset() + const registry = yield* ToolRegistry.Service + respond = () => Effect.succeed(new Response("png", { headers: { "content-type": "image/png" } })) + expect(yield* registry.execute(call({ url: "https://1.1.1.1/image", format: "html" }))).toEqual({ + type: "error", + value: "Unable to fetch https://1.1.1.1/image", + }) + + respond = () => Effect.succeed(new Response("pdf", { headers: { "content-type": "application/pdf" } })) + expect(yield* registry.execute(call({ url: "https://1.1.1.1/file", format: "html" }))).toEqual({ + type: "error", + value: "Unable to fetch https://1.1.1.1/file", + }) + expect(truncations).toEqual([]) + }), + ) + + it.effect("retries Cloudflare challenges with an honest user agent", () => + Effect.gen(function* () { + reset() + let count = 0 + respond = () => + Effect.succeed( + ++count === 1 + ? new Response("challenge", { status: 403, headers: { "cf-mitigated": "challenge" } }) + : new Response("ok", { headers: { "content-type": "text/plain" } }), + ) + const registry = yield* ToolRegistry.Service + + expect(yield* registry.execute(call({ url: "https://1.1.1.1", format: "text" }))).toEqual({ + type: "text", + value: "ok", + }) + expect(requests).toHaveLength(2) + expect(requests[0]?.headers["user-agent"]).toContain("Mozilla/5.0") + expect(requests[1]?.headers["user-agent"]).toBe("opencode") + }), + ) + + it.effect("times out stalled requests", () => + Effect.gen(function* () { + reset() + respond = () => Effect.never + const registry = yield* ToolRegistry.Service + const fiber = yield* registry + .execute(call({ url: "https://1.1.1.1/slow", format: "text", timeout: 1 })) + .pipe(Effect.forkChild) + yield* TestClock.adjust(Duration.seconds(1)) + + expect(yield* Fiber.join(fiber)).toEqual({ type: "error", value: "Unable to fetch https://1.1.1.1/slow" }) + }), + ) +}) diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts new file mode 100644 index 0000000000..f282080f3d --- /dev/null +++ b/packages/core/test/tool-websearch.test.ts @@ -0,0 +1,305 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Layer, Schema } from "effect" +import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { WebSearchTool } from "@opencode-ai/core/tool/websearch" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { testEffect } from "./lib/effect" + +const sessionID = SessionV2.ID.make("ses_websearch_test") +const payload = (text: string) => + JSON.stringify({ + jsonrpc: "2.0", + id: 1, + result: { content: [{ type: "text", text }] }, + }) + +describe("WebSearchTool provider selection", () => { + test("rejects out-of-range numeric controls", () => { + const decode = Schema.decodeUnknownSync(WebSearchTool.Parameters) + expect(() => decode({ query: "x", numResults: 0 })).toThrow() + expect(() => decode({ query: "x", numResults: WebSearchTool.MAX_NUM_RESULTS + 1 })).toThrow() + expect(() => decode({ query: "x", contextMaxCharacters: WebSearchTool.MAX_CONTEXT_CHARACTERS + 1 })).toThrow() + }) + test("selects a stable provider per session", () => { + expect(WebSearchTool.selectProvider(sessionID)).toBe(WebSearchTool.selectProvider(sessionID)) + }) + + test("supports an explicit operational override", () => { + expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "parallel")).toBe("parallel") + expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "exa")).toBe("exa") + }) + + test("prefers Parallel when both explicit flags are enabled", () => { + expect(WebSearchTool.selectProvider(sessionID, { enableExa: true, enableParallel: true })).toBe("parallel") + }) + + test("prefers Exa when only its explicit flag is enabled", () => { + expect(WebSearchTool.selectProvider(sessionID, { enableExa: true, enableParallel: false })).toBe("exa") + }) +}) + +describe("WebSearchTool MCP response parser", () => { + test("parses plain JSON-RPC responses", async () => { + expect(await Effect.runPromise(WebSearchTool.parseResponse(payload("search results")))).toBe("search results") + }) + + test("parses SSE JSON-RPC responses and ignores non-JSON frames", async () => { + expect(await Effect.runPromise(WebSearchTool.parseResponse(`data: [DONE]\nevent: message\ndata: ${payload("search results")}\n\n`))).toBe( + "search results", + ) + }) +}) + +interface Request { + readonly url: string + readonly headers: Record + readonly body: unknown +} + +const requests: Request[] = [] +const assertions: PermissionV2.AssertInput[] = [] +const truncations: ToolOutputStore.TruncateInput[] = [] +let responseBody = payload("search results") +let config: WebSearchTool.Config = { enableExa: false, enableParallel: false } +let truncate = (input: ToolOutputStore.TruncateInput): Effect.Effect => + Effect.succeed({ content: input.content, truncated: false }) + +const http = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.sync(() => { + if (request.body._tag !== "Uint8Array") throw new Error(`Unexpected request body: ${request.body._tag}`) + requests.push({ + url: request.url, + headers: request.headers, + body: JSON.parse(new TextDecoder().decode(request.body.body)), + }) + return HttpClientResponse.fromWeb(request, new Response(responseBody, { status: 200 })) + }), + ), +) +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => Effect.sync(() => assertions.push(input)), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) +const websearchConfig = Layer.succeed( + WebSearchTool.ConfigService, + WebSearchTool.ConfigService.of({ + get provider() { + return config.provider + }, + get enableExa() { + return config.enableExa + }, + get enableParallel() { + return config.enableParallel + }, + get exaApiKey() { + return config.exaApiKey + }, + get parallelApiKey() { + return config.parallelApiKey + }, + }), +) +const resources = Layer.succeed( + ToolOutputStore.Service, + ToolOutputStore.Service.of({ + limits: () => Effect.die("unused"), + write: () => Effect.die("unused"), + truncate: (input) => Effect.sync(() => truncations.push(input)).pipe(Effect.andThen(truncate(input))), + read: () => Effect.die("unused"), + cleanup: () => Effect.die("unused"), + }), +) +const websearch = WebSearchTool.layer.pipe( + Layer.provide(registry), + Layer.provide(permission), + Layer.provide(http), + Layer.provide(websearchConfig), + Layer.provide(resources), +) +const it = testEffect(Layer.mergeAll(registry, permission, http, websearchConfig, resources, websearch)) + +describe("WebSearchTool contribution", () => { + it.effect("registers websearch, asserts query permission, and calls Exa", () => + Effect.gen(function* () { + requests.length = 0 + assertions.length = 0 + truncations.length = 0 + truncate = (input) => Effect.succeed({ content: input.content, truncated: false }) + responseBody = payload("exa results") + config = { provider: "exa", enableExa: false, enableParallel: false } + const registry = yield* ToolRegistry.Service + + expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["websearch"]) + expect( + yield* registry.execute({ + sessionID, + call: { + type: "tool-call", + id: "call-exa", + name: "websearch", + input: { query: "effect typescript", numResults: 3, livecrawl: "preferred", type: "fast", contextMaxCharacters: 2500 }, + }, + }), + ).toEqual({ type: "text", value: "exa results" }) + expect(assertions).toEqual([ + { + sessionID, + action: "websearch", + resources: ["effect typescript"], + save: ["*"], + metadata: { query: "effect typescript", numResults: 3, livecrawl: "preferred", type: "fast", contextMaxCharacters: 2500, provider: "exa" }, + }, + ]) + expect(requests).toEqual([ + { + url: WebSearchTool.EXA_URL, + headers: expect.any(Object), + body: { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "web_search_exa", + arguments: { query: "effect typescript", type: "fast", numResults: 3, livecrawl: "preferred", contextMaxCharacters: 2500 }, + }, + }, + }, + ]) + }), + ) + + it.effect("calls Parallel with session ID and keeps bearer credentials out of output", () => + Effect.gen(function* () { + requests.length = 0 + assertions.length = 0 + responseBody = payload("parallel results") + config = { provider: "parallel", enableExa: false, enableParallel: false, parallelApiKey: "parallel-secret" } + const registry = yield* ToolRegistry.Service + + const settled = yield* registry.settle({ + sessionID, + call: { type: "tool-call", id: "call-parallel", name: "websearch", input: { query: "effect layers" } }, + }) + + expect(requests[0]).toMatchObject({ + url: WebSearchTool.PARALLEL_URL, + headers: { authorization: "Bearer parallel-secret" }, + body: { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "web_search", + arguments: { objective: "effect layers", search_queries: ["effect layers"], session_id: sessionID }, + }, + }, + }) + expect(requests[0]?.body).not.toHaveProperty("params.arguments.model_name") + expect(settled).toEqual({ + result: { type: "text", value: "parallel results" }, + output: { structured: { provider: "parallel", text: "parallel results", truncated: false }, content: [{ type: "text", text: "parallel results" }] }, + }) + expect(JSON.stringify(settled)).not.toContain("parallel-secret") + }), + ) + + it.effect("keeps an Exa credential in the transport URL and out of model output", () => + Effect.gen(function* () { + requests.length = 0 + assertions.length = 0 + responseBody = payload("credentialed exa results") + config = { provider: "exa", enableExa: false, enableParallel: false, exaApiKey: "exa secret" } + const registry = yield* ToolRegistry.Service + + const settled = yield* registry.settle({ + sessionID, + call: { type: "tool-call", id: "call-exa-key", name: "websearch", input: { query: "effect schema" } }, + }) + + expect(requests[0]?.url).toBe(`${WebSearchTool.EXA_URL}?exaApiKey=exa+secret`) + expect(JSON.stringify(settled)).not.toContain("exa secret") + }), + ) + + it.effect("returns the legacy no-results fallback as concise model text", () => + Effect.gen(function* () { + requests.length = 0 + assertions.length = 0 + responseBody = "" + config = { provider: "exa", enableExa: false, enableParallel: false } + const registry = yield* ToolRegistry.Service + + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-empty", name: "websearch", input: { query: "nothing" } }, + }), + ).toEqual({ type: "text", value: WebSearchTool.NO_RESULTS }) + }), + ) + + it.effect("exposes managed overflow through typed structured output", () => + Effect.gen(function* () { + requests.length = 0 + assertions.length = 0 + truncations.length = 0 + responseBody = payload("full search results") + config = { provider: "exa", enableExa: false, enableParallel: false } + truncate = (input) => + Effect.succeed({ + content: "HEAD\n\n... output truncated; full content available as tool-output://opaque ...\n\nTAIL", + truncated: true, + resource: new ToolOutputStore.Resource({ + uri: "tool-output://opaque", + mime: "text/plain", + size: input.content.length, + }), + }) + const registry = yield* ToolRegistry.Service + + const settled = yield* registry.settle({ + sessionID, + call: { type: "tool-call", id: "call-overflow", name: "websearch", input: { query: "verbose" } }, + }) + + expect(settled.result).toMatchObject({ type: "text", value: expect.stringContaining("tool-output://opaque") }) + expect(settled.output?.structured).toMatchObject({ + provider: "exa", + truncated: true, + resource: { uri: "tool-output://opaque", mime: "text/plain" }, + }) + expect(truncations).toEqual([{ sessionID, toolCallID: "call-overflow", content: "full search results" }]) + }), + ) + + it.effect("rejects oversized MCP response bodies", () => + Effect.gen(function* () { + requests.length = 0 + assertions.length = 0 + responseBody = "x".repeat(WebSearchTool.MAX_RESPONSE_BYTES + 1) + config = { provider: "exa", enableExa: false, enableParallel: false } + const registry = yield* ToolRegistry.Service + + expect( + yield* registry.execute({ + sessionID, + call: { type: "tool-call", id: "call-large-response", name: "websearch", input: { query: "too much" } }, + }), + ).toEqual({ type: "error", value: "Unable to search the web for too much" }) + }), + ) +}) diff --git a/packages/core/test/tool-write.test.ts b/packages/core/test/tool-write.test.ts new file mode 100644 index 0000000000..56ba7bb944 --- /dev/null +++ b/packages/core/test/tool-write.test.ts @@ -0,0 +1,328 @@ +import fs from "fs/promises" +import path from "path" +import { fileURLToPath } from "url" +import { describe, expect, test } from "bun:test" +import { Effect, Layer } from "effect" +import { FileMutation } from "@opencode-ai/core/file-mutation" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Location } from "@opencode-ai/core/location" +import { LocationMutation } from "@opencode-ai/core/location-mutation" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolRegistry } from "@opencode-ai/core/tool-registry" +import { WriteTool } from "@opencode-ai/core/tool/write" +import { location } from "./fixture/location" +import { tmpdir } from "./fixture/tmpdir" +import { testEffect } from "./lib/effect" + +const sessionID = SessionV2.ID.make("ses_write_tool_test") +const assertions: PermissionV2.AssertInput[] = [] +const writes: string[] = [] +let denyAction: string | undefined +let afterAssertion = (_input: PermissionV2.AssertInput): Effect.Effect => Effect.void + +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => assertions.push(input)).pipe( + Effect.andThen( + input.action === denyAction + ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) + : afterAssertion(input), + ), + ), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) + +const reset = () => { + assertions.length = 0 + writes.length = 0 + denyAction = undefined + afterAssertion = () => Effect.void +} + +const filesystem = Layer.effect( + FSUtil.Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + return FSUtil.Service.of({ + ...fs, + writeWithDirs: (target, content, mode) => + Effect.sync(() => writes.push(target)).pipe(Effect.andThen(fs.writeWithDirs(target, content, mode))), + }) + }), +).pipe(Layer.provide(FSUtil.defaultLayer)) + +const withTool = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => { + const activeLocation = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make(directory) })), + ) + const planning = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation)) + const commits = FileMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(planning)) + const registry = ToolRegistry.layer.pipe(Layer.provide(permission)) + const write = WriteTool.layer.pipe(Layer.provide(registry), Layer.provide(planning), Layer.provide(commits)) + return Effect.gen(function* () { + return yield* body(yield* ToolRegistry.Service) + }).pipe(Effect.provide(Layer.mergeAll(registry, planning, commits, write))) +} + +const call = (input: typeof WriteTool.Parameters.Type, id = "call-write") => ({ + sessionID, + call: { type: "tool-call" as const, id, name: "write", input }, +}) + +const it = testEffect(Layer.empty) + +describe("WriteTool", () => { + it.live("registers and creates a relative file through FileMutation once", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + return withTool(tmp.path, (registry) => + Effect.gen(function* () { + expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["write"]) + const settled = yield* registry.settle(call({ path: "src/new.txt", content: "created" })) + expect(settled).toEqual({ + result: { type: "text", value: "Created file successfully: src/new.txt" }, + output: { + structured: { + operation: "write", + target: path.join(yield* Effect.promise(() => fs.realpath(tmp.path)), "src", "new.txt"), + resource: "src/new.txt", + existed: false, + }, + content: [{ type: "text", text: "Created file successfully: src/new.txt" }], + }, + }) + expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "src", "new.txt"), "utf8"))).toBe( + "created", + ) + expect(assertions).toEqual([{ sessionID, action: "edit", resources: ["src/new.txt"], save: ["*"] }]) + expect(writes).toEqual([path.join(yield* Effect.promise(() => fs.realpath(tmp.path)), "src", "new.txt")]) + }), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("overwrites a relative existing file and reports that it wrote the file", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + return Effect.promise(() => fs.writeFile(path.join(tmp.path, "existing.txt"), "before")).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => registry.settle(call({ path: "existing.txt", content: "after" }))), + ), + Effect.andThen((settled) => + Effect.gen(function* () { + expect(settled.result).toEqual({ type: "text", value: "Wrote file successfully: existing.txt" }) + expect(settled.output?.structured).toMatchObject({ resource: "existing.txt", existed: true }) + expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "existing.txt"), "utf8"))).toBe( + "after", + ) + expect(writes).toHaveLength(1) + }), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("preserves exactly one BOM when overwriting existing files", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const preserved = path.join(tmp.path, "preserved.txt") + const deduplicated = path.join(tmp.path, "deduplicated.txt") + return Effect.promise(() => + Promise.all([fs.writeFile(preserved, "\uFEFFbefore"), fs.writeFile(deduplicated, "\uFEFFbefore")]), + ).pipe( + Effect.andThen( + withTool(tmp.path, (registry) => + Effect.gen(function* () { + yield* registry.settle(call({ path: "preserved.txt", content: "after" }, "call-preserved")) + yield* registry.settle(call({ path: "deduplicated.txt", content: "\uFEFFafter" }, "call-deduplicated")) + + expect(yield* Effect.promise(() => fs.readFile(preserved, "utf8"))).toBe("\uFEFFafter") + expect(yield* Effect.promise(() => fs.readFile(deduplicated, "utf8"))).toBe("\uFEFFafter") + }), + ), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("accepts an absolute file path inside the active Location", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + const target = path.join(tmp.path, "absolute.txt") + return withTool(tmp.path, (registry) => registry.execute(call({ path: target, content: "inside" }))).pipe( + Effect.andThen((result) => + Effect.gen(function* () { + expect(result).toEqual({ type: "text", value: "Created file successfully: absolute.txt" }) + expect(assertions.map((input) => input.action)).toEqual(["edit"]) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("inside") + }), + ), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("approves an explicit external absolute path before edit", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + reset() + const target = path.join(outside.path, "external.txt") + return withTool(active.path, (registry) => registry.settle(call({ path: target, content: "external" }))).pipe( + Effect.andThen((settled) => + Effect.gen(function* () { + const canonicalTarget = path.join(yield* Effect.promise(() => fs.realpath(outside.path)), "external.txt") + expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"]) + expect(assertions[0]).toMatchObject({ + resources: [ + path.join(yield* Effect.promise(() => fs.realpath(outside.path)), "*").replaceAll("\\", "/"), + ], + }) + expect(assertions[1]).toMatchObject({ resources: [canonicalTarget.replaceAll("\\", "/")], save: ["*"] }) + expect(settled.output?.structured).toMatchObject({ + target: canonicalTarget, + resource: canonicalTarget.replaceAll("\\", "/"), + existed: false, + }) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("external") + expect(writes).toEqual([canonicalTarget]) + }), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + + it.live("does not write when external_directory or edit approval is denied", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => + Effect.gen(function* () { + const external = path.join(outside.path, "denied.txt") + reset() + denyAction = "external_directory" + expect( + yield* withTool(active.path, (registry) => registry.execute(call({ path: external, content: "blocked" }))), + ).toEqual({ + type: "error", + value: `Unable to write ${external}`, + }) + expect(assertions.map((input) => input.action)).toEqual(["external_directory"]) + expect(writes).toEqual([]) + + reset() + denyAction = "edit" + expect( + yield* withTool(active.path, (registry) => + registry.execute(call({ path: "denied.txt", content: "blocked" })), + ), + ).toEqual({ + type: "error", + value: "Unable to write denied.txt", + }) + expect(assertions.map((input) => input.action)).toEqual(["edit"]) + expect(writes).toEqual([]) + }), + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + + if (process.platform !== "win32") { + it.live("delegates post-approval revalidation to FileMutation before writing", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + reset() + const parent = path.join(active.path, "parent") + afterAssertion = (input) => + input.action === "edit" + ? Effect.promise(async () => { + await fs.rmdir(parent) + await fs.symlink(outside.path, parent) + }) + : Effect.void + return Effect.promise(() => fs.mkdir(parent)).pipe( + Effect.andThen( + withTool(active.path, (registry) => + registry.execute(call({ path: "parent/escape.txt", content: "blocked" })), + ), + ), + Effect.andThen((result) => + Effect.gen(function* () { + expect(result).toEqual({ type: "error", value: "Unable to write parent/escape.txt" }) + expect(assertions.map((input) => input.action)).toEqual(["edit"]) + expect(writes).toEqual([]) + expect( + yield* Effect.promise(() => + fs.stat(path.join(outside.path, "escape.txt")).then( + () => true, + () => false, + ), + ), + ).toBe(false) + }), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + } +}) + +test("keeps the locked write schema, semantics docstring, and deferred UX TODOs visible", async () => { + const source = (await fs.readFile(new URL("../src/tool/write.ts", import.meta.url), "utf8")).replaceAll("\r\n", "\n") + const definition = await Effect.runPromise( + withTool(path.dirname(fileURLToPath(import.meta.url)), (registry) => registry.definitions()), + ) + const schema = definition[0]?.inputSchema as { readonly properties?: Record } + + expect(Object.keys(schema.properties ?? {}).sort()).toEqual(["content", "path"]) + expect(source).toContain( + "Named project references\n * are read-oriented and deliberately are not accepted by mutation tools.", + ) + for (const todo of [ + "Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.", + "Add formatter integration after V2 formatter runtime exists.", + "Publish watcher/file-edit events after V2 watcher integration exists.", + "Add snapshots / undo after design exists.", + "Add LSP notification and diagnostics after V2 LSP runtime exists.", + ]) { + expect(source).toContain(`TODO: ${todo}`) + } +}) diff --git a/packages/llm/AGENTS.md b/packages/llm/AGENTS.md index 29cb71f1c4..25d993b234 100644 --- a/packages/llm/AGENTS.md +++ b/packages/llm/AGENTS.md @@ -10,7 +10,7 @@ ## Conventions -Per-type constructors live on the type, not as top-level re-exports. Use `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `Model.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, `LLM.updateRequest`, and `LLM.generateObject`. Two ways to construct the same thing is one too many. +Per-type constructors live on the type, not as top-level re-exports. Use `Message.system(...)`, `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `Model.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, `LLM.updateRequest`, and `LLM.generateObject`. Two ways to construct the same thing is one too many. ## Tests @@ -25,7 +25,7 @@ Primary in-repo integration point: - `packages/opencode/src/session/llm.ts` is the session-owned orchestration layer that decides whether a request uses AI SDK or this package's native route runtime. - `packages/opencode/src/session/llm/native-request.ts` is the lowering adapter from opencode's session/AI SDK-shaped data into this package's `LLMRequest` model. -- `packages/opencode/src/session/llm/native-runtime.ts` is the execution adapter that calls `LLMClient.stream(...)` and bridges opencode tools into this package's tool runtime. +- `packages/opencode/src/session/llm/native-runtime.ts` is the execution adapter that calls raw `LLMClient.stream(request)` and bridges one provider turn of opencode tool calls through this package's typed dispatcher. - `packages/opencode/src/session/llm/ai-sdk.ts` keeps the default AI SDK path compatible by converting AI SDK stream parts into this package's shared `LLMEvent`s. Keep this package independent of session concerns. Session auth, permissions, plugins, telemetry headers, and runtime selection belong in `packages/opencode/src/session/llm.ts` and its local adapters. @@ -153,7 +153,7 @@ packages/llm/src/ openai-compatible-profile.ts family defaults (deepseek, togetherai, ...) azure.ts / amazon-bedrock.ts / cloudflare.ts / github-copilot.ts / google.ts / xai.ts / openai.ts / anthropic.ts / openrouter.ts tool.ts typed tool() helper - tool-runtime.ts implementation helpers for LLMClient tool execution + tool-runtime.ts narrow one-call typed tool dispatcher ``` The dependency arrow points down: `providers/*.ts` files import protocol routes and auth-option utilities; protocol modules import `endpoint`, `auth`, `framing`, and transport pieces. Protocols do not import provider facades. Lower-level modules know nothing about provider catalog metadata. @@ -171,6 +171,20 @@ The dependency arrow points down: `providers/*.ts` files import protocol routes If you find yourself copying a 3-to-5-line snippet between two protocols, lift it into `ProviderShared` next to these helpers rather than duplicating. +### Chronological System Updates + +`LLMRequest.system` is the initial privileged prompt that applies ahead of the conversation. `Message.system(...)` is a separate, provider-neutral chronological operator update inside `LLMRequest.messages`; it applies only from its position in history onward and accepts text content only. + +Native chronological system messages are route/model-specific. Anthropic Messages lowers them natively for Claude Opus 4.8 (`claude-opus-4-8`). Other routes and models intentionally lower the update in place into ordinary user-compatible text using this stable escaped representation: + +```text + +... + +``` + +The wrapped-user fallback preserves ordering while visibly lowering authority. Never silently pass a raw chronological `role: "system"` through a route that might reject it. Do not insert raw retrieved documents, tool output, or web content into privileged chronological system updates; keep untrusted content in ordinary user/tool channels. + ### Tools Tool loops are represented in common messages and events: @@ -187,9 +201,9 @@ const followUp = LLM.request({ Routes lower these into provider-native assistant tool-call messages and tool-result messages. Streaming providers should emit `tool-input-delta` events while arguments arrive, then a final `tool-call` event with parsed input. -### Tool runtime +### Tool dispatch -`LLM.stream({ request, tools })` executes model-requested tools with full type safety. Plain `LLM.stream(request)` only streams the model; if `request.tools` contains schemas, tool calls are returned for the caller to handle. Use `toolExecution: "none"` to pass executable tool definitions as schemas without invoking handlers. Add `stopWhen` to opt into follow-up model rounds after tool results. +`LLM.stream(request)` and `LLM.generate(request)` each run exactly one provider turn. Add tool schemas to `request.tools` with `Tool.toDefinitions(tools)`. When a caller wants the package's typed one-call execution behavior, pass each canonical local `tool-call` event to `ToolRuntime.dispatch(tools, call)`. ```ts const get_weather = tool({ @@ -205,22 +219,25 @@ const get_weather = tool({ }), }) -const events = yield* LLM.stream({ - request, - tools: { get_weather, get_time, ... }, - stopWhen: LLM.stepCountIs(10), -}).pipe(Stream.runCollect) +const tools = { get_weather, get_time, ... } +const events = yield* LLM.stream( + LLM.updateRequest(request, { tools: Tool.toDefinitions(tools) }), +).pipe(Stream.runCollect) + +const call = Array.from(events).find(LLMEvent.is.toolCall) +if (call && !call.providerExecuted) { + const dispatched = yield* ToolRuntime.dispatch(tools, call) + // Persist call + dispatched.result, then construct the next request explicitly. +} ``` -The runtime: +The dispatcher: -- Adds tool definitions (derived from each tool's `parameters` Schema via `Schema.toJsonSchemaDocument`) onto `request.tools`. -- Streams the model. -- On `tool-call`: looks up the named tool, decodes input against `parameters` Schema, dispatches to the typed `execute`, encodes the result against `success` Schema, emits `tool-result`. -- Emits local `tool-result` events in the same step by default. -- Loops only when `stopWhen` is provided and the step finishes with `tool-calls`, appending the assistant + tool messages. +- On `tool-call`: looks up the named tool, decodes input against `parameters` Schema, dispatches to the typed `execute`, encodes the result against `success` Schema, and returns canonical `tool-result` events. +- Does not stream providers, construct Session events, schedule fibers, append history, count steps, or continue model rounds. +- Leaves persistence and continuation to the enclosing product flow. -Handler dependencies (services, permissions, plugin hooks, abort handling) are closed over by the consumer at tool-construction time. The runtime's only environment requirement is `RequestExecutor.Service`. Build the tools record inside an `Effect.gen` once and reuse it across many runs. +Handler dependencies (services, permissions, plugin hooks, abort handling) are closed over by the consumer at tool-construction time. Build the tools record inside an `Effect.gen` once and reuse it across many dispatches. Errors must be expressed as `ToolFailure`. The runtime catches it and emits a `tool-error` event, then a `tool-result` of `type: "error"`, so the model can self-correct on the next step. Anything that is not a `ToolFailure` is treated as a defect and fails the stream. Three recoverable error paths produce `tool-error` events: @@ -231,8 +248,8 @@ Errors must be expressed as `ToolFailure`. The runtime catches it and emits a `t Provider-defined / hosted tools (Anthropic `web_search` / `code_execution` / `web_fetch`, OpenAI Responses `web_search_call` / `file_search_call` / `code_interpreter_call` / `mcp_call` / `local_shell_call` / `image_generation_call` / `computer_use_call`) pass through the runtime untouched: - Routes surface the model's call as a `tool-call` event with `providerExecuted: true`, and the provider's result as a matching `tool-result` event with `providerExecuted: true`. -- The runtime detects `providerExecuted` on `tool-call` and **skips client dispatch** — no handler is invoked and no `tool-error` is raised for "unknown tool". The provider already executed it. -- Both events are appended to the assistant message in `assistantContent` so the next round's history carries the call + result for context. Anthropic encodes them back as `server_tool_use` + `web_search_tool_result` (or `code_execution_tool_result` / `web_fetch_tool_result`) blocks; OpenAI Responses callers typically use `previous_response_id` instead of resending hosted-tool items. +- Callers detect `providerExecuted` on `tool-call` and **skip local dispatch** — no handler is invoked and no `tool-error` is raised for "unknown tool". The provider already executed it. +- Callers that continue should retain both events in explicit history when the protocol requires it. Anthropic encodes them back as `server_tool_use` + `web_search_tool_result` (or `code_execution_tool_result` / `web_fetch_tool_result`) blocks; OpenAI Responses callers typically use `previous_response_id` instead of resending hosted-tool items. Add provider-defined tools to `request.tools` (no runtime entry needed). The matching route must know how to lower the tool definition into the provider-native shape; right now Anthropic accepts `web_search` / `code_execution` / `web_fetch` and OpenAI Responses accepts the hosted tool names listed above. diff --git a/packages/llm/example/tutorial.ts b/packages/llm/example/tutorial.ts index 0bf766c529..f233497cff 100644 --- a/packages/llm/example/tutorial.ts +++ b/packages/llm/example/tutorial.ts @@ -1,5 +1,5 @@ import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect" -import { LLM, LLMClient, ProviderID, Tool } from "@opencode-ai/llm" +import { LLM, LLMClient, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/llm" import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route" import { OpenAI } from "@opencode-ai/llm/providers" @@ -84,9 +84,9 @@ const streamText = LLM.stream(request).pipe( Stream.runDrain, ) -// 5. Tools are typed with Effect Schema. Passing tools to `LLMClient.stream` -// adds their definitions to the request and dispatches matching tool calls. -// Add `stopWhen` to opt into follow-up model rounds after tool results. +// 5. Tools are typed with Effect Schema. Provider turns remain explicit: +// advertise definitions on the request, stream one turn, dispatch local calls, +// then persist/build follow-up history in the enclosing product flow. const tools = { get_weather: Tool.make({ description: "Get current weather for a city.", @@ -96,24 +96,29 @@ const tools = { }), } -const streamWithTools = LLM.stream({ - request: LLM.request({ +const streamWithTools = Effect.gen(function* () { + const request = LLM.request({ model, prompt: "Use get_weather for San Francisco, then answer in one sentence.", generation: { maxTokens: 80, temperature: 0 }, - }), - tools, - stopWhen: LLM.stepCountIs(3), -}).pipe( - Stream.tap((event) => - Effect.sync(() => { + tools: Tool.toDefinitions(tools), + }) + const events = Array.from(yield* LLM.stream(request).pipe(Stream.runCollect)) + for (const event of events) { if (event.type === "tool-call") console.log("tool call", event.name, event.input) - if (event.type === "tool-result") console.log("tool result", event.name, event.result) if (event.type === "text-delta") process.stdout.write(event.text) - }), - ), - Stream.runDrain, -) + if (event.type !== "tool-call" || event.providerExecuted) continue + const dispatched = yield* ToolRuntime.dispatch(tools, event) + console.log("tool result", event.name, dispatched.result) + + // A durable agent would persist these messages before starting another + // raw model turn. This tutorial keeps the boundary visible instead. + const followUp = LLM.updateRequest(request, { + messages: [...request.messages, Message.assistant([event]), Message.tool({ ...event, result: dispatched.result })], + }) + console.log("follow-up history messages:", followUp.messages.length) + } +}) // 6. `generateObject` is the structured-output helper. It forces a synthetic // tool call internally, so the same call site works across providers instead of diff --git a/packages/llm/src/index.ts b/packages/llm/src/index.ts index 389bc263d2..b71bd6f7b5 100644 --- a/packages/llm/src/index.ts +++ b/packages/llm/src/index.ts @@ -8,7 +8,9 @@ export type { Service as LLMClientService, } from "./route/client" export * from "./schema" -export { Tool, ToolFailure, toDefinitions, tool } from "./tool" +export { Tool, ToolFailure, toDefinitions } from "./tool" +export { ToolRuntime } from "./tool-runtime" +export type { DispatchResult as ToolDispatchResult, ToolSettlement } from "./tool-runtime" export type { AnyExecutableTool, AnyTool, @@ -17,16 +19,11 @@ export type { Tool as ToolShape, ToolExecute, ToolExecuteContext, + ToolModelOutputInput, Tools, ToolSchema, + ToolToModelOutput, } from "./tool" -export type { - RunOptions as ToolRunOptions, - RuntimeState as ToolRuntimeState, - StopCondition as ToolStopCondition, - ToolExecution, -} from "./tool-runtime" - export * as LLM from "./llm" export type { Definition as ProviderDefinition, diff --git a/packages/llm/src/llm.ts b/packages/llm/src/llm.ts index 33ec56f3e9..e4781d8608 100644 --- a/packages/llm/src/llm.ts +++ b/packages/llm/src/llm.ts @@ -16,7 +16,7 @@ import { type ContentPart, ToolResultPart, } from "./schema" -import { make as makeTool, type ToolSchema } from "./tool" +import { make as makeTool, toDefinitions, type ToolSchema } from "./tool" export type ModelInput = SchemaModelInput @@ -46,8 +46,6 @@ export const generate = LLMClient.generate export const stream = LLMClient.stream -export const stepCountIs = LLMClient.stepCountIs - export const requestInput = (input: LLMRequest): RequestInput => ({ ...LLMRequest.input(input), }) @@ -115,13 +113,10 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* ( ) { const baseRequest = request(options) const generateRequest = LLMRequest.update(baseRequest, { + tools: toDefinitions({ [GENERATE_OBJECT_TOOL_NAME]: tool }), toolChoice: ToolChoice.named(GENERATE_OBJECT_TOOL_NAME), }) - const response = yield* LLMClient.generate({ - request: generateRequest, - tools: { [GENERATE_OBJECT_TOOL_NAME]: tool }, - toolExecution: "none", - }) + const response = yield* LLMClient.generate(generateRequest) const call = response.toolCalls.find( (event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME, ) diff --git a/packages/llm/src/protocols/anthropic-messages.ts b/packages/llm/src/protocols/anthropic-messages.ts index 234ccd5baf..0be0644fb6 100644 --- a/packages/llm/src/protocols/anthropic-messages.ts +++ b/packages/llm/src/protocols/anthropic-messages.ts @@ -128,6 +128,7 @@ type AnthropicToolResultBlock = Schema.Schema.Type @@ -340,13 +341,79 @@ const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultConte return yield* Effect.forEach(content, lowerToolResultContentItem) }) +// Mid-conversation system messages are a native Claude API feature only for +// Opus 4.8. Other Anthropic models intentionally use the same visible wrapped- +// user fallback as non-Anthropic routes rather than sending a role they reject. +const supportsNativeSystemUpdates = (request: LLMRequest) => String(request.model.id) === "claude-opus-4-8" + +const endsInServerToolUse = (message: LLMRequest["messages"][number]) => { + const last = message.content.at(-1) + return message.role === "assistant" && last?.type === "tool-call" && last.providerExecuted === true +} + +const endsInLocalToolUse = (message: LLMRequest["messages"][number]) => { + const last = message.content.at(-1) + return message.role === "assistant" && last?.type === "tool-call" && last.providerExecuted !== true +} + +const validateNativeSystemUpdate = Effect.fn("AnthropicMessages.validateNativeSystemUpdate")(function* ( + messages: LLMRequest["messages"], + index: number, +) { + const previous = messages[index - 1] + const next = messages[index + 1] + if (!previous) + return yield* invalid("Anthropic Messages chronological system updates cannot be the first message; use LLMRequest.system") + if (previous.role === "system") + return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive") + if (endsInLocalToolUse(previous)) + return yield* invalid("Anthropic Messages chronological system updates cannot appear between a local tool call and its tool result") + if (previous.role !== "user" && previous.role !== "tool" && !endsInServerToolUse(previous)) + return yield* invalid( + "Anthropic Messages chronological system updates must follow a user message, tool result, or assistant server tool use", + ) + if (next?.role === "system") + return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive") + if (next && next.role !== "assistant") + return yield* invalid("Anthropic Messages chronological system updates must end the messages array or immediately precede an assistant message") +}) + +const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUpdate")(function* ( + message: LLMRequest["messages"][number], + breakpoints: Cache.Breakpoints, +) { + const content = yield* ProviderShared.systemUpdateText("Anthropic Messages", message) + return { + role: "system" as const, + content: content.map((part) => ({ + type: "text" as const, + text: part.text, + cache_control: cacheControl(breakpoints, part.cache), + })), + } +}) + const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( request: LLMRequest, breakpoints: Cache.Breakpoints, ) { const messages: AnthropicMessage[] = [] - for (const message of request.messages) { + for (const [index, message] of request.messages.entries()) { + if (message.role === "system") { + if (supportsNativeSystemUpdates(request)) { + yield* validateNativeSystemUpdate(request.messages, index) + messages.push(yield* lowerNativeSystemUpdate(message, breakpoints)) + continue + } + const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message) + const block = { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) } + const previous = messages.at(-1) + if (previous?.role === "user") messages[messages.length - 1] = { role: "user", content: [...previous.content, block] } + else messages.push({ role: "user", content: [block] }) + continue + } + if (message.role === "user") { const content: AnthropicUserBlock[] = [] for (const part of message.content) { diff --git a/packages/llm/src/protocols/bedrock-converse.ts b/packages/llm/src/protocols/bedrock-converse.ts index 54eb7930f8..d437a004a4 100644 --- a/packages/llm/src/protocols/bedrock-converse.ts +++ b/packages/llm/src/protocols/bedrock-converse.ts @@ -8,6 +8,8 @@ import { type CacheHint, type FinishReason, type LLMRequest, + type ProviderMetadata, + type ReasoningPart, type ToolCallPart, type ToolDefinition, type ToolResultPart, @@ -237,6 +239,13 @@ const lowerToolChoice = (toolChoice: NonNullable) => tool: (name) => ({ tool: { name } }) as const, }) +const bedrockMetadata = (metadata: Record): ProviderMetadata => ({ bedrock: metadata }) + +const reasoningSignature = (part: ReasoningPart) => { + const bedrock = part.providerMetadata?.bedrock + return part.encrypted ?? (ProviderShared.isRecord(bedrock) && typeof bedrock.signature === "string" ? bedrock.signature : undefined) +} + const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({ toolUse: { toolUseId: part.id, @@ -281,6 +290,15 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* ( const messages: BedrockMessage[] = [] for (const message of request.messages) { + if (message.role === "system") { + const part = yield* ProviderShared.wrappedSystemUpdate("Bedrock Converse", message) + const content = textWithCache(breakpoints, part.text, part.cache) + const previous = messages.at(-1) + if (previous?.role === "user") messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] } + else messages.push({ role: "user", content }) + continue + } + if (message.role === "user") { const content: BedrockUserBlock[] = [] for (const part of message.content) { @@ -315,7 +333,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* ( if (part.type === "reasoning") { content.push({ reasoningContent: { - reasoningText: { text: part.text, signature: part.encrypted }, + reasoningText: { text: part.text, signature: reasoningSignature(part) }, }, }) continue @@ -425,6 +443,7 @@ interface ParserState { readonly pendingFinish: { readonly reason: FinishReason; readonly usage?: Usage } | undefined readonly hasToolCalls: boolean readonly lifecycle: Lifecycle.State + readonly reasoningSignatures: Readonly> } const step = (state: ParserState, event: BedrockEvent) => @@ -468,17 +487,19 @@ const step = (state: ParserState, event: BedrockEvent) => ] as const } - if (event.contentBlockDelta?.delta?.reasoningContent?.text) { + if (event.contentBlockDelta?.delta?.reasoningContent) { + const index = event.contentBlockDelta.contentBlockIndex + const reasoning = event.contentBlockDelta.delta.reasoningContent const events: LLMEvent[] = [] return [ { ...state, - lifecycle: Lifecycle.reasoningDelta( - state.lifecycle, - events, - `reasoning-${event.contentBlockDelta.contentBlockIndex}`, - event.contentBlockDelta.delta.reasoningContent.text, - ), + lifecycle: reasoning.text + ? Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text) + : state.lifecycle, + reasoningSignatures: reasoning.signature + ? { ...state.reasoningSignatures, [index]: reasoning.signature } + : state.reasoningSignatures, }, events, ] as const @@ -501,15 +522,17 @@ const step = (state: ParserState, event: BedrockEvent) => } if (event.contentBlockStop) { - const result = yield* ToolStream.finish(ADAPTER, state.tools, event.contentBlockStop.contentBlockIndex) + const index = event.contentBlockStop.contentBlockIndex + const result = yield* ToolStream.finish(ADAPTER, state.tools, index) const events: LLMEvent[] = [] const resultEvents = result.events ?? [] const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : Lifecycle.reasoningEnd( - Lifecycle.textEnd(state.lifecycle, events, `text-${event.contentBlockStop.contentBlockIndex}`), + Lifecycle.textEnd(state.lifecycle, events, `text-${index}`), events, - `reasoning-${event.contentBlockStop.contentBlockIndex}`, + `reasoning-${index}`, + state.reasoningSignatures[index] ? bedrockMetadata({ signature: state.reasoningSignatures[index] }) : undefined, ) events.push(...resultEvents) return [ @@ -518,6 +541,9 @@ const step = (state: ParserState, event: BedrockEvent) => hasToolCalls: resultEvents.some(LLMEvent.is.toolCall) ? true : state.hasToolCalls, lifecycle, tools: result.tools, + reasoningSignatures: Object.fromEntries( + Object.entries(state.reasoningSignatures).filter(([key]) => key !== String(index)), + ), }, events, ] as const @@ -591,6 +617,7 @@ export const protocol = Protocol.make({ pendingFinish: undefined, hasToolCalls: false, lifecycle: Lifecycle.initial(), + reasoningSignatures: {}, }), step, onHalt, diff --git a/packages/llm/src/protocols/gemini.ts b/packages/llm/src/protocols/gemini.ts index 5fe4dcc760..480ce17b89 100644 --- a/packages/llm/src/protocols/gemini.ts +++ b/packages/llm/src/protocols/gemini.ts @@ -10,6 +10,7 @@ import { type FinishReason, type LLMRequest, type MediaPart, + type ProviderMetadata, type TextPart, type ToolCallPart, type ToolDefinition, @@ -136,6 +137,7 @@ interface ParserState { readonly nextToolCallId: number readonly usage?: Usage readonly lifecycle: Lifecycle.State + readonly reasoningSignature?: string } const mediaData = ProviderShared.mediaBytes @@ -181,14 +183,32 @@ const lowerToolConfig = (toolChoice: NonNullable) => const lowerUserPart = (part: TextPart | MediaPart) => part.type === "text" ? { text: part.text } : { inlineData: { mimeType: part.mediaType, data: mediaData(part) } } +const googleMetadata = (metadata: Record): ProviderMetadata => ({ google: metadata }) + +const thoughtSignature = (providerMetadata: ProviderMetadata | undefined) => { + const google = providerMetadata?.google + return ProviderShared.isRecord(google) && typeof google.thoughtSignature === "string" + ? google.thoughtSignature + : undefined +} + const lowerToolCall = (part: ToolCallPart) => ({ functionCall: { name: part.name, args: part.input }, + thoughtSignature: thoughtSignature(part.providerMetadata), }) const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMRequest) { const contents: GeminiContent[] = [] for (const message of request.messages) { + if (message.role === "system") { + const part = yield* ProviderShared.wrappedSystemUpdate("Gemini", message) + const previous = contents.at(-1) + if (previous?.role === "user") contents[contents.length - 1] = { role: "user", parts: [...previous.parts, { text: part.text }] } + else contents.push({ role: "user", parts: [{ text: part.text }] }) + continue + } + if (message.role === "user") { const parts: Array> = [] for (const part of message.content) { @@ -210,7 +230,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR continue } if (part.type === "reasoning") { - parts.push({ text: part.text, thought: true }) + parts.push({ text: part.text, thought: true, thoughtSignature: thoughtSignature(part.providerMetadata) }) continue } if (part.type === "tool-call") { @@ -326,7 +346,15 @@ const finish = (state: ParserState): ReadonlyArray => state.finishReason || state.usage ? (() => { const events: LLMEvent[] = [] - Lifecycle.finish(state.lifecycle, events, { + const lifecycle = state.reasoningSignature + ? Lifecycle.reasoningEnd( + state.lifecycle, + events, + "reasoning-0", + googleMetadata({ thoughtSignature: state.reasoningSignature }), + ) + : state.lifecycle + Lifecycle.finish(lifecycle, events, { reason: mapFinishReason(state.finishReason, state.hasToolCalls), usage: state.usage, }) @@ -350,11 +378,20 @@ const step = (state: ParserState, event: GeminiEvent) => { let hasToolCalls = nextState.hasToolCalls let lifecycle = nextState.lifecycle let nextToolCallId = nextState.nextToolCallId + let reasoningSignature = nextState.reasoningSignature for (const part of candidate.content.parts) { + if ("thoughtSignature" in part && part.thoughtSignature && "thought" in part && part.thought) + reasoningSignature = part.thoughtSignature if ("text" in part && part.text.length > 0) { lifecycle = part.thought - ? Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", part.text) + ? Lifecycle.reasoningDelta( + lifecycle, + events, + "reasoning-0", + part.text, + part.thoughtSignature ? googleMetadata({ thoughtSignature: part.thoughtSignature }) : undefined, + ) : Lifecycle.textDelta(lifecycle, events, "text-0", part.text) continue } @@ -363,7 +400,14 @@ const step = (state: ParserState, event: GeminiEvent) => { const input = part.functionCall.args const id = `tool_${nextToolCallId++}` lifecycle = Lifecycle.stepStart(lifecycle, events) - events.push(LLMEvent.toolCall({ id, name: part.functionCall.name, input })) + events.push( + LLMEvent.toolCall({ + id, + name: part.functionCall.name, + input, + providerMetadata: part.thoughtSignature ? googleMetadata({ thoughtSignature: part.thoughtSignature }) : undefined, + }), + ) hasToolCalls = true } } @@ -374,6 +418,7 @@ const step = (state: ParserState, event: GeminiEvent) => { hasToolCalls, lifecycle, nextToolCallId, + reasoningSignature, finishReason: candidate.finishReason ?? nextState.finishReason, }, events, diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index 6a85c37d59..e15a2b588e 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -1,4 +1,4 @@ -import { Array as Arr, Effect, Schema } from "effect" +import { Effect, Schema } from "effect" import { Route } from "../route/client" import { Auth } from "../route/auth" import { Endpoint } from "../route/endpoint" @@ -9,6 +9,7 @@ import { Usage, type FinishReason, type LLMRequest, + type ReasoningPart, type TextPart, type ToolCallPart, type ToolDefinition, @@ -202,14 +203,19 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func message: OpenAIChatRequestMessage, ) { const content: TextPart[] = [] + const reasoning: ReasoningPart[] = [] const toolCalls: OpenAIChatAssistantToolCall[] = [] for (const part of message.content) { - if (!ProviderShared.supportsContent(part, ["text", "tool-call"])) - return yield* ProviderShared.unsupportedContent("OpenAI Chat", "assistant", ["text", "tool-call"]) + if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"])) + return yield* ProviderShared.unsupportedContent("OpenAI Chat", "assistant", ["text", "reasoning", "tool-call"]) if (part.type === "text") { content.push(part) continue } + if (part.type === "reasoning") { + reasoning.push(part) + continue + } if (part.type === "tool-call") { toolCalls.push(lowerToolCall(part)) continue @@ -219,7 +225,8 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func role: "assistant" as const, content: content.length === 0 ? null : ProviderShared.joinText(content), tool_calls: toolCalls.length === 0 ? undefined : toolCalls, - reasoning_content: openAICompatibleReasoningContent(message.native?.openaiCompatible), + reasoning_content: + reasoning.length > 0 ? reasoning.map((part) => part.text).join("") : openAICompatibleReasoningContent(message.native?.openaiCompatible), } }) @@ -242,7 +249,19 @@ const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message: Op const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: LLMRequest) { const system: OpenAIChatMessage[] = request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] - return [...system, ...Arr.flatten(yield* Effect.forEach(request.messages, lowerMessage))] + const messages = [...system] + for (const message of request.messages) { + if (message.role === "system") { + const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message) + const previous = messages.at(-1) + if (previous?.role === "user") + messages[messages.length - 1] = { role: "user", content: `${previous.content}\n${part.text}` } + else messages.push({ role: "user", content: part.text }) + continue + } + messages.push(...(yield* lowerMessage(message))) + } + return messages }) const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) { diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index cc29f50190..99fc91a18f 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -291,6 +291,13 @@ const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | un } } +const hostedToolItemID = (part: ToolResultPart) => { + const openai = part.providerMetadata?.openai + return ProviderShared.isRecord(openai) && typeof openai.itemId === "string" && openai.itemId.length > 0 + ? openai.itemId + : undefined +} + const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ( part: LLMRequest["messages"][number]["content"][number], ) { @@ -332,6 +339,15 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ const store = OpenAIOptions.store(request) for (const message of request.messages) { + if (message.role === "system") { + const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Responses", message) + const previous = input.at(-1) + if (previous && "role" in previous && previous.role === "user") + input[input.length - 1] = { role: "user", content: [...previous.content, { type: "input_text", text: part.text }] } + else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] }) + continue + } + if (message.role === "user") { input.push({ role: "user", content: yield* Effect.forEach(message.content, lowerUserContent) }) continue @@ -341,6 +357,7 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ const content: TextPart[] = [] const reasoningItems: Record = {} const reasoningReferences = new Set() + const hostedToolReferences = new Set() const flushText = () => { if (content.length === 0) return input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) }) @@ -373,13 +390,22 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ } if (part.type === "tool-call") { flushText() + if (part.providerExecuted === true) continue input.push(lowerToolCall(part)) continue } + if (part.type === "tool-result" && part.providerExecuted === true) { + flushText() + const itemID = hostedToolItemID(part) + if (store !== false && itemID && !hostedToolReferences.has(itemID)) input.push({ type: "item_reference", id: itemID }) + if (itemID) hostedToolReferences.add(itemID) + continue + } return yield* ProviderShared.unsupportedContent("OpenAI Responses", "assistant", [ "text", "reasoning", "tool-call", + "tool-result", ]) } flushText() diff --git a/packages/llm/src/protocols/shared.ts b/packages/llm/src/protocols/shared.ts index aa37c62e43..a14fd20c54 100644 --- a/packages/llm/src/protocols/shared.ts +++ b/packages/llm/src/protocols/shared.ts @@ -9,6 +9,7 @@ import { type ContentPart, type LLMRequest, type MediaPart, + type TextPart, type ToolResultPart, } from "../schema" export { isRecord } from "../utils/record" @@ -104,6 +105,43 @@ export const parseJson = (route: string, input: string, message: string) => */ export const joinText = (parts: ReadonlyArray<{ readonly text: string }>) => parts.map((part) => part.text).join("\n") +const escapeSystemUpdateText = (text: string) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">") + +/** + * Stable fallback representation for chronological `Message.system(...)` + * updates on routes that do not support that privileged role natively. The + * wrapper remains visibly lower-authority user text, preserves the original + * temporal position, and XML-escapes content so it cannot close the wrapper. + */ +export const wrapSystemUpdate = (parts: ReadonlyArray<{ readonly text: string }>) => + `\n${escapeSystemUpdateText(joinText(parts))}\n` + +/** + * Chronological system updates deliberately accept text only. Do not insert + * raw retrieved, tool, or web content into privileged updates: keep untrusted + * data in ordinary user/tool messages instead. + */ +export const systemUpdateText = Effect.fn("ProviderShared.systemUpdateText")(function* ( + route: string, + message: LLMRequest["messages"][number], +) { + const content: TextPart[] = [] + for (const part of message.content) { + if (!supportsContent(part, ["text"])) return yield* unsupportedContent(route, "system", ["text"]) + content.push(part) + } + return content +}) + +/** Lower an unsupported privileged update into visible, in-order user text. */ +export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate")(function* ( + route: string, + message: LLMRequest["messages"][number], +) { + const content = yield* systemUpdateText(route, message) + return { type: "text" as const, text: wrapSystemUpdate(content), cache: content.at(-1)?.cache } +}) + /** * Parse the streamed JSON input of a tool call. Treats an empty string as * `"{}"` — providers occasionally finish a tool call without ever emitting diff --git a/packages/llm/src/protocols/utils/lifecycle.ts b/packages/llm/src/protocols/utils/lifecycle.ts index 21301df47a..eb6c95dfbd 100644 --- a/packages/llm/src/protocols/utils/lifecycle.ts +++ b/packages/llm/src/protocols/utils/lifecycle.ts @@ -36,8 +36,14 @@ export const reasoningStart = ( return { ...stepped, reasoning: new Set([...stepped.reasoning, id]) } } -export const reasoningDelta = (state: State, events: LLMEvent[], id: string, text: string): State => { - const started = reasoningStart(state, events, id) +export const reasoningDelta = ( + state: State, + events: LLMEvent[], + id: string, + text: string, + providerMetadata?: ProviderMetadata, +): State => { + const started = reasoningStart(state, events, id, providerMetadata) events.push(LLMEvent.reasoningDelta({ id, text })) return started } diff --git a/packages/llm/src/route/client.ts b/packages/llm/src/route/client.ts index 63993bc2b7..5b5bc5ab2d 100644 --- a/packages/llm/src/route/client.ts +++ b/packages/llm/src/route/client.ts @@ -10,8 +10,6 @@ import { WebSocketExecutor } from "./transport" import type { Protocol } from "./protocol" import { applyCachePolicy } from "../cache-policy" import * as ProviderShared from "../protocols/shared" -import * as ToolRuntime from "../tool-runtime" -import type { Tools } from "../tool" import type { LLMError, LLMEvent, PreparedRequestOf, ProtocolID, ProviderOptions } from "../schema" import { GenerationOptions, @@ -158,12 +156,10 @@ export interface Interface { export interface StreamMethod { (request: LLMRequest): Stream.Stream - (options: ToolRuntime.RunOptions): Stream.Stream } export interface GenerateMethod { (request: LLMRequest): Effect.Effect - (options: ToolRuntime.RunOptions): Effect.Effect } export class Service extends Context.Service()("@opencode/LLMClient") {} @@ -376,19 +372,10 @@ const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) = }), ) -const isToolRunOptions = (input: LLMRequest | ToolRuntime.RunOptions): input is ToolRuntime.RunOptions => - "request" in input && "tools" in input - -const streamWith = (streamRequest: (request: LLMRequest) => Stream.Stream): StreamMethod => - ((input: LLMRequest | ToolRuntime.RunOptions) => { - if (isToolRunOptions(input)) return ToolRuntime.stream({ ...input, stream: streamRequest }) - return streamRequest(input) - }) as StreamMethod - const generateWith = (stream: Interface["stream"]) => - Effect.fn("LLM.generate")(function* (input: LLMRequest | ToolRuntime.RunOptions) { + Effect.fn("LLM.generate")(function* (request: LLMRequest) { return new LLMResponse( - yield* stream(input as never).pipe( + yield* stream(request).pipe( Stream.runFold( () => ({ events: [] as LLMEvent[], usage: undefined as LLMResponse["usage"] }), (acc, event) => { @@ -404,22 +391,18 @@ const generateWith = (stream: Interface["stream"]) => export const prepare = (request: LLMRequest) => prepareWith(request) as Effect.Effect, LLMError> -export function stream(request: LLMRequest): Stream.Stream -export function stream(options: ToolRuntime.RunOptions): Stream.Stream -export function stream(input: LLMRequest | ToolRuntime.RunOptions) { +export function stream(request: LLMRequest): Stream.Stream { return Stream.unwrap( Effect.gen(function* () { - return (yield* Service).stream(input as never) + return (yield* Service).stream(request) }), - ) + ) as Stream.Stream } -export function generate(request: LLMRequest): Effect.Effect -export function generate(options: ToolRuntime.RunOptions): Effect.Effect -export function generate(input: LLMRequest | ToolRuntime.RunOptions) { +export function generate(request: LLMRequest): Effect.Effect { return Effect.gen(function* () { - return yield* (yield* Service).generate(input as never) - }) + return yield* (yield* Service).generate(request) + }) as Effect.Effect } export const streamRequest = (request: LLMRequest) => @@ -432,12 +415,10 @@ export const streamRequest = (request: LLMRequest) => export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { - const stream = streamWith( - streamRequestWith({ - http: yield* RequestExecutor.Service, - webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)), - }), - ) + const stream = streamRequestWith({ + http: yield* RequestExecutor.Service, + webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)), + }) return Service.of({ prepare: prepareWith as Interface["prepare"], stream, generate: generateWith(stream) }) }), ) @@ -450,5 +431,4 @@ export const LLMClient = { prepare, stream, generate, - stepCountIs: ToolRuntime.stepCountIs, } as const diff --git a/packages/llm/src/schema/events.ts b/packages/llm/src/schema/events.ts index dd3e6d0362..67ba6a9eb3 100644 --- a/packages/llm/src/schema/events.ts +++ b/packages/llm/src/schema/events.ts @@ -1,7 +1,7 @@ import { Schema } from "effect" import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids" import { ModelSchema } from "./options" -import { ToolResultValue } from "./messages" +import { ToolOutput, ToolResultValue } from "./messages" /** * Token usage reported by an LLM provider. @@ -163,6 +163,7 @@ export const ToolResult = Schema.Struct({ id: ToolCallID, name: Schema.String, result: ToolResultValue, + output: Schema.optional(ToolOutput), providerExecuted: Schema.optional(Schema.Boolean), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Event.ToolResult" }) @@ -252,7 +253,12 @@ export const LLMEvent = Object.assign(llmEventTagged, { ToolInputDelta.make({ ...input, id: toolCallID(input.id) }), toolInputEnd: (input: WithID) => ToolInputEnd.make({ ...input, id: toolCallID(input.id) }), toolCall: (input: WithID) => ToolCall.make({ ...input, id: toolCallID(input.id) }), - toolResult: (input: WithID) => ToolResult.make({ ...input, id: toolCallID(input.id) }), + toolResult: (input: WithID) => + ToolResult.make({ + ...input, + id: toolCallID(input.id), + output: input.output === undefined ? undefined : ToolOutput.make(input.output.structured, input.output.content), + }), toolError: (input: WithID) => ToolError.make({ ...input, id: toolCallID(input.id) }), stepFinish: (input: WithUsage) => StepFinish.make({ diff --git a/packages/llm/src/schema/ids.ts b/packages/llm/src/schema/ids.ts index ada133f0db..61289aa9d0 100644 --- a/packages/llm/src/schema/ids.ts +++ b/packages/llm/src/schema/ids.ts @@ -30,7 +30,7 @@ export type ReasoningEffort = Schema.Schema.Type export const TextVerbosity = Schema.Literals(["low", "medium", "high"]) export type TextVerbosity = Schema.Schema.Type -export const MessageRole = Schema.Literals(["user", "assistant", "tool"]) +export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"]) export type MessageRole = Schema.Schema.Type export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"]) diff --git a/packages/llm/src/schema/messages.ts b/packages/llm/src/schema/messages.ts index 9e19afce62..beff70fe2b 100644 --- a/packages/llm/src/schema/messages.ts +++ b/packages/llm/src/schema/messages.ts @@ -51,6 +51,56 @@ export type ToolResultMediaPart = Schema.Schema.Type export const ToolResultContentPart = Schema.Union([TextPart, ToolResultMediaPart]) export type ToolResultContentPart = Schema.Schema.Type +export class ToolTextContent extends Schema.Class("Tool.TextContent")({ + type: Schema.Literal("text"), + text: Schema.String, +}) {} + +export const ToolFileSource = Schema.Union([ + Schema.Struct({ type: Schema.Literal("data"), data: Schema.String }), + Schema.Struct({ type: Schema.Literal("url"), url: Schema.String }), + Schema.Struct({ type: Schema.Literal("file"), uri: Schema.String }), +]).pipe(Schema.toTaggedUnion("type")) +export type ToolFileSource = Schema.Schema.Type + +export class ToolFileContent extends Schema.Class("Tool.FileContent")({ + type: Schema.Literal("file"), + source: ToolFileSource, + mime: Schema.String, + name: Schema.optional(Schema.String), +}) {} + +/** Ordered, provider-independent content shown to models and UIs after a tool succeeds. */ +export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]).pipe(Schema.toTaggedUnion("type")) +export type ToolContent = Schema.Schema.Type + +export const toolText = (value: ConstructorParameters[0]) => new ToolTextContent(value) +export const toolFile = (value: ConstructorParameters[0]) => new ToolFileContent(value) + +const inlineData = (uri: string) => { + if (!uri.startsWith("data:")) return undefined + const match = /^data:[^;,]+;base64,(.*)$/s.exec(uri) + if (!match) throw new Error("Tool file data URI must contain raw base64 bytes") + return match[1]! +} + +const legacyInlineData = (value: string) => { + const data = inlineData(value) + if (data !== undefined) return data + if (/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) return value + throw new Error("Legacy tool-result media must contain raw base64 bytes or a base64 data URI") +} + +/** Convert a legacy attachment URI without guessing unknown string semantics. */ +export const toolFileSourceFromUri = (uri: string): ToolFileSource => { + const data = inlineData(uri) + if (data !== undefined) return { type: "data", data } + const url = URL.parse(uri) + if (url?.protocol === "file:") return { type: "file", uri } + if (url?.protocol === "http:" || url?.protocol === "https:") return { type: "url", url: uri } + throw new Error(`Unsupported tool file URI: ${uri}`) +} + const isToolResultValue = (value: unknown): value is ToolResultValue => isRecord(value) && (value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") && @@ -86,6 +136,80 @@ export const ToolResultValue = Object.assign( ) export type ToolResultValue = Schema.Schema.Type +export interface ToolOutput { + readonly structured: unknown + readonly content: ReadonlyArray +} + +export const ToolOutput = Object.assign( + Schema.Struct({ + structured: Schema.Unknown, + content: Schema.Array(ToolContent), + }).annotate({ identifier: "LLM.ToolOutput" }), + { + make: (structured: unknown, content: ReadonlyArray = []): ToolOutput => ({ + structured, + content: content.map((item) => + item.type === "text" + ? toolText({ type: "text", text: item.text }) + : toolFile({ type: "file", source: item.source, mime: item.mime, name: item.name }), + ), + }), + fromResultValue: (result: ToolResultValue): ToolOutput | undefined => { + switch (result.type) { + case "json": + return { structured: result.value, content: [] } + case "text": + return { structured: {}, content: [toolText({ type: "text", text: toolResultText(result.value) })] } + case "content": + return { + structured: {}, + content: result.value.map((item) => + item.type === "text" + ? toolText({ type: "text", text: item.text }) + : toolFile({ + type: "file", + source: { type: "data", data: legacyInlineData(item.data) }, + mime: item.mediaType, + name: item.filename, + }), + ), + } + case "error": + return undefined + } + }, + toResultValue: (output: ToolOutput): ToolResultValue => { + if (output.content.length === 0) return { type: "json", value: output.structured } + if (output.content.length === 1 && output.content[0]?.type === "text") + return { type: "text", value: output.content[0].text } + const unsupported = output.content.find((item) => item.type === "file" && item.source.type !== "data") + if (unsupported?.type === "file") + return { + type: "error", + value: `Tool file source "${unsupported.source.type}" must be materialized to inline data before provider conversion`, + } + return { + type: "content", + value: output.content.map((item) => { + if (item.type === "text") return { type: "text", text: item.text } + if (item.source.type !== "data") throw new Error("Unmaterialized tool file source reached provider conversion") + return { type: "media", mediaType: item.mime, data: item.source.data, filename: item.name } + }), + } + }, + }, +) + +const toolResultText = (value: unknown) => { + if (typeof value === "string") return value + try { + return JSON.stringify(value) ?? String(value) + } catch { + return String(value) + } +} + export const ToolCallPart = Object.assign( Schema.Struct({ type: Schema.Literal("tool-call"), @@ -157,6 +281,7 @@ export class Message extends Schema.Class("LLM.Message")({ export namespace Message { export type ContentInput = string | ContentPart | ReadonlyArray + export type SystemContentInput = string | TextPart | ReadonlyArray export type Input = Omit[0], "content"> & { readonly content: ContentInput } @@ -175,6 +300,14 @@ export namespace Message { export const assistant = (content: ContentInput) => make({ role: "assistant", content }) + /** + * Add an operator-authored instruction at this chronological point in the + * conversation. This is distinct from the initial `LLMRequest.system` + * prompt. Keep raw retrieved, tool, and web content out of privileged system + * updates; pass that untrusted content through ordinary user/tool channels. + */ + export const system = (content: SystemContentInput) => make({ role: "system", content }) + export const tool = (result: ToolResultPart | Parameters[0]) => make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] }) } @@ -183,6 +316,7 @@ export class ToolDefinition extends Schema.Class("LLM.ToolDefini name: Schema.String, description: Schema.String, inputSchema: JsonSchema, + outputSchema: Schema.optional(JsonSchema), cache: Schema.optional(CacheHint), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), diff --git a/packages/llm/src/tool-runtime.ts b/packages/llm/src/tool-runtime.ts index 4f6bc83407..c8d5e2d87b 100644 --- a/packages/llm/src/tool-runtime.ts +++ b/packages/llm/src/tool-runtime.ts @@ -1,340 +1,74 @@ -import { Effect, Stream } from "effect" -import type { Concurrency } from "effect/Types" -import { - type ContentPart, - type FinishReason, - type LLMError, - LLMEvent, - LLMRequest, - Message, - type ProviderMetadata, - ToolCallPart, - ToolFailure, - ToolResultPart, - ToolResultValue, - type ToolResultValue as ToolResultValueType, - Usage, -} from "./schema" -import { type AnyTool, type ExecutableTools, type Tools, toDefinitions } from "./tool" +import { Effect } from "effect" +import { LLMEvent, type ToolCallPart, ToolFailure, ToolOutput, ToolResultValue, type ToolOutput as ToolOutputType, type ToolResultValue as ToolResultValueType } from "./schema" +import { type AnyTool, type Tools } from "./tool" -export interface RuntimeState { - readonly step: number - readonly request: LLMRequest +export interface ToolSettlement { + readonly result: ToolResultValueType + readonly output?: ToolOutputType } -export type StopCondition = (state: RuntimeState) => boolean - -export type ToolExecution = "auto" | "none" - -interface RunOptionsBase { - readonly request: LLMRequest - readonly concurrency?: Concurrency - readonly stopWhen?: StopCondition +export interface DispatchResult extends ToolSettlement { + readonly events: ReadonlyArray } -export type RunOptions = RunOptionsAuto | RunOptionsNone - -export interface RunOptionsAuto extends RunOptionsBase { - readonly request: LLMRequest - readonly tools: T - readonly toolExecution?: "auto" -} - -export interface RunOptionsNone extends RunOptionsBase { - readonly request: LLMRequest - readonly tools: T - /** Advertise tool schemas but leave model-emitted tool calls for the caller. */ - readonly toolExecution: "none" -} - -export type StreamOptions = RunOptions & { - readonly stream: (request: LLMRequest) => Stream.Stream -} - -export const stepCountIs = - (count: number): StopCondition => - (state) => - state.step + 1 >= count - -/** - * Run a model with typed tools. This helper owns tool orchestration, while the - * caller supplies the actual model stream function. It can advertise schemas - * only (`toolExecution: "none"`), execute one step, or continue model rounds - * when `stopWhen` is provided. - */ -export const stream = (options: StreamOptions): Stream.Stream => { - const concurrency = options.concurrency ?? 10 - const tools = options.tools as Tools - const runtimeTools = toDefinitions(tools) - const runtimeToolNames = new Set(runtimeTools.map((tool) => tool.name)) - const initialRequest = - runtimeTools.length === 0 - ? options.request - : LLMRequest.update(options.request, { - tools: [...options.request.tools.filter((tool) => !runtimeToolNames.has(tool.name)), ...runtimeTools], - }) - - const loop = ( - request: LLMRequest, - step: number, - usage: Usage | undefined, - providerMetadata: ProviderMetadata | undefined, - ): Stream.Stream => - Stream.unwrap( - Effect.gen(function* () { - const state: StepState = { - assistantContent: [], - toolCalls: [], - finishReason: undefined, - usage: undefined, - providerMetadata: undefined, - } - - const modelStream = options - .stream(request) - .pipe(Stream.map((event) => indexStep(event, step))) - .pipe(Stream.tap((event) => Effect.sync(() => accumulate(state, event)))) - .pipe(Stream.filter((event) => event.type !== "finish")) - - const continuation = Stream.unwrap( - Effect.gen(function* () { - const totalUsage = addUsage(usage, state.usage) - const totalProviderMetadata = mergeProviderMetadata(providerMetadata, state.providerMetadata) - const finishStream = Stream.fromIterable([ - LLMEvent.finish({ - reason: state.finishReason ?? "unknown", - usage: totalUsage, - providerMetadata: totalProviderMetadata, - }), - ]) - - if (state.finishReason !== "tool-calls" || state.toolCalls.length === 0) return finishStream - if (options.toolExecution === "none") return finishStream - - const dispatched = yield* Effect.forEach( - state.toolCalls, - (call) => - dispatch(tools, call).pipe(Effect.map((result) => [call, result.result, result.error] as const)), - { concurrency }, - ) - const resultStream = Stream.fromIterable( - dispatched.flatMap(([call, result, error]) => emitEvents(call, result, error)), - ) - - if (!options.stopWhen) return resultStream.pipe(Stream.concat(finishStream)) - if (options.stopWhen({ step, request })) return resultStream.pipe(Stream.concat(finishStream)) - - return resultStream.pipe( - Stream.concat( - loop( - followUpRequest( - request, - state, - dispatched.map(([call, result]) => [call, result] as const), - ), - step + 1, - totalUsage, - totalProviderMetadata, - ), - ), - ) - }), - ) - - return modelStream.pipe(Stream.concat(continuation)) - }), - ) - - return loop(initialRequest, 0, undefined, undefined) -} - -const indexStep = (event: LLMEvent, index: number): LLMEvent => { - if (event.type === "step-start") return LLMEvent.stepStart({ index }) - if (event.type === "step-finish") return LLMEvent.stepFinish({ ...event, index }) - return event -} - -interface StepState { - assistantContent: ContentPart[] - toolCalls: ToolCallPart[] - finishReason: FinishReason | undefined - usage: Usage | undefined - providerMetadata: ProviderMetadata | undefined -} - -const accumulate = (state: StepState, event: LLMEvent) => { - if (event.type === "text-delta") { - appendStreamingText(state, "text", event.text, undefined) - return - } - if (event.type === "reasoning-delta") { - appendStreamingText(state, "reasoning", event.text, undefined) - return - } - if (event.type === "reasoning-end") { - appendStreamingText(state, "reasoning", "", event.providerMetadata) - return - } - if (event.type === "text-end") { - appendStreamingText(state, "text", "", event.providerMetadata) - return - } - if (event.type === "tool-call") { - const part = ToolCallPart.make({ - id: event.id, - name: event.name, - input: event.input, - providerExecuted: event.providerExecuted, - providerMetadata: event.providerMetadata, - }) - state.assistantContent.push(part) - if (!event.providerExecuted) state.toolCalls.push(part) - return - } - if (event.type === "tool-result" && event.providerExecuted) { - state.assistantContent.push( - ToolResultPart.make({ - id: event.id, - name: event.name, - result: event.result, - providerExecuted: true, - providerMetadata: event.providerMetadata, - }), - ) - return - } - if (event.type === "step-finish") { - state.finishReason = event.reason === "stop" && state.toolCalls.length > 0 ? "tool-calls" : event.reason - state.usage = addUsage(state.usage, event.usage) - state.providerMetadata = mergeProviderMetadata(state.providerMetadata, event.providerMetadata) - return - } - if (event.type === "finish") { - state.finishReason ??= event.reason - state.usage ??= event.usage - state.providerMetadata = mergeProviderMetadata(state.providerMetadata, event.providerMetadata) - } -} - -const addUsage = (left: Usage | undefined, right: Usage | undefined) => { - if (!left) return right - if (!right) return left - type UsageKey = - | "inputTokens" - | "outputTokens" - | "nonCachedInputTokens" - | "cacheReadInputTokens" - | "cacheWriteInputTokens" - | "reasoningTokens" - | "totalTokens" - const sum = (key: UsageKey) => - left[key] === undefined && right[key] === undefined ? undefined : (left[key] ?? 0) + (right[key] ?? 0) - - return new Usage({ - inputTokens: sum("inputTokens"), - outputTokens: sum("outputTokens"), - nonCachedInputTokens: sum("nonCachedInputTokens"), - cacheReadInputTokens: sum("cacheReadInputTokens"), - cacheWriteInputTokens: sum("cacheWriteInputTokens"), - reasoningTokens: sum("reasoningTokens"), - totalTokens: sum("totalTokens"), - providerMetadata: mergeProviderMetadata(left.providerMetadata, right.providerMetadata), - }) -} - -const sameProviderMetadata = (left: ProviderMetadata | undefined, right: ProviderMetadata | undefined) => - left === right || JSON.stringify(left) === JSON.stringify(right) - -const mergeProviderMetadata = (left: ProviderMetadata | undefined, right: ProviderMetadata | undefined) => { - if (!left) return right - if (!right) return left - return Object.fromEntries( - Array.from(new Set([...Object.keys(left), ...Object.keys(right)])).map((provider) => [ - provider, - { ...left[provider], ...right[provider] }, - ]), - ) -} - -const appendStreamingText = ( - state: StepState, - type: "text" | "reasoning", - text: string, - providerMetadata: ProviderMetadata | undefined, -) => { - const last = state.assistantContent.at(-1) - if (last?.type === type && text.length === 0) { - state.assistantContent[state.assistantContent.length - 1] = { - ...last, - providerMetadata: mergeProviderMetadata(last.providerMetadata, providerMetadata), - } - return - } - if (last?.type === type && sameProviderMetadata(last.providerMetadata, providerMetadata)) { - state.assistantContent[state.assistantContent.length - 1] = { ...last, text: `${last.text}${text}` } - return - } - state.assistantContent.push({ type, text, providerMetadata }) -} - -const dispatch = ( - tools: Tools, - call: ToolCallPart, -): Effect.Effect<{ result: ToolResultValueType; error?: unknown }> => { +/** Execute one canonical tool call without owning provider IO or continuation. */ +export const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect => { const tool = tools[call.name] - if (!tool) return Effect.succeed({ result: { type: "error" as const, value: `Unknown tool: ${call.name}` } }) + if (!tool) return Effect.succeed(result(call, { type: "error", value: `Unknown tool: ${call.name}` })) if (!tool.execute) - return Effect.succeed({ result: { type: "error" as const, value: `Tool has no execute handler: ${call.name}` } }) + return Effect.succeed(result(call, { type: "error", value: `Tool has no execute handler: ${call.name}` })) return decodeAndExecute(tool, call).pipe( + Effect.map((value) => result(call, value)), Effect.catchTag("LLM.ToolFailure", (failure) => - Effect.succeed({ - result: { type: "error" as const, value: failure.message } satisfies ToolResultValueType, - error: failure.error, - }), + Effect.succeed(result(call, { type: "error", value: failure.message }, failure.error)), ), - Effect.map((result) => ("result" in result ? result : { result })), ) } -const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect => +const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect => tool._decode(call.input).pipe( Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })), - Effect.flatMap((decoded) => tool.execute!(decoded, { id: call.id, name: call.name })), - Effect.flatMap((value) => - tool._encode(value).pipe( - Effect.mapError( - (error) => - new ToolFailure({ - message: `Tool returned an invalid value for its success schema: ${error.message}`, - }), + Effect.flatMap((decoded) => + tool.execute!(decoded, { id: call.id, name: call.name }).pipe( + Effect.flatMap((value) => + tool._encode(value).pipe( + Effect.mapError( + (error) => + new ToolFailure({ + message: `Tool returned an invalid value for its success schema: ${error.message}`, + }), + ), + ), ), + Effect.map((encoded) => { + if (tool._legacyResult && ToolResultValue.is(encoded)) + return { result: encoded, output: ToolOutput.fromResultValue(encoded) } + const output = tool._project(decoded, call.id, encoded) + const result = ToolOutput.toResultValue(output) + return result.type === "error" ? { result } : { result, output } + }), ), ), - Effect.map( - (encoded): ToolResultValueType => (ToolResultValue.is(encoded) ? encoded : { type: "json", value: encoded }), - ), ) -const emitEvents = (call: ToolCallPart, result: ToolResultValueType, error: unknown): ReadonlyArray => - result.type === "error" - ? [ - LLMEvent.toolError({ id: call.id, name: call.name, message: String(result.value), error }), - LLMEvent.toolResult({ id: call.id, name: call.name, result }), - ] - : [LLMEvent.toolResult({ id: call.id, name: call.name, result })] +const result = ( + call: ToolCallPart, + value: ToolResultValueType | ToolSettlement, + error?: unknown, +): DispatchResult => { + const settlement = ToolResultValue.is(value) ? { result: value } : value + return { + result: settlement.result, + output: settlement.output, + events: + settlement.result.type === "error" + ? [ + LLMEvent.toolError({ id: call.id, name: call.name, message: String(settlement.result.value), error }), + LLMEvent.toolResult({ id: call.id, name: call.name, result: settlement.result }), + ] + : [LLMEvent.toolResult({ id: call.id, name: call.name, result: settlement.result, output: settlement.output })], + } +} -const followUpRequest = ( - request: LLMRequest, - state: StepState, - dispatched: ReadonlyArray, -) => - LLMRequest.update(request, { - messages: [ - ...request.messages, - Message.assistant(state.assistantContent), - ...dispatched.map(([call, result]) => Message.tool({ id: call.id, name: call.name, result })), - ], - }) - -export const ToolRuntime = { stream, stepCountIs } as const +export const ToolRuntime = { dispatch } as const diff --git a/packages/llm/src/tool.ts b/packages/llm/src/tool.ts index df0a1cd3d3..5179d68252 100644 --- a/packages/llm/src/tool.ts +++ b/packages/llm/src/tool.ts @@ -1,6 +1,6 @@ import { Effect, JsonSchema, Schema } from "effect" -import type { ToolCallPart, ToolDefinition as ToolDefinitionClass } from "./schema" -import { ToolDefinition, ToolFailure } from "./schema" +import type { ToolCallPart, ToolContent, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema" +import { ToolDefinition, ToolFailure, ToolOutput, toolText } from "./schema" /** * Schema constraint for tool parameters / success values: no decoding or @@ -18,6 +18,16 @@ export type ToolExecute, Success extends Tool context?: ToolExecuteContext, ) => Effect.Effect, ToolFailure> +export interface ToolModelOutputInput { + readonly callID: ToolCallPart["id"] + readonly parameters: Parameters + readonly output: Output +} + +export type ToolToModelOutput, Success extends ToolSchema> = ( + input: ToolModelOutputInput, Success["Encoded"]>, +) => ReadonlyArray + /** * A type-safe LLM tool. Each tool bundles its own description, parameter * Schema and success Schema. The execute handler is optional: omit it when you @@ -28,22 +38,27 @@ export type ToolExecute, Success extends Tool * the stream. * * Internally each tool also carries memoized codecs and a precomputed - * `ToolDefinition` so the runtime doesn't rebuild them per invocation. + * `ToolDefinition` so callers do not rebuild them per invocation. */ export interface Tool, Success extends ToolSchema> { readonly description: string readonly parameters: Parameters readonly success: Success readonly execute?: ToolExecute + readonly toModelOutput?: ToolToModelOutput /** @internal */ readonly _decode: (input: unknown) => Effect.Effect, Schema.SchemaError> /** @internal */ readonly _encode: (value: Schema.Schema.Type) => Effect.Effect /** @internal */ + readonly _project: (parameters: Schema.Schema.Type, callID: ToolCallPart["id"], output: unknown) => ToolOutputType + /** @internal */ + readonly _legacyResult: boolean + /** @internal */ readonly _definition: ToolDefinitionClass } -export type AnyTool = Tool, ToolSchema> +export type AnyTool = Tool export type ExecutableTool, Success extends ToolSchema> = Tool< Parameters, @@ -52,7 +67,7 @@ export type ExecutableTool, Success extends T readonly execute: ToolExecute } -export type AnyExecutableTool = ExecutableTool, ToolSchema> +export type AnyExecutableTool = ExecutableTool export type ExecutableTools = Record @@ -61,12 +76,15 @@ type TypedToolConfig = { readonly parameters: ToolSchema readonly success: ToolSchema readonly execute?: ToolExecute, ToolSchema> + readonly toModelOutput?: ToolToModelOutput, ToolSchema> } type DynamicToolConfig = { readonly description: string readonly jsonSchema: JsonSchema.JsonSchema + readonly outputSchema?: JsonSchema.JsonSchema readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect + readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray } /** @@ -97,30 +115,36 @@ type DynamicToolConfig = { * }) * ``` * - * In both modes the produced tool flows through `toDefinitions(...)` and the - * runtime identically. + * In both modes the produced tool flows through `toDefinitions(...)` + * identically. */ export function make, Success extends ToolSchema>(config: { readonly description: string readonly parameters: Parameters readonly success: Success readonly execute: ToolExecute + readonly toModelOutput?: ToolToModelOutput }): ExecutableTool export function make, Success extends ToolSchema>(config: { readonly description: string readonly parameters: Parameters readonly success: Success readonly execute?: undefined + readonly toModelOutput?: ToolToModelOutput }): Tool export function make(config: { readonly description: string readonly jsonSchema: JsonSchema.JsonSchema + readonly outputSchema?: JsonSchema.JsonSchema readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect + readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray }): AnyExecutableTool export function make(config: { readonly description: string readonly jsonSchema: JsonSchema.JsonSchema + readonly outputSchema?: JsonSchema.JsonSchema readonly execute?: undefined + readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray }): AnyTool export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool { if ("jsonSchema" in config) { @@ -129,12 +153,16 @@ export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool { parameters: Schema.Unknown as ToolSchema, success: Schema.Unknown as ToolSchema, execute: config.execute, + toModelOutput: config.toModelOutput, _decode: Effect.succeed, _encode: Effect.succeed, + _project: (parameters, callID, output) => project(config.toModelOutput, parameters, callID, output), + _legacyResult: config.toModelOutput === undefined, _definition: new ToolDefinition({ name: "", description: config.description, inputSchema: config.jsonSchema, + outputSchema: config.outputSchema, }), } } @@ -143,18 +171,20 @@ export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool { parameters: config.parameters, success: config.success, execute: config.execute, + toModelOutput: config.toModelOutput, _decode: Schema.decodeUnknownEffect(config.parameters), _encode: Schema.encodeEffect(config.success), + _project: (parameters, callID, output) => project(config.toModelOutput, parameters, callID, output), + _legacyResult: false, _definition: new ToolDefinition({ name: "", description: config.description, inputSchema: toJsonSchema(config.parameters), + outputSchema: toJsonSchema(config.success), }), } } -export const tool = make - /** * A record of named tools. The record key becomes the tool name on the wire. */ @@ -162,8 +192,7 @@ export type Tools = Record /** * Convert a tools record into the `ToolDefinition[]` shape that - * `LLMRequest.tools` expects. The runtime calls this internally; consumers - * that build `LLMRequest` themselves can use it too. + * `LLMRequest.tools` expects. * * Tool names come from the record keys, so the per-tool cached * `_definition` is rebuilt with the correct name here. The JSON Schema body @@ -176,6 +205,7 @@ export const toDefinitions = (tools: Tools): ReadonlyArray name, description: item._definition.description, inputSchema: item._definition.inputSchema, + outputSchema: item._definition.outputSchema, }), ) @@ -185,6 +215,18 @@ const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => { return { ...document.schema, $defs: document.definitions } } +const project = ( + toModelOutput: ((input: ToolModelOutputInput) => ReadonlyArray) | undefined, + parameters: unknown, + callID: ToolCallPart["id"], + output: unknown, +): ToolOutputType => + ToolOutput.make( + output, + toModelOutput?.({ callID, parameters, output }) ?? + (typeof output === "string" ? [toolText({ type: "text", text: output })] : []), + ) + export { ToolFailure } export * as Tool from "./tool" diff --git a/packages/llm/test/lib/tool-runtime.ts b/packages/llm/test/lib/tool-runtime.ts index 5c98b78435..1808cd79ed 100644 --- a/packages/llm/test/lib/tool-runtime.ts +++ b/packages/llm/test/lib/tool-runtime.ts @@ -1,8 +1,142 @@ +import { Effect, Stream } from "effect" import { LLMClient } from "../../src/route" -import type { Tools } from "../../src/tool" -import type { RunOptions } from "../../src/tool-runtime" +import { + LLMEvent, + LLMRequest, + Message, + type ContentPart, + type ProviderMetadata, + type ToolCallPart, + ToolResultPart, + type ToolResultValue, + type Usage, +} from "../../src/schema" +import { type Tools, toDefinitions } from "../../src/tool" +import { ToolRuntime } from "../../src/tool-runtime" -type CompatRunOptions = RunOptions & { readonly maxSteps?: number } +interface RunOptions { + readonly request: LLMRequest + readonly tools: T + readonly maxSteps?: number +} -export const runTools = (options: CompatRunOptions) => - LLMClient.stream({ ...options, stopWhen: options.stopWhen ?? LLMClient.stepCountIs(options.maxSteps ?? 10) }) +/** Test-owned continuation loop. Production callers must own durable history. */ +export const runTools = (options: RunOptions) => + Stream.unwrap( + Effect.gen(function* () { + const names = new Set(Object.keys(options.tools)) + let request = LLMRequest.update(options.request, { + tools: [...options.request.tools.filter((tool) => !names.has(tool.name)), ...toDefinitions(options.tools)], + }) + let usage: Usage | undefined + const events: LLMEvent[] = [] + + for (let step = 0; step < (options.maxSteps ?? 10); step++) { + const streamed = Array.from(yield* LLMClient.stream(request).pipe(Stream.runCollect)) + const state = stepState(streamed) + usage = addUsage(usage, state.usage) + events.push(...streamed.filter((event) => event.type !== "finish").map((event) => indexStep(event, step))) + + if (state.toolCalls.length === 0) { + events.push(LLMEvent.finish({ reason: state.reason, usage, providerMetadata: state.providerMetadata })) + return Stream.fromIterable(events) + } + + const dispatched = yield* Effect.forEach( + state.toolCalls, + (call) => ToolRuntime.dispatch(options.tools, call).pipe(Effect.map((result) => [call, result] as const)), + { concurrency: 10 }, + ) + events.push(...dispatched.flatMap(([, result]) => result.events)) + + if (step + 1 >= (options.maxSteps ?? 10)) { + events.push(LLMEvent.finish({ reason: state.reason, usage, providerMetadata: state.providerMetadata })) + return Stream.fromIterable(events) + } + + request = LLMRequest.update(request, { + messages: [ + ...request.messages, + Message.assistant(state.assistantContent), + ...dispatched.map(([call, dispatched]) => + Message.tool({ id: call.id, name: call.name, result: dispatched.result }), + ), + ], + }) + } + + return Stream.fromIterable(events) + }), + ) + +const indexStep = (event: LLMEvent, index: number): LLMEvent => { + if (event.type === "step-start") return LLMEvent.stepStart({ index }) + if (event.type === "step-finish") return LLMEvent.stepFinish({ ...event, index }) + return event +} + +const stepState = (events: ReadonlyArray) => { + const assistantContent: ContentPart[] = [] + const toolCalls: ToolCallPart[] = [] + let reason: Extract["reason"] = "unknown" + let usage: Usage | undefined + let providerMetadata: ProviderMetadata | undefined + + for (const event of events) { + if (event.type === "text-delta" || event.type === "reasoning-delta") { + appendText(assistantContent, event.type === "text-delta" ? "text" : "reasoning", event.text) + } else if (event.type === "text-end" || event.type === "reasoning-end") { + appendText(assistantContent, event.type === "text-end" ? "text" : "reasoning", "", event.providerMetadata) + } else if (event.type === "tool-call") { + assistantContent.push(event) + if (!event.providerExecuted) toolCalls.push(event) + } else if (event.type === "tool-result" && event.providerExecuted && event.result !== undefined) { + assistantContent.push( + ToolResultPart.make({ + id: event.id, + name: event.name, + result: event.result, + providerExecuted: true, + providerMetadata: event.providerMetadata, + }), + ) + } else if (event.type === "finish") { + reason = event.reason + usage = event.usage + providerMetadata = event.providerMetadata + } + } + return { assistantContent, toolCalls, reason, usage, providerMetadata } +} + +const appendText = ( + content: ContentPart[], + type: "text" | "reasoning", + text: string, + providerMetadata?: ProviderMetadata, +) => { + const last = content.at(-1) + if (last?.type === type) { + content[content.length - 1] = { ...last, text: `${last.text}${text}`, providerMetadata: providerMetadata ?? last.providerMetadata } + return + } + content.push({ type, text, providerMetadata }) +} + +const addUsage = (left: Usage | undefined, right: Usage | undefined): Usage | undefined => { + if (!left) return right + if (!right) return left + const sum = (key: keyof Usage) => + typeof left[key] !== "number" && typeof right[key] !== "number" + ? undefined + : ((left[key] as number | undefined) ?? 0) + ((right[key] as number | undefined) ?? 0) + return { + inputTokens: sum("inputTokens"), + outputTokens: sum("outputTokens"), + nonCachedInputTokens: sum("nonCachedInputTokens"), + cacheReadInputTokens: sum("cacheReadInputTokens"), + cacheWriteInputTokens: sum("cacheWriteInputTokens"), + reasoningTokens: sum("reasoningTokens"), + totalTokens: sum("totalTokens"), + } as Usage +} diff --git a/packages/llm/test/llm.test.ts b/packages/llm/test/llm.test.ts index 007b602ce3..22329986ca 100644 --- a/packages/llm/test/llm.test.ts +++ b/packages/llm/test/llm.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { LLM, LLMResponse } from "../src" +import { CacheHint, LLM, LLMResponse } from "../src" import * as OpenAIChat from "../src/protocols/openai-chat" import * as OpenAIResponses from "../src/protocols/openai-responses" import { LLMRequest, Message, Model, ToolCallPart, ToolChoice, ToolDefinition, ToolResultPart } from "../src/schema" @@ -135,6 +135,23 @@ describe("llm constructors", () => { ]) }) + test("builds chronological text-only system updates separately from the initial system prompt", () => { + const update = Message.system([{ type: "text", text: "Use parameterized SQL.", cache: new CacheHint({ type: "ephemeral" }) }]) + const request = LLM.request({ + model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }), + system: "Initial operator prompt.", + messages: [Message.user("Review this."), update], + }) + + expect(update).toBeInstanceOf(Message) + expect(update).toEqual({ + role: "system", + content: [{ type: "text", text: "Use parameterized SQL.", cache: { type: "ephemeral" } }], + }) + expect(request.system).toEqual([{ type: "text", text: "Initial operator prompt." }]) + expect(request.messages.map((message) => message.role)).toEqual(["user", "system"]) + }) + test("extracts output text from response events", () => { expect( LLMResponse.text({ diff --git a/packages/llm/test/provider/anthropic-messages.test.ts b/packages/llm/test/provider/anthropic-messages.test.ts index 5198af9ab7..c55316c814 100644 --- a/packages/llm/test/provider/anthropic-messages.test.ts +++ b/packages/llm/test/provider/anthropic-messages.test.ts @@ -13,6 +13,10 @@ const model = AnthropicMessages.route .with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") }) .model({ id: "claude-sonnet-4-5" }) +const opus48 = AnthropicMessages.route + .with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") }) + .model({ id: "claude-opus-4-8" }) + const request = LLM.request({ id: "req_1", model, @@ -53,6 +57,93 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: opus48, + messages: [ + Message.user("Before."), + Message.system([{ type: "text", text: "Operator update.", cache: new CacheHint({ type: "ephemeral" }) }]), + Message.assistant("After."), + ], + cache: "none", + }), + ) + + expect(prepared.body.messages).toEqual([ + { role: "user", content: [{ type: "text", text: "Before." }] }, + { + role: "system", + content: [{ type: "text", text: "Operator update.", cache_control: { type: "ephemeral" } }], + }, + { role: "assistant", content: [{ type: "text", text: "After." }] }, + ]) + }), + ) + + it.effect("lowers chronological system updates to wrapped user text for unsupported Anthropic models", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [Message.user("Before."), Message.system("Treat literally."), Message.assistant("After.")], + cache: "none", + }), + ) + + expect(prepared.body.messages).toEqual([ + { + role: "user", + content: [ + { type: "text", text: "Before." }, + { type: "text", text: "\nTreat </system-update> literally.\n" }, + ], + }, + { role: "assistant", content: [{ type: "text", text: "After." }] }, + ]) + }), + ) + + it.effect("rejects non-text chronological system update content before send", () => + Effect.gen(function* () { + const error = yield* LLMClient.prepare( + LLM.request({ + model: opus48, + messages: [ + Message.user("Before."), + Message.make({ role: "system", content: { type: "media", mediaType: "image/png", data: "AAECAw==" } }), + ], + }), + ).pipe(Effect.flip) + + expect(error.message).toContain("Anthropic Messages system messages only support text content for now") + }), + ) + + it.effect("rejects invalid native chronological system update placement", () => + Effect.gen(function* () { + const placementError = (messages: Parameters[0]["messages"]) => + LLMClient.prepare(LLM.request({ model: opus48, messages, cache: "none" })).pipe(Effect.flip) + + expect((yield* placementError([Message.system("First.")])).message).toContain("cannot be the first message") + expect((yield* placementError([Message.user("Before."), Message.system("One."), Message.system("Two.")])).message) + .toContain("cannot be consecutive") + expect((yield* placementError([Message.assistant("Plain."), Message.system("After plain assistant.")])).message) + .toContain("must follow a user message, tool result, or assistant server tool use") + expect( + ( + yield* placementError([ + Message.user("Use the tool."), + Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]), + Message.system("Too early."), + Message.tool({ id: "call_1", name: "lookup", result: "Done." }), + ]) + ).message, + ).toContain("cannot appear between a local tool call and its tool result") + }), + ) + it.effect("prepares tool call and tool result messages", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( diff --git a/packages/llm/test/provider/bedrock-converse.test.ts b/packages/llm/test/provider/bedrock-converse.test.ts index a3d8c5c626..effd208627 100644 --- a/packages/llm/test/provider/bedrock-converse.test.ts +++ b/packages/llm/test/provider/bedrock-converse.test.ts @@ -5,6 +5,7 @@ import { Effect } from "effect" import { CacheHint, LLM, Message, ToolCallPart, ToolChoice } from "../../src" import { LLMClient } from "../../src/route" import { AmazonBedrock } from "../../src/providers" +import * as BedrockConverse from "../../src/protocols/bedrock-converse" import { it } from "../lib/effect" import { fixedResponse } from "../lib/http" import { @@ -82,6 +83,23 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("lowers chronological system updates to wrapped user text in order", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")], + cache: "none", + }), + ) + + expect(prepared.body.messages).toEqual([ + { role: "user", content: [{ text: "Before." }, { text: "\nUpdate.\n" }] }, + { role: "assistant", content: [{ text: "After." }] }, + ]) + }), + ) + it.effect("prepares tool config with toolSpec and toolChoice", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( @@ -279,6 +297,41 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("preserves streamed reasoning signatures for continuation lowering", () => + Effect.gen(function* () { + const body = eventStreamBody( + ["messageStart", { role: "assistant" }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + ) + const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body))) + const reasoning = response.events.find((event) => event.type === "reasoning-end") + + expect(reasoning).toEqual({ + type: "reasoning-end", + id: "reasoning-0", + providerMetadata: { bedrock: { signature: "sig_1" } }, + }) + + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [ + Message.assistant([ + { type: "reasoning", text: "Let me think.", providerMetadata: reasoning?.providerMetadata }, + ]), + ], + cache: "none", + }), + ) + expect(prepared.body.messages).toEqual([ + { role: "assistant", content: [{ reasoningContent: { reasoningText: { text: "Let me think.", signature: "sig_1" } } }] }, + ]) + }), + ) + it.effect("emits provider-error for throttlingException", () => Effect.gen(function* () { const body = eventStreamBody( diff --git a/packages/llm/test/provider/gemini.test.ts b/packages/llm/test/provider/gemini.test.ts index 9e519723f1..db578da1bc 100644 --- a/packages/llm/test/provider/gemini.test.ts +++ b/packages/llm/test/provider/gemini.test.ts @@ -35,6 +35,22 @@ describe("Gemini route", () => { }), ) + it.effect("lowers chronological system updates to wrapped user text in order", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")], + }), + ) + + expect(prepared.body.contents).toEqual([ + { role: "user", parts: [{ text: "Before." }, { text: "\nUpdate.\n" }] }, + { role: "model", parts: [{ text: "After." }] }, + ]) + }), + ) + it.effect("prepares multimodal user input and tool history", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( @@ -241,6 +257,72 @@ describe("Gemini route", () => { }), ) + it.effect("preserves thoughtSignature for reasoning and tool-call continuation", () => + Effect.gen(function* () { + const body = sseEvents({ + candidates: [ + { + content: { + role: "model", + parts: [ + { text: "thinking", thought: true }, + { text: "", thought: true, thoughtSignature: "thought_sig" }, + { functionCall: { name: "lookup", args: { query: "weather" } }, thoughtSignature: "tool_sig" }, + ], + }, + finishReason: "STOP", + }, + ], + }) + const response = yield* LLMClient.generate( + LLM.updateRequest(request, { + tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + }), + ).pipe(Effect.provide(fixedResponse(body))) + const reasoning = response.events.find((event) => event.type === "reasoning-start") + const reasoningEnd = response.events.find((event) => event.type === "reasoning-end") + const toolCall = response.events.find((event) => event.type === "tool-call") + + expect(reasoning).toEqual({ + type: "reasoning-start", + id: "reasoning-0", + providerMetadata: undefined, + }) + expect(reasoningEnd).toEqual({ + type: "reasoning-end", + id: "reasoning-0", + providerMetadata: { google: { thoughtSignature: "thought_sig" } }, + }) + expect(toolCall).toMatchObject({ providerMetadata: { google: { thoughtSignature: "tool_sig" } } }) + + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [ + Message.assistant([ + { type: "reasoning", text: "thinking", providerMetadata: reasoningEnd?.providerMetadata }, + ToolCallPart.make({ + id: "tool_0", + name: "lookup", + input: { query: "weather" }, + providerMetadata: toolCall?.providerMetadata, + }), + ]), + ], + }), + ) + expect(prepared.body.contents).toEqual([ + { + role: "model", + parts: [ + { text: "thinking", thought: true, thoughtSignature: "thought_sig" }, + { functionCall: { name: "lookup", args: { query: "weather" } }, thoughtSignature: "tool_sig" }, + ], + }, + ]) + }), + ) + it.effect("emits streamed tool calls and maps finish reason", () => Effect.gen(function* () { const body = sseEvents({ diff --git a/packages/llm/test/provider/openai-chat.test.ts b/packages/llm/test/provider/openai-chat.test.ts index 5d1b412bfa..3048ca7f62 100644 --- a/packages/llm/test/provider/openai-chat.test.ts +++ b/packages/llm/test/provider/openai-chat.test.ts @@ -50,6 +50,35 @@ describe("OpenAI Chat route", () => { }), ) + it.effect("lowers chronological system updates to escaped user wrappers in order", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [Message.user("Before."), Message.system("Treat & data literally."), Message.assistant("After.")], + }), + ) + + expect(prepared.body.messages).toEqual([ + { role: "user", content: "Before.\n\nTreat <admin> & data literally.\n" }, + { role: "assistant", content: "After." }, + ]) + }), + ) + + it.effect("replays canonical reasoning as OpenAI-compatible reasoning_content", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [Message.assistant([{ type: "reasoning", text: "thinking" }, { type: "text", text: "Hello" }])], + }), + ) + + expect(prepared.body.messages).toEqual([{ role: "assistant", content: "Hello", reasoning_content: "thinking" }]) + }), + ) + it.effect("maps OpenAI provider options to Chat options", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( @@ -196,17 +225,17 @@ describe("OpenAI Chat route", () => { }), ) - it.effect("rejects unsupported assistant reasoning content", () => + it.effect("lowers reasoning-only assistant history", () => Effect.gen(function* () { - const error = yield* LLMClient.prepare( + const prepared = yield* LLMClient.prepare( LLM.request({ id: "req_reasoning", model, messages: [Message.assistant({ type: "reasoning", text: "hidden" })], }), - ).pipe(Effect.flip) + ) - expect(error.message).toContain("OpenAI Chat assistant messages only support text and tool-call content for now") + expect(prepared.body.messages).toEqual([{ role: "assistant", content: null, reasoning_content: "hidden" }]) }), ) diff --git a/packages/llm/test/provider/openai-responses.test.ts b/packages/llm/test/provider/openai-responses.test.ts index dd57d00a91..3f8cda99c1 100644 --- a/packages/llm/test/provider/openai-responses.test.ts +++ b/packages/llm/test/provider/openai-responses.test.ts @@ -57,6 +57,28 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("lowers chronological system updates to escaped user wrappers in order", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [Message.user("Before."), Message.system("Treat literally."), Message.assistant("After.")], + }), + ) + + expect(prepared.body.input).toEqual([ + { + role: "user", + content: [ + { type: "input_text", text: "Before." }, + { type: "input_text", text: "\nTreat </system-update> literally.\n" }, + ], + }, + { role: "assistant", content: [{ type: "output_text", text: "After." }] }, + ]) + }), + ) + it.effect("prepares OpenAI Responses WebSocket target", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( @@ -857,6 +879,42 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("references stored provider-executed hosted tool results by id", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [ + Message.assistant([ + ToolCallPart.make({ + id: "ws_1", + name: "web_search", + input: { query: "effect 4" }, + providerExecuted: true, + providerMetadata: { openai: { itemId: "ws_1" } }, + }), + { + type: "tool-result", + id: "ws_1", + name: "web_search", + result: { type: "json", value: { type: "web_search_call", id: "ws_1", status: "completed" } }, + providerExecuted: true, + providerMetadata: { openai: { itemId: "ws_1" } }, + }, + ]), + Message.user("Continue."), + ], + providerOptions: { openai: { store: true } }, + }), + ) + + expect(prepared.body.input).toEqual([ + { type: "item_reference", id: "ws_1" }, + { role: "user", content: [{ type: "input_text", text: "Continue." }] }, + ]) + }), + ) + it.effect("joins streamed summary blocks into one continuation reasoning item", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( diff --git a/packages/llm/test/recorded-scenarios.ts b/packages/llm/test/recorded-scenarios.ts index db28ec4493..1fc01ae76e 100644 --- a/packages/llm/test/recorded-scenarios.ts +++ b/packages/llm/test/recorded-scenarios.ts @@ -5,15 +5,17 @@ import { LLMEvent, LLMResponse, Message, + ToolRuntime, ToolChoice, ToolDefinition, + toDefinitions, type ContentPart, type FinishReason, type LLMRequest, type Model, } from "../src" import { LLMClient } from "../src/route" -import { tool } from "../src/tool" +import { Tool } from "../src/tool" export const weatherToolName = "get_weather" @@ -40,7 +42,7 @@ export const weatherTool = ToolDefinition.make({ }, }) -export const weatherRuntimeTool = tool({ +export const weatherRuntimeTool = Tool.make({ description: weatherTool.description, parameters: Schema.Struct({ city: Schema.String }), success: Schema.Struct({ temperature: Schema.Number, condition: Schema.String }), @@ -87,14 +89,60 @@ const restroomImage = () => ) export const runWeatherToolLoop = (request: LLMRequest) => - LLMClient.stream({ - request, - tools: { [weatherToolName]: weatherRuntimeTool }, - stopWhen: LLMClient.stepCountIs(10), - }).pipe( - Stream.runCollect, - Effect.map((events) => Array.from(events)), - ) + Effect.gen(function* () { + const tools = { [weatherToolName]: weatherRuntimeTool } + let next = LLM.updateRequest(request, { tools: toDefinitions(tools) }) + const events: LLMEvent[] = [] + + for (let step = 0; step < 10; step++) { + const response = yield* LLMClient.generate(next) + events.push(...response.events.filter((event) => event.type !== "finish")) + const calls = response.events.filter(LLMEvent.is.toolCall).filter((call) => !call.providerExecuted) + if (calls.length === 0) { + const finish = response.events.find(LLMEvent.is.finish) + if (finish) events.push(finish) + return events + } + + const dispatched = yield* Effect.forEach(calls, (call) => + ToolRuntime.dispatch(tools, call).pipe(Effect.map((result) => [call, result] as const)), + ) + events.push(...dispatched.flatMap(([, result]) => result.events)) + next = LLM.updateRequest(next, { + messages: [ + ...next.messages, + Message.assistant(assistantContent(response.events)), + ...dispatched.map(([call, result]) => Message.tool({ id: call.id, name: call.name, result: result.result })), + ], + }) + } + + throw new Error("Weather tool loop exceeded 10 steps") + }) + +const assistantContent = (events: ReadonlyArray) => { + const content: ContentPart[] = [] + for (const event of events) { + if (event.type === "text-delta" || event.type === "reasoning-delta") { + const type = event.type === "text-delta" ? "text" : "reasoning" + const last = content.at(-1) + if (last?.type === type) { + content[content.length - 1] = { ...last, text: `${last.text}${event.text}` } + } else { + content.push({ type, text: event.text }) + } + continue + } + if (event.type === "text-end" || event.type === "reasoning-end") { + const type = event.type === "text-end" ? "text" : "reasoning" + const last = content.at(-1) + if (last?.type === type) content[content.length - 1] = { ...last, providerMetadata: event.providerMetadata } + continue + } + if (event.type === "tool-call") content.push(event) + } + return content +} export const expectFinish = ( events: ReadonlyArray, diff --git a/packages/llm/test/schema.test.ts b/packages/llm/test/schema.test.ts index 3c6628c2e5..ff1bc448b9 100644 --- a/packages/llm/test/schema.test.ts +++ b/packages/llm/test/schema.test.ts @@ -43,6 +43,17 @@ describe("llm schema", () => { expect(decoded.model.route.id).toBe("openai-responses") }) + test("decodes chronological system messages", () => { + const decoded = decodeLLMRequest({ + model, + system: [], + messages: [{ role: "system", content: [{ type: "text", text: "Operator update." }] }], + tools: [], + }) + + expect(decoded.messages[0]).toMatchObject({ role: "system", content: [{ type: "text", text: "Operator update." }] }) + }) + test("rejects invalid event type", () => { expect(() => decodeLLMEvent({ type: "bogus" })).toThrow() }) diff --git a/packages/llm/test/tool-runtime.test.ts b/packages/llm/test/tool-runtime.test.ts index 6c85e2d38c..47adcb42ae 100644 --- a/packages/llm/test/tool-runtime.test.ts +++ b/packages/llm/test/tool-runtime.test.ts @@ -1,11 +1,11 @@ import { describe, expect } from "bun:test" import { Effect, Schema, Stream } from "effect" -import { GenerationOptions, LLM, LLMEvent, LLMRequest, LLMResponse, ToolChoice } from "../src" +import { GenerationOptions, LLM, LLMEvent, LLMRequest, LLMResponse, ToolChoice, ToolContent, ToolOutput, toolFileSourceFromUri, toDefinitions } from "../src" import { Auth, LLMClient } from "../src/route" import * as AnthropicMessages from "../src/protocols/anthropic-messages" import * as OpenAIChat from "../src/protocols/openai-chat" import * as OpenAIResponses from "../src/protocols/openai-responses" -import { tool, ToolFailure, type ToolExecuteContext } from "../src/tool" +import { Tool, ToolFailure, type ToolExecuteContext } from "../src/tool" import { ToolRuntime } from "../src/tool-runtime" import { it } from "./lib/effect" import * as TestToolRuntime from "./lib/tool-runtime" @@ -26,7 +26,7 @@ const baseRequest = LLM.request({ }) const weatherFailureCause = new Error("weather lookup denied") -const get_weather = tool({ +const get_weather = Tool.make({ description: "Get current weather for a city.", parameters: Schema.Struct({ city: Schema.String }), success: Schema.Struct({ temperature: Schema.Number, condition: Schema.String }), @@ -38,7 +38,7 @@ const get_weather = tool({ }), }) -const schema_only_weather = tool({ +const schema_only_weather = Tool.make({ description: "Get current weather for a city.", parameters: Schema.Struct({ city: Schema.String }), success: Schema.Struct({ temperature: Schema.Number, condition: Schema.String }), @@ -140,9 +140,161 @@ describe("LLMClient tools", () => { }), ) + it.effect("projects encoded typed tool success into canonical model content", () => + Effect.gen(function* () { + const calls: unknown[] = [] + const projected = Tool.make({ + description: "Project an encoded success.", + parameters: Schema.Struct({ prefix: Schema.String }), + success: Schema.Struct({ count: Schema.NumberFromString }), + execute: () => Effect.succeed({ count: 2 }), + toModelOutput: (input) => { + calls.push(input) + return [{ type: "text", text: `${input.parameters.prefix}:${input.output.count}` }] + }, + }) + + const dispatched = yield* ToolRuntime.dispatch( + { projected }, + LLMEvent.toolCall({ id: "call_projected", name: "projected", input: { prefix: "count" } }), + ) + + expect(calls).toEqual([{ callID: "call_projected", parameters: { prefix: "count" }, output: { count: "2" } }]) + expect(dispatched.result).toEqual({ type: "text", value: "count:2" }) + expect(dispatched.output).toEqual({ structured: { count: "2" }, content: [{ type: "text", text: "count:2" }] }) + expect(dispatched.events).toEqual([ + LLMEvent.toolResult({ + id: "call_projected", + name: "projected", + result: { type: "text", value: "count:2" }, + output: { structured: { count: "2" }, content: [{ type: "text", text: "count:2" }] }, + }), + ]) + }), + ) + + it.effect("uses the narrow default projection for encoded typed success", () => + Effect.gen(function* () { + const text = Tool.make({ + description: "Return text.", + parameters: Schema.Struct({}), + success: Schema.String, + execute: () => Effect.succeed("hello"), + }) + const json = Tool.make({ + description: "Return JSON.", + parameters: Schema.Struct({}), + success: Schema.Struct({ ok: Schema.Boolean }), + execute: () => Effect.succeed({ ok: true }), + }) + + expect((yield* ToolRuntime.dispatch({ text }, LLMEvent.toolCall({ id: "call_text", name: "text", input: {} }))).output) + .toEqual({ structured: "hello", content: [{ type: "text", text: "hello" }] }) + expect((yield* ToolRuntime.dispatch({ json }, LLMEvent.toolCall({ id: "call_json", name: "json", input: {} }))).output) + .toEqual({ structured: { ok: true }, content: [] }) + }), + ) + + it.effect("models canonical tool files with explicit data, url, and file sources", () => + Effect.sync(() => { + const decode = Schema.decodeUnknownSync(ToolContent) + + expect(decode({ type: "file", source: { type: "data", data: "AAAA" }, mime: "image/png" })).toEqual({ + type: "file", + source: { type: "data", data: "AAAA" }, + mime: "image/png", + }) + expect(decode({ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" })).toEqual({ + type: "file", + source: { type: "url", url: "https://example.test/image.png" }, + mime: "image/png", + }) + expect(decode({ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" })).toEqual({ + type: "file", + source: { type: "file", uri: "file:///tmp/image.png" }, + mime: "image/png", + }) + }), + ) + + it.effect("converts canonical data files deliberately and rejects unmaterialized sources", () => + Effect.sync(() => { + expect( + ToolOutput.toResultValue( + ToolOutput.make({}, [{ type: "file", source: { type: "data", data: "AAAA" }, mime: "image/png" }]), + ), + ).toEqual({ type: "content", value: [{ type: "media", mediaType: "image/png", data: "AAAA" }] }) + expect( + ToolOutput.toResultValue( + ToolOutput.make({}, [{ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" }]), + ), + ).toEqual({ type: "error", value: 'Tool file source "url" must be materialized to inline data before provider conversion' }) + expect( + ToolOutput.toResultValue( + ToolOutput.make({}, [{ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" }]), + ), + ).toEqual({ type: "error", value: 'Tool file source "file" must be materialized to inline data before provider conversion' }) + expect(toolFileSourceFromUri("data:image/png;base64,AAAA")).toEqual({ type: "data", data: "AAAA" }) + expect(toolFileSourceFromUri("https://example.test/image.png")).toEqual({ type: "url", url: "https://example.test/image.png" }) + expect(toolFileSourceFromUri("file:///tmp/image.png")).toEqual({ type: "file", uri: "file:///tmp/image.png" }) + expect(() => toolFileSourceFromUri("opaque-value")).toThrow("Unsupported tool file URI") + expect(() => + ToolOutput.fromResultValue({ + type: "content", + value: [{ type: "media", mediaType: "image/png", data: "https://example.test/image.png" }], + }), + ).toThrow("Legacy tool-result media must contain raw base64 bytes or a base64 data URI") + }), + ) + + it.effect("settles projected url files as materialization errors", () => + Effect.gen(function* () { + const remote = Tool.make({ + description: "Return a remote file.", + parameters: Schema.Struct({}), + success: Schema.Struct({ ok: Schema.Boolean }), + execute: () => Effect.succeed({ ok: true }), + toModelOutput: () => [ + { type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" }, + ], + }) + + const dispatched = yield* ToolRuntime.dispatch( + { remote }, + LLMEvent.toolCall({ id: "call_remote", name: "remote", input: {} }), + ) + + expect(dispatched.output).toBeUndefined() + expect(dispatched.result).toEqual({ + type: "error", + value: 'Tool file source "url" must be materialized to inline data before provider conversion', + }) + expect(dispatched.events.map((event) => event.type)).toEqual(["tool-error", "tool-result"]) + }), + ) + + it.effect("derives typed output schemas and preserves dynamic output schemas", () => + Effect.sync(() => { + const [typed] = toDefinitions({ get_weather }) + const schema = { type: "object", properties: { result: { type: "string" } } } as const + const [dynamic] = toDefinitions({ + dynamic: Tool.make({ description: "Dynamic tool.", jsonSchema: { type: "object" }, outputSchema: schema }), + }) + + expect(typed?.outputSchema).toMatchObject({ + type: "object", + properties: { condition: { type: "string" } }, + required: ["temperature", "condition"], + additionalProperties: false, + }) + expect(Reflect.get(Reflect.get(typed?.outputSchema ?? {}, "properties") as object, "temperature")).toBeDefined() + expect(dynamic?.outputSchema).toEqual(schema) + }), + ) + it.effect("preserves content tool results from dynamic tools", () => Effect.gen(function* () { - const screenshot = tool({ + const screenshot = Tool.make({ description: "Capture a screenshot.", jsonSchema: { type: "object", properties: {} }, execute: () => @@ -156,7 +308,7 @@ describe("LLMClient tools", () => { }) const events = Array.from( - yield* LLMClient.stream({ request: baseRequest, tools: { screenshot } }).pipe( + yield* TestToolRuntime.runTools({ request: baseRequest, tools: { screenshot }, maxSteps: 1 }).pipe( Stream.runCollect, Effect.provide( scriptedResponses([sseEvents(toolCallChunk("call_1", "screenshot", "{}"), finishChunk("tool_calls"))]), @@ -179,6 +331,32 @@ describe("LLMClient tools", () => { }), ) + it.effect("does not mistake dynamic tool output fields for dispatcher state", () => + Effect.gen(function* () { + const callerOwned = { type: "json" as const, value: { ok: true }, events: ["caller-owned"] } + const eventful = Tool.make({ + description: "Return an events field.", + jsonSchema: { type: "object", properties: {} }, + execute: () => Effect.succeed(callerOwned), + }) + + const dispatched = yield* ToolRuntime.dispatch( + { eventful }, + LLMEvent.toolCall({ id: "call_1", name: "eventful", input: {} }), + ) + + expect(dispatched.result).toEqual(callerOwned) + expect(dispatched.events).toEqual([ + LLMEvent.toolResult({ + id: "call_1", + name: "eventful", + result: callerOwned, + output: { structured: { ok: true }, content: [] }, + }), + ]) + }), + ) + it.effect("executes tool calls for one step without looping by default", () => Effect.gen(function* () { const layer = scriptedResponses([ @@ -187,7 +365,7 @@ describe("LLMClient tools", () => { ]) const events = Array.from( - yield* LLMClient.stream({ request: baseRequest, tools: { get_weather } }).pipe( + yield* TestToolRuntime.runTools({ request: baseRequest, tools: { get_weather }, maxSteps: 1 }).pipe( Stream.runCollect, Effect.provide(layer), ), @@ -201,7 +379,7 @@ describe("LLMClient tools", () => { it.effect("passes tool call context to execute", () => Effect.gen(function* () { let context: ToolExecuteContext | undefined - const contextual = tool({ + const contextual = Tool.make({ description: "Capture tool context.", parameters: Schema.Struct({ value: Schema.String }), success: Schema.Struct({ ok: Schema.Boolean }), @@ -234,11 +412,9 @@ describe("LLMClient tools", () => { ]) const events = Array.from( - yield* LLMClient.stream({ - request: baseRequest, - tools: { get_weather: schema_only_weather }, - toolExecution: "none", - }).pipe(Stream.runCollect, Effect.provide(layer)), + yield* LLMClient.stream( + LLMRequest.update(baseRequest, { tools: toDefinitions({ get_weather: schema_only_weather }) }), + ).pipe(Stream.runCollect, Effect.provide(layer)), ) expect(events.find(LLMEvent.is.toolCall)).toMatchObject({ type: "tool-call", id: "call_1" }) @@ -500,74 +676,6 @@ describe("LLMClient tools", () => { }), ) - it.effect("emits one final finish with aggregate usage", () => - Effect.gen(function* () { - let calls = 0 - const events = Array.from( - yield* ToolRuntime.stream({ - request: baseRequest, - tools: { get_weather }, - stopWhen: ToolRuntime.stepCountIs(2), - stream: () => - Stream.fromIterable( - calls++ === 0 - ? [ - LLMEvent.stepStart({ index: 0 }), - LLMEvent.toolCall({ id: "call_1", name: "get_weather", input: { city: "Paris" } }), - LLMEvent.stepFinish({ - index: 0, - reason: "tool-calls", - usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 }, - }), - LLMEvent.finish({ - reason: "tool-calls", - usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 }, - }), - ] - : [ - LLMEvent.stepStart({ index: 0 }), - LLMEvent.textDelta({ id: "text_1", text: "Done." }), - LLMEvent.stepFinish({ - index: 0, - reason: "stop", - usage: { inputTokens: 4, outputTokens: 5, totalTokens: 9 }, - }), - LLMEvent.finish({ reason: "stop", usage: { inputTokens: 4, outputTokens: 5, totalTokens: 9 } }), - ], - ), - }).pipe(Stream.runCollect), - ) - - expect(events.filter(LLMEvent.is.stepFinish).map((event) => event.index)).toEqual([0, 1]) - expect(events.filter(LLMEvent.is.finish)).toHaveLength(1) - expect(events.find(LLMEvent.is.finish)?.usage).toMatchObject({ - inputTokens: 5, - outputTokens: 7, - totalTokens: 12, - }) - }), - ) - - it.effect("stops follow-up when stopWhen returns true after the first step", () => - Effect.gen(function* () { - const layer = scriptedResponses([ - sseEvents(toolCallChunk("call_1", "get_weather", '{"city":"Paris"}'), finishChunk("tool_calls")), - sseEvents(deltaChunk({ role: "assistant", content: "Should not run." }), finishChunk("stop")), - ]) - - const events = Array.from( - yield* TestToolRuntime.runTools({ - request: baseRequest, - tools: { get_weather }, - stopWhen: (state) => state.step >= 0, - }).pipe(Stream.runCollect, Effect.provide(layer)), - ) - - expect(events.filter(LLMEvent.is.finish)).toHaveLength(1) - expect(events.find(LLMEvent.is.toolResult)).toMatchObject({ type: "tool-result", id: "call_1" }) - }), - ) - it.effect("does not dispatch provider-executed tool calls", () => Effect.gen(function* () { let streams = 0 diff --git a/packages/llm/test/tool.types.ts b/packages/llm/test/tool.types.ts index 1fce9fd231..5876b9f454 100644 --- a/packages/llm/test/tool.types.ts +++ b/packages/llm/test/tool.types.ts @@ -1,30 +1,38 @@ import { Effect, Schema } from "effect" -import { LLM } from "../src" +import { LLM, LLMRequest, ToolRuntime, toDefinitions } from "../src" import * as OpenAIChat from "../src/protocols/openai-chat" import { Auth } from "../src/route" -import { tool } from "../src/tool" +import { Tool } from "../src/tool" const request = LLM.request({ model: OpenAIChat.route.with({ auth: Auth.bearer("fixture") }).model({ id: "gpt-4o-mini" }), prompt: "Use the tool.", }) -const executable = tool({ +const executable = Tool.make({ description: "Get weather.", parameters: Schema.Struct({ city: Schema.String }), success: Schema.Struct({ forecast: Schema.String }), execute: (input) => Effect.succeed({ forecast: input.city }), }) -const schemaOnly = tool({ +const schemaOnly = Tool.make({ description: "Get weather.", parameters: Schema.Struct({ city: Schema.String }), success: Schema.Struct({ forecast: Schema.String }), }) -LLM.stream({ request, tools: { executable } }) -LLM.generate({ request, tools: { executable }, stopWhen: LLM.stepCountIs(2) }) -LLM.stream({ request, tools: { schemaOnly }, toolExecution: "none" }) +Tool.make({ + description: "Encode success before projection.", + parameters: Schema.Struct({ city: Schema.String }), + success: Schema.Struct({ forecast: Schema.NumberFromString }), + execute: () => Effect.succeed({ forecast: 1 }), + toModelOutput: ({ callID, parameters, output }) => [{ type: "text", text: `${callID}:${parameters.city}:${output.forecast}` }], +}) -// @ts-expect-error Handler-less tools can only be passed with toolExecution: "none". +LLM.stream(request) +LLM.generate(LLMRequest.update(request, { tools: toDefinitions({ schemaOnly }) })) +ToolRuntime.dispatch({ executable }, { type: "tool-call", id: "call_1", name: "executable", input: { city: "Paris" } }) + +// @ts-expect-error High-level tool orchestration overloads are intentionally not supported. LLM.stream({ request, tools: { schemaOnly } }) diff --git a/packages/opencode/src/background/job.ts b/packages/opencode/src/background/job.ts index 0e0c387430..4bc3bde883 100644 --- a/packages/opencode/src/background/job.ts +++ b/packages/opencode/src/background/job.ts @@ -1,274 +1,31 @@ +import { BackgroundJob as CoreBackgroundJob } from "@opencode-ai/core/background-job" import { InstanceState } from "@/effect/instance-state" -import { Identifier } from "@/id/id" -import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect" +import { Effect, Layer } from "effect" -export type Status = "running" | "completed" | "error" | "cancelled" - -export type Info = { - id: string - type: string - title?: string - status: Status - started_at: number - completed_at?: number - output?: string - error?: string - metadata?: Record -} - -type Active = { - info: Info - done: Deferred.Deferred - scope: Scope.Closeable - token: object - pending: number - next: number - output?: { sequence: number; text: string } -} - -type State = { - jobs: SynchronizedRef.SynchronizedRef> - scope: Scope.Scope -} - -type FinishResult = { - info?: Info - done?: Deferred.Deferred - scope?: Scope.Closeable -} - -export type StartInput = { - id?: string - type: string - title?: string - metadata?: Record - run: Effect.Effect -} - -export type ExtendInput = { - id: string - run: Effect.Effect -} - -export type WaitInput = { - id: string - timeout?: number -} - -export type WaitResult = { - info?: Info - timedOut: boolean -} - -export interface Interface { - readonly list: () => Effect.Effect - readonly get: (id: string) => Effect.Effect - readonly start: (input: StartInput) => Effect.Effect - readonly extend: (input: ExtendInput) => Effect.Effect - readonly wait: (input: WaitInput) => Effect.Effect - readonly cancel: (id: string) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/BackgroundJob") {} - -function snapshot(job: Active): Info { - return { - ...job.info, - ...(job.info.metadata ? { metadata: { ...job.info.metadata } } : {}), - } -} - -function errorText(error: unknown) { - if (error instanceof Error) return error.message - return String(error) -} - -export const layer = Layer.effect( +export { Service, + type ExtendInput, + type Info, + type Interface, + type StartInput, + type Status, + type WaitInput, + type WaitResult, +} from "@opencode-ai/core/background-job" + +/** Keeps the legacy service instance-scoped while sharing the core registry engine. */ +export const layer = Layer.effect( + CoreBackgroundJob.Service, Effect.gen(function* () { - const state = yield* InstanceState.make( - Effect.fn("BackgroundJob.state")(function* () { - return { - jobs: yield* SynchronizedRef.make(new Map()), - scope: yield* Scope.Scope, - } - }), - ) - - const settle = Effect.fn("BackgroundJob.settle")(function* ( - id: string, - token: object, - sequence: number, - exit: Exit.Exit, - ) { - const completed_at = yield* Clock.currentTimeMillis - const s = yield* InstanceState.get(state) - const result = yield* SynchronizedRef.modify(s.jobs, (jobs): readonly [FinishResult, Map] => { - const job = jobs.get(id) - if (!job) return [{}, jobs] - if (job.token !== token) return [{}, jobs] - if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs] - const pending = job.pending - 1 - const output = - Exit.isSuccess(exit) && (!job.output || sequence > job.output.sequence) - ? { sequence, text: exit.value } - : job.output - if (Exit.isSuccess(exit) && pending > 0) { - return [{}, new Map(jobs).set(id, { ...job, pending, output })] - } - const status: Exclude = Exit.isSuccess(exit) - ? "completed" - : Cause.hasInterruptsOnly(exit.cause) - ? "cancelled" - : "error" - const next = { - ...job, - pending: 0, - output, - info: { - ...job.info, - status, - completed_at, - ...(output ? { output: output.text } : {}), - ...(Exit.isFailure(exit) ? { error: errorText(Cause.squash(exit.cause)) } : {}), - }, - } - return [{ info: snapshot(next), done: job.done, scope: job.scope }, new Map(jobs).set(id, next)] - }) - if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore) - if (result.scope) { - yield* Scope.close(result.scope, Exit.void).pipe(Effect.forkIn(s.scope, { startImmediately: true })) - } - return result.info + const state = yield* InstanceState.make(() => CoreBackgroundJob.make) + return CoreBackgroundJob.Service.of({ + list: () => InstanceState.useEffect(state, (jobs) => jobs.list()), + get: (id) => InstanceState.useEffect(state, (jobs) => jobs.get(id)), + start: (input) => InstanceState.useEffect(state, (jobs) => jobs.start(input)), + extend: (input) => InstanceState.useEffect(state, (jobs) => jobs.extend(input)), + wait: (input) => InstanceState.useEffect(state, (jobs) => jobs.wait(input)), + cancel: (id) => InstanceState.useEffect(state, (jobs) => jobs.cancel(id)), }) - - const fork = Effect.fn("BackgroundJob.fork")(function* ( - scope: Scope.Scope, - id: string, - token: object, - sequence: number, - run: Effect.Effect, - ) { - return yield* run.pipe( - Effect.matchCauseEffect({ - onSuccess: (output) => settle(id, token, sequence, Exit.succeed(output)), - onFailure: (cause) => settle(id, token, sequence, Exit.failCause(cause)), - }), - Effect.asVoid, - Effect.forkIn(scope, { startImmediately: true }), - ) - }) - - const list: Interface["list"] = Effect.fn("BackgroundJob.list")(function* () { - return Array.from((yield* SynchronizedRef.get((yield* InstanceState.get(state)).jobs)).values()) - .map(snapshot) - .toSorted((a, b) => a.started_at - b.started_at) - }) - - const get: Interface["get"] = Effect.fn("BackgroundJob.get")(function* (id) { - const job = (yield* SynchronizedRef.get((yield* InstanceState.get(state)).jobs)).get(id) - if (!job) return - return snapshot(job) - }) - - const start: Interface["start"] = Effect.fn("BackgroundJob.start")(function* (input) { - return yield* Effect.uninterruptibleMask((restore) => - Effect.gen(function* () { - const s = yield* InstanceState.get(state) - const id = input.id ?? Identifier.ascending("job") - const started_at = yield* Clock.currentTimeMillis - const done = yield* Deferred.make() - return yield* SynchronizedRef.modifyEffect( - s.jobs, - Effect.fnUntraced(function* (jobs) { - const existing = jobs.get(id) - if (existing?.info.status === "running") return [snapshot(existing), jobs] as const - const scope = yield* Scope.fork(s.scope, "parallel") - const token = {} - yield* fork(scope, id, token, 0, restore(input.run)) - const job = { - info: { - id, - type: input.type, - title: input.title, - status: "running" as const, - started_at, - metadata: input.metadata, - }, - done, - scope, - token, - pending: 1, - next: 1, - } - return [snapshot(job), new Map(jobs).set(id, job)] as const - }), - ) - }), - ) - }) - - const extend: Interface["extend"] = Effect.fn("BackgroundJob.extend")(function* (input) { - return yield* Effect.uninterruptibleMask((restore) => - Effect.gen(function* () { - const s = yield* InstanceState.get(state) - return yield* SynchronizedRef.modifyEffect( - s.jobs, - Effect.fnUntraced(function* (jobs) { - const job = jobs.get(input.id) - if (!job || job.info.status !== "running") return [false, jobs] as const - yield* fork(job.scope, input.id, job.token, job.next, restore(input.run)) - return [ - true, - new Map(jobs).set(input.id, { - ...job, - pending: job.pending + 1, - next: job.next + 1, - }), - ] as const - }), - ) - }), - ) - }) - - const wait: Interface["wait"] = Effect.fn("BackgroundJob.wait")(function* (input) { - const job = (yield* SynchronizedRef.get((yield* InstanceState.get(state)).jobs)).get(input.id) - if (!job) return { timedOut: false } - if (job.info.status !== "running") return { info: snapshot(job), timedOut: false } - if (input.timeout === undefined) return { info: yield* Deferred.await(job.done), timedOut: false } - if (input.timeout <= 0) return { info: snapshot(job), timedOut: true } - const info = yield* Deferred.await(job.done).pipe(Effect.timeoutOption(input.timeout)) - if (info._tag === "Some") return { info: info.value, timedOut: false } - return { info: snapshot(job), timedOut: true } - }) - - const cancel: Interface["cancel"] = Effect.fn("BackgroundJob.cancel")(function* (id) { - const completed_at = yield* Clock.currentTimeMillis - const result = yield* SynchronizedRef.modify( - (yield* InstanceState.get(state)).jobs, - (jobs): readonly [FinishResult, Map] => { - const job = jobs.get(id) - if (!job) return [{}, jobs] - if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs] - const next = { - ...job, - pending: 0, - info: { - ...job.info, - status: "cancelled" as const, - completed_at, - }, - } - return [{ info: snapshot(next), done: job.done, scope: job.scope }, new Map(jobs).set(id, next)] - }, - ) - if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore) - if (result.scope) yield* Scope.close(result.scope, Exit.void) - return result.info - }) - - return Service.of({ list, get, start, extend, wait, cancel }) }), ) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx index d9d23999d2..59d4fff6a3 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx @@ -17,6 +17,11 @@ function activeAssistant(messages: SessionMessage[]) { return assistant?.type === "assistant" ? assistant : undefined } +function ownedAssistant(messages: SessionMessage[], messageID: string) { + const message = messages.find((message) => message.type === "assistant" && message.id === messageID) + return message?.type === "assistant" ? message : undefined +} + function activeCompaction(messages: SessionMessage[]) { const index = messages.findIndex((message) => message.type === "compaction") if (index < 0) return @@ -37,8 +42,8 @@ function latestTool(assistant: SessionMessageAssistant | undefined, callID?: str ) } -function latestText(assistant: SessionMessageAssistant | undefined) { - return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text") +function latestText(assistant: SessionMessageAssistant | undefined, textID: string) { + return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID) } function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) { @@ -72,6 +77,26 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( event.subscribe((event) => { switch (event.type) { + case "session.next.agent.switched": + update(event.properties.sessionID, (draft) => { + draft.unshift({ + id: event.id, + type: "agent-switched", + agent: event.properties.agent, + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.model.switched": + update(event.properties.sessionID, (draft) => { + draft.unshift({ + id: event.id, + type: "model-switched", + model: event.properties.model, + time: { created: event.properties.timestamp }, + }) + }) + break case "session.next.prompted": { update(event.properties.sessionID, (draft) => { draft.unshift({ @@ -80,6 +105,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( text: event.properties.prompt.text, files: event.properties.prompt.files, agents: event.properties.prompt.agents, + references: event.properties.prompt.references, time: { created: event.properties.timestamp }, }) }) @@ -133,7 +159,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( break case "session.next.step.ended": update(event.properties.sessionID, (draft) => { - const currentAssistant = activeAssistant(draft) + const currentAssistant = ownedAssistant(draft, event.properties.assistantMessageID) if (!currentAssistant) return currentAssistant.time.completed = event.properties.timestamp currentAssistant.finish = event.properties.finish @@ -145,7 +171,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( break case "session.next.step.failed": update(event.properties.sessionID, (draft) => { - const currentAssistant = activeAssistant(draft) + const currentAssistant = ownedAssistant(draft, event.properties.assistantMessageID) if (!currentAssistant) return currentAssistant.time.completed = event.properties.timestamp currentAssistant.finish = "error" @@ -154,24 +180,24 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( break case "session.next.text.started": update(event.properties.sessionID, (draft) => { - activeAssistant(draft)?.content.push({ type: "text", text: "" }) + activeAssistant(draft)?.content.push({ type: "text", id: event.properties.textID, text: "" }) }) break case "session.next.text.delta": update(event.properties.sessionID, (draft) => { - const match = latestText(activeAssistant(draft)) + const match = latestText(activeAssistant(draft), event.properties.textID) if (match) match.text += event.properties.delta }) break case "session.next.text.ended": update(event.properties.sessionID, (draft) => { - const match = latestText(activeAssistant(draft)) + const match = latestText(activeAssistant(draft), event.properties.textID) if (match) match.text = event.properties.text }) break case "session.next.tool.input.started": update(event.properties.sessionID, (draft) => { - activeAssistant(draft)?.content.push({ + ownedAssistant(draft, event.properties.assistantMessageID)?.content.push({ type: "tool", id: event.properties.callID, name: event.properties.name, @@ -182,15 +208,19 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( break case "session.next.tool.input.delta": update(event.properties.sessionID, (draft) => { - const match = latestTool(activeAssistant(draft), event.properties.callID) + const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID) if (match?.state.status === "pending") match.state.input += event.properties.delta }) break case "session.next.tool.input.ended": + update(event.properties.sessionID, (draft) => { + const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID) + if (match?.state.status === "pending") match.state.input = event.properties.text + }) break case "session.next.tool.called": update(event.properties.sessionID, (draft) => { - const match = latestTool(activeAssistant(draft), event.properties.callID) + const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID) if (!match) return match.time.ran = event.properties.timestamp match.provider = event.properties.provider @@ -199,7 +229,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( break case "session.next.tool.progress": update(event.properties.sessionID, (draft) => { - const match = latestTool(activeAssistant(draft), event.properties.callID) + const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID) if (match?.state.status !== "running") return match.state.structured = event.properties.structured match.state.content = [...event.properties.content] @@ -207,13 +237,14 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( break case "session.next.tool.success": update(event.properties.sessionID, (draft) => { - const match = latestTool(activeAssistant(draft), event.properties.callID) + const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID) if (match?.state.status !== "running") return match.state = { status: "completed", input: match.state.input, structured: event.properties.structured, content: [...event.properties.content], + result: event.properties.result, } match.provider = event.properties.provider match.time.completed = event.properties.timestamp @@ -221,14 +252,15 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( break case "session.next.tool.failed": update(event.properties.sessionID, (draft) => { - const match = latestTool(activeAssistant(draft), event.properties.callID) - if (match?.state.status !== "running") return + const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID) + if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return match.state = { status: "error", error: event.properties.error, - input: match.state.input, - structured: match.state.structured, - content: match.state.content, + input: typeof match.state.input === "string" ? {} : match.state.input, + structured: match.state.status === "running" ? match.state.structured : {}, + content: match.state.status === "running" ? match.state.content : [], + result: event.properties.result, } match.provider = event.properties.provider match.time.completed = event.properties.timestamp @@ -240,6 +272,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( type: "reasoning", id: event.properties.reasoningID, text: "", + providerMetadata: event.properties.providerMetadata, }) }) break @@ -252,7 +285,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( case "session.next.reasoning.ended": update(event.properties.sessionID, (draft) => { const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID) - if (match) match.text = event.properties.text + if (match) { + match.text = event.properties.text + if (event.properties.providerMetadata !== undefined) match.providerMetadata = event.properties.providerMetadata + } }) break case "session.next.retried": diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx index 8b2b2ed37b..23726e972a 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx @@ -1087,7 +1087,8 @@ function toolOutput(content?: Array) { return (content ?? []) .map((item) => { if (item.type === "text") return item.text.trim() - return `[file ${item.name ?? item.uri}]` + const source = item.source.type === "data" ? "inline data" : item.source.type === "url" ? item.source.url : item.source.uri + return `[file ${item.name ?? source}]` }) .filter(Boolean) .join("\n") diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 5e89c59e0f..6c29bd5ee2 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -389,7 +389,7 @@ export const layer = Layer.effect( type: event.type, data: event.data, }, - { publish: true }, + { publish: true, ownerID: space.id }, ) .pipe(Effect.provideService(WorkspaceRef, space.id)), { discard: true }, @@ -434,7 +434,7 @@ export const layer = Layer.effect( if (payload.type === "server.heartbeat") return if (payload.type === "sync" && payload.syncEvent) { - const failed = yield* events.replay(payload.syncEvent, { publish: true }).pipe( + const failed = yield* events.replay(payload.syncEvent, { publish: true, ownerID: space.id }).pipe( Effect.as(false), Effect.catchCause((error) => Effect.sync(() => { diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts index 6e704956b9..9f0ba92744 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts @@ -5,6 +5,7 @@ import { ProviderGroup } from "./v2/provider" import { SessionGroup } from "./v2/session" import { PermissionGroup, PermissionSavedGroup, SessionPermissionGroup } from "./v2/permission" import { FileSystemGroup } from "./v2/fs" +import { QuestionGroup, SessionQuestionGroup } from "./v2/question" export const V2Api = HttpApi.make("v2") .add(SessionGroup) @@ -15,6 +16,8 @@ export const V2Api = HttpApi.make("v2") .add(SessionPermissionGroup) .add(PermissionSavedGroup) .add(FileSystemGroup) + .add(QuestionGroup) + .add(SessionQuestionGroup) .annotateMerge( OpenApi.annotations({ title: "opencode experimental HttpApi", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/location.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/location.ts index a760642e7f..024069c84d 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/location.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/location.ts @@ -6,6 +6,8 @@ import { PermissionV2 } from "@opencode-ai/core/permission" import { ProjectReference } from "@opencode-ai/core/project-reference" import { AbsolutePath } from "@opencode-ai/core/schema" import { PluginBoot } from "@opencode-ai/core/plugin/boot" +import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { QuestionV2 } from "@opencode-ai/core/question" import { Effect, Layer, Schema } from "effect" import { HttpServerRequest } from "effect/unstable/http" import { HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi" @@ -43,16 +45,18 @@ export class V2LocationMiddleware extends HttpApiMiddleware.Service< | PermissionV2.Service | ProjectReference.Service | FileSystem.Service + | QuestionV2.Service } >()("@opencode/ExperimentalHttpApiV2Location") {} function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref { const query = new URL(request.url, "http://localhost").searchParams + const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"] return { directory: AbsolutePath.make( query.get("location[directory]") || request.headers["x-opencode-directory"] || process.cwd(), ), - workspaceID: query.get("location[workspace]") || request.headers["x-opencode-workspace"], + workspaceID: workspaceID ? WorkspaceV2.ID.make(workspaceID) : undefined, } } diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/model.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/model.ts index 2f52ff23d4..6d63153618 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/model.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/model.ts @@ -9,7 +9,7 @@ export const ModelGroup = HttpApiGroup.make("v2.model") .add( HttpApiEndpoint.get("models", "/api/model", { query: LocationQuery, - success: Schema.Array(ModelV2.Info), + success: Schema.Array(ModelV2.PublicInfo), error: ServiceUnavailableError, }) .annotateMerge(locationQueryOpenApi) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/provider.ts index 2038ddfedd..0075e0db16 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/provider.ts @@ -9,7 +9,7 @@ export const ProviderGroup = HttpApiGroup.make("v2.provider") .add( HttpApiEndpoint.get("providers", "/api/provider", { query: LocationQuery, - success: Schema.Array(ProviderV2.Info), + success: Schema.Array(ProviderV2.PublicInfo), error: ServiceUnavailableError, }) .annotateMerge(locationQueryOpenApi) @@ -25,7 +25,7 @@ export const ProviderGroup = HttpApiGroup.make("v2.provider") HttpApiEndpoint.get("provider", "/api/provider/:providerID", { params: { providerID: ProviderV2.ID }, query: LocationQuery, - success: ProviderV2.Info, + success: ProviderV2.PublicInfo, error: [ProviderNotFoundError, ServiceUnavailableError], }) .annotateMerge(locationQueryOpenApi) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/question.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/question.ts new file mode 100644 index 0000000000..b09f209bff --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/question.ts @@ -0,0 +1,59 @@ +import { QuestionV2 } from "@opencode-ai/core/question" +import { SessionV2 } from "@opencode-ai/core/session" +import { Schema } from "effect" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { QuestionNotFoundError, SessionNotFoundError } from "../../errors" +import { V2Authorization } from "../../middleware/authorization" +import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location" + +export const QuestionGroup = HttpApiGroup.make("v2.question") + .add( + HttpApiEndpoint.get("questionRequests", "/api/question/request", { + query: LocationQuery, + success: Schema.Array(QuestionV2.Request), + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.question.request.list", + summary: "List pending question requests", + description: "Retrieve pending question requests for a location.", + }), + ), + ) + .annotateMerge(OpenApi.annotations({ title: "v2 questions", description: "Experimental v2 question routes." })) + .middleware(V2LocationMiddleware) + .middleware(V2Authorization) + +export const SessionQuestionGroup = HttpApiGroup.make("v2.session.question") + .add( + HttpApiEndpoint.post("questionRequestReply", "/api/session/:sessionID/question/request/:requestID/reply", { + params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID }, + payload: QuestionV2.Reply, + success: HttpApiSchema.NoContent, + error: [SessionNotFoundError, QuestionNotFoundError], + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.question.reply", + summary: "Reply to pending question request", + description: "Answer a pending question request owned by a session.", + }), + ), + ) + .add( + HttpApiEndpoint.post("questionRequestReject", "/api/session/:sessionID/question/request/:requestID/reject", { + params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID }, + success: HttpApiSchema.NoContent, + error: [SessionNotFoundError, QuestionNotFoundError], + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.question.reject", + summary: "Reject pending question request", + description: "Reject a pending question request owned by a session.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ title: "v2 session questions", description: "Experimental v2 session question routes." }), + ) + .middleware(V2Authorization) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts index 7ec6b9875a..59b14eee33 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts @@ -1,5 +1,6 @@ import { SessionID } from "@/session/schema" import { SessionMessage } from "@opencode-ai/core/session/message" +import { SessionInput } from "@opencode-ai/core/session/input" import { Prompt } from "@opencode-ai/core/session/prompt" import { SessionV2 } from "@opencode-ai/core/session" import { ProjectV2 } from "@opencode-ai/core/project" @@ -8,6 +9,7 @@ import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { Schema, Struct } from "effect" import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { + ConflictError, InvalidCursorError, InvalidRequestError, ServiceUnavailableError, @@ -110,16 +112,18 @@ export const SessionGroup = HttpApiGroup.make("v2.session") params: { sessionID: SessionID }, query: WorkspaceRoutingQuery, payload: Schema.Struct({ + id: SessionMessage.ID.pipe(Schema.optional), prompt: Prompt, - delivery: SessionV2.Delivery.pipe(Schema.optional), + delivery: SessionInput.Delivery.pipe(Schema.optional), + resume: Schema.Boolean.pipe(Schema.optional), }), - success: SessionMessage.Message, - error: [SessionNotFoundError, ServiceUnavailableError], + success: SessionMessage.User, + error: [ConflictError, SessionNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "v2.session.prompt", summary: "Send v2 message", - description: "Create a v2 session message and queue it for the agent loop.", + description: "Durably admit one v2 session input and schedule agent-loop execution unless resume is false.", }), ), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts index 5269f35469..32b02b4c74 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts @@ -50,7 +50,8 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl last: payload.at(-1)?.seq, directory: ctx.payload.directory, }) - yield* events.replayAll(payload) + const ownerID = yield* InstanceState.workspaceID + yield* events.replayAll(payload, { ownerID, strictOwner: true }) log.info("sync replay complete", { sessionID: source, events: payload.length, diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts index 245d79a471..18ff8bd0af 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts @@ -1,6 +1,12 @@ import { SessionV2 } from "@opencode-ai/core/session" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { PermissionSaved } from "@opencode-ai/core/permission/saved" +import { ProjectV2 } from "@opencode-ai/core/project" +import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local" +import { SessionProjector } from "@opencode-ai/core/session/projector" +import { SessionStore } from "@opencode-ai/core/session/store" import { Layer } from "effect" import { layer as v2LocationLayer } from "../groups/v2/location" import { messageHandlers } from "./v2/message" @@ -9,6 +15,18 @@ import { providerHandlers } from "./v2/provider" import { sessionHandlers } from "./v2/session" import { permissionHandlers, savedPermissionHandlers, sessionPermissionHandlers } from "./v2/permission" import { fileSystemHandlers } from "./v2/fs" +import { questionHandlers, sessionQuestionHandlers } from "./v2/question" + +const routedSessions = SessionV2.layer.pipe( + Layer.provide(SessionProjector.layer), + Layer.provide(SessionExecutionLocal.layer), + Layer.provide(LocationServiceMap.layer), + Layer.provide(SessionStore.layer), + Layer.provide(EventV2.layer), + Layer.provide(Database.defaultLayer), + Layer.provide(ProjectV2.defaultLayer), + Layer.orDie, +) export const v2Handlers = Layer.mergeAll( sessionHandlers, @@ -19,9 +37,11 @@ export const v2Handlers = Layer.mergeAll( sessionPermissionHandlers, savedPermissionHandlers, fileSystemHandlers, + questionHandlers, + sessionQuestionHandlers, ).pipe( Layer.provide(v2LocationLayer), Layer.provide(LocationServiceMap.layer), Layer.provide(PermissionSaved.layer), - Layer.provide(SessionV2.defaultLayer), + Layer.provide(routedSessions), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts index c9cfe33bc8..e3b28aa439 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts @@ -1,7 +1,6 @@ import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionV2 } from "@opencode-ai/core/session" import { Effect, Schema } from "effect" -import * as DateTime from "effect/DateTime" import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../../api" import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors" @@ -10,7 +9,6 @@ const DefaultMessagesLimit = 50 const Cursor = Schema.Struct({ id: SessionMessage.ID, - time: Schema.Finite, order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]), direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]), }) @@ -19,9 +17,7 @@ const decodeCursor = Schema.decodeUnknownSync(Cursor) const cursor = { encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") { - return Buffer.from( - JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), order, direction }), - ).toString("base64url") + return Buffer.from(JSON.stringify({ id: message.id, order, direction })).toString("base64url") }, decode(input: string) { return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) @@ -47,7 +43,7 @@ export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message sessionID: ctx.params.sessionID, limit: ctx.query.limit ?? DefaultMessagesLimit, order, - cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined, + cursor: decoded ? { id: decoded.id, direction: decoded.direction } : undefined, }) .pipe( Effect.catchTag("Session.NotFoundError", (error) => diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/model.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/model.ts index 4a748ef9b7..0a39e04c5b 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/model.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/model.ts @@ -1,5 +1,6 @@ import { Catalog } from "@opencode-ai/core/catalog" import { PluginBoot } from "@opencode-ai/core/plugin/boot" +import { ModelV2 } from "@opencode-ai/core/model" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../../api" @@ -18,7 +19,7 @@ export const modelHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.model", ( const catalog = yield* Catalog.Service const pluginBoot = yield* PluginBoot.Service yield* pluginBoot.wait().pipe(Effect.catchDefect(() => Effect.fail(catalogUnavailable))) - return yield* catalog.model.available() + return (yield* catalog.model.available()).map(ModelV2.toPublic) }), ) }), diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/provider.ts index 2bc5cfbe82..bf3c5b52b1 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/provider.ts @@ -1,5 +1,6 @@ import { Catalog } from "@opencode-ai/core/catalog" import { PluginBoot } from "@opencode-ai/core/plugin/boot" +import { ProviderV2 } from "@opencode-ai/core/provider" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../../api" @@ -19,7 +20,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.provid const catalog = yield* Catalog.Service const pluginBoot = yield* PluginBoot.Service yield* pluginBoot.wait().pipe(Effect.catchDefect(() => Effect.fail(catalogUnavailable))) - return yield* catalog.provider.available() + return (yield* catalog.provider.available()).map(ProviderV2.toPublic) }), ) .handle( @@ -29,6 +30,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.provid const pluginBoot = yield* PluginBoot.Service yield* pluginBoot.wait().pipe(Effect.catchDefect(() => Effect.fail(catalogUnavailable))) return yield* catalog.provider.get(ctx.params.providerID).pipe( + Effect.map(ProviderV2.toPublic), Effect.catchTag("CatalogV2.ProviderNotFound", (error) => Effect.fail( new ProviderNotFoundError({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/question.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/question.ts new file mode 100644 index 0000000000..30eac559f9 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/question.ts @@ -0,0 +1,96 @@ +import { Database } from "@opencode-ai/core/database/database" +import { LocationServiceMap } from "@opencode-ai/core/location-layer" +import { QuestionV2 } from "@opencode-ai/core/question" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionTable } from "@opencode-ai/core/session/sql" +import { eq } from "drizzle-orm" +import { Effect } from "effect" +import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { InstanceHttpApi } from "../../api" +import { QuestionNotFoundError, SessionNotFoundError } from "../../errors" + +function missingRequest(id: QuestionV2.ID) { + return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` }) +} + +export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.question", (handlers) => + Effect.gen(function* () { + return handlers.handle( + "questionRequests", + Effect.fn(function* () { + return yield* (yield* QuestionV2.Service).list() + }), + ) + }), +) + +export const sessionQuestionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session.question", (handlers) => + Effect.gen(function* () { + const { db } = yield* Database.Service + const locations = yield* LocationServiceMap + + const withSessionQuestion = Effect.fnUntraced(function* ( + sessionID: QuestionV2.Request["sessionID"], + use: (question: QuestionV2.Interface) => Effect.Effect, + ) { + const row = yield* db + .select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id }) + .from(SessionTable) + .where(eq(SessionTable.id, sessionID)) + .get() + .pipe(Effect.orDie) + if (!row) + return yield* new SessionNotFoundError({ + sessionID, + message: `Session not found: ${sessionID}`, + }) + + return yield* Effect.gen(function* () { + return yield* use(yield* QuestionV2.Service) + }).pipe( + Effect.scoped, + Effect.provide( + locations.get({ directory: AbsolutePath.make(row.directory), workspaceID: row.workspaceID ?? undefined }), + ), + ) + }) + + const withOwnedQuestion = Effect.fnUntraced(function* ( + sessionID: QuestionV2.Request["sessionID"], + requestID: QuestionV2.ID, + use: (question: QuestionV2.Interface) => Effect.Effect, + ) { + return yield* withSessionQuestion(sessionID, (question) => + Effect.gen(function* () { + const request = (yield* question.list()).find((request) => request.id === requestID) + if (!request || request.sessionID !== sessionID) return yield* missingRequest(requestID) + return yield* use(question) + }), + ) + }) + + return handlers + .handle( + "questionRequestReply", + Effect.fn(function* (ctx) { + yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) => + question + .reply({ requestID: ctx.params.requestID, answers: ctx.payload.answers }) + .pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))), + ) + return HttpApiSchema.NoContent.make() + }), + ) + .handle( + "questionRequestReject", + Effect.fn(function* (ctx) { + yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) => + question + .reject(ctx.params.requestID) + .pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))), + ) + return HttpApiSchema.NoContent.make() + }), + ) + }), +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts index 6befb471ad..abb7be7373 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts @@ -3,7 +3,13 @@ import { DateTime, Effect } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../../api" import { SessionsCursor } from "../../groups/v2/session" -import { InvalidCursorError, ServiceUnavailableError, SessionNotFoundError, UnknownError } from "../../errors" +import { + ConflictError, + InvalidCursorError, + ServiceUnavailableError, + SessionNotFoundError, + UnknownError, +} from "../../errors" const DefaultSessionsLimit = 50 @@ -61,8 +67,10 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session return yield* session .prompt({ sessionID: ctx.params.sessionID, + id: ctx.payload.id, prompt: ctx.payload.prompt, - delivery: ctx.payload.delivery ?? SessionV2.DefaultDelivery, + delivery: ctx.payload.delivery, + resume: ctx.payload.resume, }) .pipe( Effect.catchTag("Session.NotFoundError", (error) => @@ -73,11 +81,11 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session }), ), ), - Effect.catchTag("Session.OperationUnavailableError", (error) => + Effect.catchTag("Session.PromptConflictError", (error) => Effect.fail( - new ServiceUnavailableError({ - message: `V2 session ${error.operation} is not available yet`, - service: `v2.session.${error.operation}`, + new ConflictError({ + message: `Prompt message ID conflicts with an existing durable record: ${error.messageID}`, + resource: error.messageID, }), ), ), diff --git a/packages/opencode/src/server/routes/instance/httpapi/public.ts b/packages/opencode/src/server/routes/instance/httpapi/public.ts index 751d42d00e..8517da276f 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/public.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/public.ts @@ -110,7 +110,7 @@ function matchLegacyOpenApi(input: Record) { if (operation.requestBody) { // The legacy OpenAPI surface never marked request bodies as required. // Keep that SDK surface stable while the HttpApi spec is tightened. - delete operation.requestBody.required + if (!isV2Api) delete operation.requestBody.required const body = operation.requestBody.content?.["application/json"] if (body?.schema) body.schema = stripOptionalNull(structuredClone(body.schema)) if (path === "/experimental/workspace" && method === "post") { diff --git a/packages/opencode/src/session/llm/native-runtime.ts b/packages/opencode/src/session/llm/native-runtime.ts index 2414ab6a5b..e1981912ce 100644 --- a/packages/opencode/src/session/llm/native-runtime.ts +++ b/packages/opencode/src/session/llm/native-runtime.ts @@ -4,10 +4,10 @@ import { ProviderTransform } from "@/provider/transform" import { errorMessage } from "@/util/error" import { isRecord } from "@/util/record" import { asSchema, type ModelMessage, type Tool } from "ai" -import { Effect } from "effect" +import { Cause, Effect, FiberSet, Queue } from "effect" import * as Stream from "effect/Stream" import { FetchHttpClient } from "effect/unstable/http" -import { tool as nativeTool, ToolFailure, type JsonSchema, type LLMEvent } from "@opencode-ai/llm" +import { LLMRequest, Tool as NativeTool, ToolFailure, ToolRuntime, toDefinitions, type JsonSchema, type LLMEvent } from "@opencode-ai/llm" import type { LLMClientShape } from "@opencode-ai/llm/route" import { LLMNative } from "./native-request" @@ -78,8 +78,8 @@ export function stream(input: StreamInput): StreamResult { // OpenAI's official wire field names, so this is identity, not translation // — if a field ever needs to differ between the two surfaces, the // translation belongs here, not split across both packages. - const stream = input.llmClient.stream({ - request: LLMNative.request({ + const tools = nativeTools(input.tools, input) + const request = LLMNative.request({ model: input.model, apiKey: current.apiKey, baseURL: current.baseURL, @@ -91,9 +91,45 @@ export function stream(input: StreamInput): StreamResult { maxOutputTokens: input.maxOutputTokens, providerOptions: ProviderTransform.providerOptions(input.model, input.providerOptions ?? {}), headers: { ...providerHeaders(input.provider.options.headers), ...input.headers }, - }), - tools: nativeTools(input.tools, input), }) + const stream = Stream.scoped( + Stream.unwrap( + Effect.gen(function* () { + const settlements = yield* FiberSet.make() + const results = yield* Queue.unbounded() + const provider = input.llmClient + .stream( + LLMRequest.update(request, { + tools: [...request.tools, ...toDefinitions(tools)], + }), + ) + .pipe( + Stream.flatMap((event) => + event.type !== "tool-call" || event.providerExecuted + ? Stream.make(event) + : Stream.make(event).pipe( + Stream.concat( + Stream.fromEffectDrain( + ToolRuntime.dispatch(tools, event).pipe( + Effect.flatMap((dispatched) => Queue.offerAll(results, dispatched.events)), + Effect.catchCause((cause) => Queue.failCause(results, cause)), + Effect.asVoid, + FiberSet.run(settlements, { startImmediately: true }), + ), + ), + ), + ), + ), + Stream.concat( + Stream.fromEffectDrain( + FiberSet.awaitEmpty(settlements).pipe(Effect.andThen(Queue.end(results)), Effect.asVoid), + ), + ), + ) + return provider.pipe(Stream.concat(Stream.fromQueue(results))) + }), + ), + ) return { ...current, @@ -128,7 +164,7 @@ export function nativeTools(tools: Record, input: Pick diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 549c895d3a..2bdcdf7921 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -29,7 +29,9 @@ import { ModelV2 } from "@opencode-ai/core/model" import { ProviderV2 } from "@opencode-ai/core/provider" import * as DateTime from "effect/DateTime" import { RuntimeFlags } from "@/effect/runtime-flags" -import { Usage, type LLMEvent } from "@opencode-ai/llm" +import { toolFileSourceFromUri, Usage, type LLMEvent } from "@opencode-ai/llm" +import { ToolOutput } from "@opencode-ai/core/tool-output" +import type { EventV2 } from "@opencode-ai/core/event" const DOOM_LOOP_THRESHOLD = 3 const log = Log.create({ service: "session.processor" }) @@ -65,11 +67,13 @@ export interface Interface { } type ToolCall = { + assistantMessageID?: EventV2.ID partID: SessionV1.ToolPart["id"] messageID: SessionV1.ToolPart["messageID"] sessionID: SessionV1.ToolPart["sessionID"] done: Deferred.Deferred inputEnded: boolean + raw: string } interface ProcessorContext extends Input { @@ -79,7 +83,9 @@ interface ProcessorContext extends Input { blocked: boolean needsCompaction: boolean currentText: SessionV1.TextPart | undefined + currentTextID: string | undefined reasoningMap: Record + v2AssistantMessageID: EventV2.ID | undefined } type StreamEvent = LLMEvent @@ -119,7 +125,9 @@ export const layer = Layer.effect( blocked: false, needsCompaction: false, currentText: undefined, + currentTextID: undefined, reasoningMap: {}, + v2AssistantMessageID: undefined, } let aborted = false const slog = log.clone().tag("session.id", input.sessionID).tag("messageID", input.assistantMessage.id) @@ -136,6 +144,32 @@ export const layer = Layer.effect( if (done) yield* Deferred.succeed(done, undefined).pipe(Effect.ignore) }) + const ensureV2AssistantMessage = Effect.fn("SessionProcessor.ensureV2AssistantMessage")(function* () { + if (ctx.v2AssistantMessageID) return ctx.v2AssistantMessageID + ctx.v2AssistantMessageID = (yield* events.publish(SessionEvent.Step.Started, { + sessionID: ctx.sessionID, + agent: input.assistantMessage.agent, + model: { + id: ModelV2.ID.make(ctx.model.id), + providerID: ProviderV2.ID.make(ctx.model.providerID), + variant: ModelV2.VariantID.make(input.assistantMessage.variant ?? "default"), + }, + snapshot: ctx.snapshot, + timestamp: DateTime.makeUnsafe(Date.now()), + })).id + return ctx.v2AssistantMessageID + }) + + const requireV2AssistantMessage = (toolCall?: ToolCall) => + toolCall?.assistantMessageID === undefined + ? Effect.die("V2 tool settlement has no owning assistant message") + : Effect.succeed(toolCall.assistantMessageID) + + const currentV2AssistantMessage = () => + ctx.v2AssistantMessageID === undefined + ? Effect.die("V2 step settlement has no owning assistant message") + : Effect.succeed(ctx.v2AssistantMessageID) + const readToolCall = Effect.fn("SessionProcessor.readToolCall")(function* (toolCallID: string) { const call = ctx.toolcalls[toolCallID] if (!call) return undefined @@ -220,6 +254,7 @@ export const layer = Layer.effect( sessionID: ctx.sessionID, reasoningID, text: ctx.reasoningMap[reasoningID].text, + providerMetadata: ctx.reasoningMap[reasoningID].metadata, timestamp: DateTime.makeUnsafe(Date.now()), }) } @@ -230,6 +265,27 @@ export const layer = Layer.effect( delete ctx.reasoningMap[reasoningID] }) + const flushV2Fragments = Effect.fn("SessionProcessor.flushV2Fragments")(function* () { + if (!flags.experimentalEventSystem) return + if (!ctx.assistantMessage.summary && ctx.currentText && ctx.currentTextID) { + yield* events.publish(SessionEvent.Text.Ended, { + sessionID: ctx.sessionID, + textID: ctx.currentTextID, + text: ctx.currentText.text, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } + yield* Effect.forEach(Object.entries(ctx.reasoningMap), ([reasoningID, part]) => + events.publish(SessionEvent.Reasoning.Ended, { + sessionID: ctx.sessionID, + reasoningID, + text: part.text, + providerMetadata: part.metadata, + timestamp: DateTime.makeUnsafe(Date.now()), + }), + ) + }) + const ensureToolCall = Effect.fn("SessionProcessor.ensureToolCall")(function* (input: { id: string name: string @@ -251,9 +307,11 @@ export const layer = Layer.effect( return { call: ctx.toolcalls[input.id], part } } // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - if (flags.experimentalEventSystem) { + const assistantMessageID = flags.experimentalEventSystem ? yield* ensureV2AssistantMessage() : undefined + if (assistantMessageID) { yield* events.publish(SessionEvent.Tool.Input.Started, { sessionID: ctx.sessionID, + assistantMessageID, callID: input.id, name: input.name, timestamp: DateTime.makeUnsafe(Date.now()), @@ -270,11 +328,13 @@ export const layer = Layer.effect( metadata: input.providerExecuted ? { providerExecuted: true } : undefined, } satisfies SessionV1.ToolPart) ctx.toolcalls[input.id] = { + assistantMessageID, done: yield* Deferred.make(), partID: part.id, messageID: part.messageID, sessionID: part.sessionID, inputEnded: false, + raw: "", } return { call: ctx.toolcalls[input.id], part } }) @@ -311,6 +371,7 @@ export const layer = Layer.effect( yield* events.publish(SessionEvent.Reasoning.Started, { sessionID: ctx.sessionID, reasoningID: value.id, + providerMetadata: value.providerMetadata, timestamp: DateTime.makeUnsafe(Date.now()), }) } @@ -331,6 +392,14 @@ export const layer = Layer.effect( if (!(value.id in ctx.reasoningMap)) return ctx.reasoningMap[value.id].text += value.text if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata + if (flags.experimentalEventSystem) { + yield* events.publish(SessionEvent.Reasoning.Delta, { + sessionID: ctx.sessionID, + reasoningID: value.id, + delta: value.text, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } yield* session.updatePartDelta({ sessionID: ctx.reasoningMap[value.id].sessionID, messageID: ctx.reasoningMap[value.id].messageID, @@ -355,18 +424,32 @@ export const layer = Layer.effect( return case "tool-input-delta": - // AI SDK emits a final `tool-call` with the parsed `input`; accumulating - // delta fragments into `state.raw` is redundant work for no current consumer. + { + const toolCall = yield* ensureToolCall(value) + const assistantMessageID = flags.experimentalEventSystem ? yield* requireV2AssistantMessage(toolCall.call) : undefined + if (assistantMessageID) { + yield* events.publish(SessionEvent.Tool.Input.Delta, { + sessionID: ctx.sessionID, + assistantMessageID, + callID: value.id, + delta: value.text, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } + ctx.toolcalls[value.id] = { ...toolCall.call, raw: toolCall.call.raw + value.text } + } return case "tool-input-end": { const toolCall = yield* ensureToolCall(value) // TODO(v2): Temporary dual-write while migrating session messages to v2 events. if (flags.experimentalEventSystem) { + const assistantMessageID = yield* requireV2AssistantMessage(toolCall.call) yield* events.publish(SessionEvent.Tool.Input.Ended, { sessionID: ctx.sessionID, + assistantMessageID, callID: value.id, - text: "", + text: toolCall.call.raw, timestamp: DateTime.makeUnsafe(Date.now()), }) } @@ -383,18 +466,22 @@ export const layer = Layer.effect( if (!toolCall.call.inputEnded) { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. if (flags.experimentalEventSystem) { + const assistantMessageID = yield* requireV2AssistantMessage(toolCall.call) yield* events.publish(SessionEvent.Tool.Input.Ended, { sessionID: ctx.sessionID, + assistantMessageID, callID: value.id, - text: "", + text: toolCall.call.raw, timestamp: DateTime.makeUnsafe(Date.now()), }) } } // TODO(v2): Temporary dual-write while migrating session messages to v2 events. if (flags.experimentalEventSystem) { + const assistantMessageID = yield* requireV2AssistantMessage(toolCall.call) yield* events.publish(SessionEvent.Tool.Called, { sessionID: ctx.sessionID, + assistantMessageID, callID: value.id, tool: value.name, input, @@ -453,6 +540,27 @@ export const layer = Layer.effect( case "tool-result": { const toolCall = yield* readToolCall(value.id) + if (!toolCall && value.result.type === "error") return + if (value.result.type === "error") { + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + if (flags.experimentalEventSystem) { + const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call) + yield* events.publish(SessionEvent.Tool.Failed, { + sessionID: ctx.sessionID, + assistantMessageID, + callID: value.id, + error: { type: "unknown", message: errorMessage(value.result.value) }, + result: value.result, + provider: { + executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true, + ...(value.providerMetadata ? { metadata: value.providerMetadata } : {}), + }, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } + yield* failToolCall(value.id, value.result.value) + return + } const rawOutput = toolResultOutput(value) const normalized = yield* Effect.forEach(rawOutput.attachments ?? [], (attachment) => attachment.mime.startsWith("image/") @@ -477,24 +585,42 @@ export const layer = Layer.effect( } // TODO(v2): Temporary dual-write while migrating session messages to v2 events. if (flags.experimentalEventSystem) { - yield* events.publish(SessionEvent.Tool.Success, { + const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call) + const content = [ + ToolOutput.text({ type: "text", text: output.output }), + ...(output.attachments?.map((item: SessionV1.FilePart) => + ToolOutput.file({ type: "file", source: toolFileSourceFromUri(item.url), mime: item.mime, name: item.filename }), + ) ?? []), + ] + const unsupported = content.find((item) => item.type === "file" && item.source.type !== "data") + if (unsupported?.type === "file") { + const error = new Error(`Tool attachment source "${unsupported.source.type}" must be materialized before durable V2 settlement`) + yield* events.publish(SessionEvent.Tool.Failed, { + sessionID: ctx.sessionID, + assistantMessageID, + callID: value.id, + error: { + type: "unknown", + message: error.message, + }, + provider: { + executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true, + ...(value.providerMetadata ? { metadata: value.providerMetadata } : {}), + }, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + yield* failToolCall(value.id, error) + return + } else yield* events.publish(SessionEvent.Tool.Success, { sessionID: ctx.sessionID, + assistantMessageID, callID: value.id, structured: output.metadata, - content: [ - { - type: "text", - text: output.output, - }, - ...(output.attachments?.map((item: SessionV1.FilePart) => ({ - type: "file" as const, - uri: item.url, - mime: item.mime, - name: item.filename, - })) ?? []), - ], + content, + result: value.result, provider: { executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true, + ...(value.providerMetadata ? { metadata: value.providerMetadata } : {}), }, timestamp: DateTime.makeUnsafe(Date.now()), }) @@ -507,8 +633,10 @@ export const layer = Layer.effect( const toolCall = yield* readToolCall(value.id) // TODO(v2): Temporary dual-write while migrating session messages to v2 events. if (flags.experimentalEventSystem) { + const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call) yield* events.publish(SessionEvent.Tool.Failed, { sessionID: ctx.sessionID, + assistantMessageID, callID: value.id, error: { type: "unknown", @@ -516,6 +644,7 @@ export const layer = Layer.effect( }, provider: { executed: toolCall?.part.metadata?.providerExecuted === true, + ...(value.providerMetadata ? { metadata: value.providerMetadata } : {}), }, timestamp: DateTime.makeUnsafe(Date.now()), }) @@ -532,17 +661,7 @@ export const layer = Layer.effect( if (!ctx.assistantMessage.summary) { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. if (flags.experimentalEventSystem) { - yield* events.publish(SessionEvent.Step.Started, { - sessionID: ctx.sessionID, - agent: input.assistantMessage.agent, - model: { - id: ModelV2.ID.make(ctx.model.id), - providerID: ProviderV2.ID.make(ctx.model.providerID), - variant: ModelV2.VariantID.make(input.assistantMessage.variant ?? "default"), - }, - snapshot: ctx.snapshot, - timestamp: DateTime.makeUnsafe(Date.now()), - }) + yield* ensureV2AssistantMessage() } } yield* session.updatePart({ @@ -567,12 +686,14 @@ export const layer = Layer.effect( if (flags.experimentalEventSystem) { yield* events.publish(SessionEvent.Step.Ended, { sessionID: ctx.sessionID, + assistantMessageID: yield* currentV2AssistantMessage(), finish: value.reason, cost: usage.cost, tokens: usage.tokens, snapshot: completedSnapshot, timestamp: DateTime.makeUnsafe(Date.now()), }) + ctx.v2AssistantMessageID = undefined } } ctx.assistantMessage.finish = value.reason @@ -625,6 +746,7 @@ export const layer = Layer.effect( yield* events.publish(SessionEvent.Text.Started, { sessionID: ctx.sessionID, timestamp: DateTime.makeUnsafe(Date.now()), + textID: value.id, }) } } @@ -637,6 +759,7 @@ export const layer = Layer.effect( time: { start: Date.now() }, metadata: value.providerMetadata, } + ctx.currentTextID = value.id yield* session.updatePart(ctx.currentText) return @@ -644,6 +767,14 @@ export const layer = Layer.effect( if (!ctx.currentText) return ctx.currentText.text += value.text if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata + if (flags.experimentalEventSystem) { + yield* events.publish(SessionEvent.Text.Delta, { + sessionID: ctx.sessionID, + textID: value.id, + delta: value.text, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } yield* session.updatePartDelta({ sessionID: ctx.currentText.sessionID, messageID: ctx.currentText.messageID, @@ -673,6 +804,7 @@ export const layer = Layer.effect( sessionID: ctx.sessionID, text: ctx.currentText.text, timestamp: DateTime.makeUnsafe(Date.now()), + textID: value.id, }) } } @@ -683,6 +815,7 @@ export const layer = Layer.effect( if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata yield* session.updatePart(ctx.currentText) ctx.currentText = undefined + ctx.currentTextID = undefined return case "finish": @@ -711,6 +844,7 @@ export const layer = Layer.effect( ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end } yield* session.updatePart(ctx.currentText) ctx.currentText = undefined + ctx.currentTextID = undefined } for (const part of Object.values(ctx.reasoningMap)) { @@ -732,6 +866,16 @@ export const layer = Layer.effect( const match = yield* readToolCall(toolCallID) if (!match) continue const part = match.part + if (flags.experimentalEventSystem && match.call.assistantMessageID) { + yield* events.publish(SessionEvent.Tool.Failed, { + sessionID: ctx.sessionID, + assistantMessageID: match.call.assistantMessageID, + callID: toolCallID, + error: { type: "unknown", message: "Tool execution aborted" }, + provider: { executed: part.metadata?.providerExecuted === true }, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } const end = Date.now() const metadata = "metadata" in part.state && isRecord(part.state.metadata) ? part.state.metadata : {} yield* session.updatePart({ @@ -753,6 +897,7 @@ export const layer = Layer.effect( const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) { slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined }) const error = parse(e) + yield* flushV2Fragments() if (SessionV1.ContextOverflowError.isInstance(error)) { ctx.needsCompaction = true yield* events.publish(Session.Event.Error, { sessionID: ctx.sessionID, error }) @@ -763,6 +908,7 @@ export const layer = Layer.effect( if (flags.experimentalEventSystem) { yield* events.publish(SessionEvent.Step.Failed, { sessionID: ctx.sessionID, + assistantMessageID: yield* ensureV2AssistantMessage(), error: { type: "unknown", message: errorMessage(e), @@ -787,6 +933,7 @@ export const layer = Layer.effect( return yield* Effect.gen(function* () { yield* Effect.gen(function* () { ctx.currentText = undefined + ctx.currentTextID = undefined ctx.reasoningMap = {} yield* status.set(ctx.sessionID, { type: "busy" }) const stream = llm.stream(streamInput) @@ -826,7 +973,8 @@ export const layer = Layer.effect( timestamp: DateTime.makeUnsafe(Date.now()), }) : Effect.void - return event.pipe( + return flushV2Fragments().pipe( + Effect.andThen(event), Effect.andThen( status.set(ctx.sessionID, { type: "retry", diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 1486f203fb..1f9dfb0477 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -53,7 +53,7 @@ import { Database } from "@opencode-ai/core/database/database" import { SessionEvent } from "@opencode-ai/core/session/event" import { ModelV2 } from "@opencode-ai/core/model" import { ProviderV2 } from "@opencode-ai/core/provider" -import { AgentAttachment, FileAttachment, ReferenceAttachment, Source } from "@opencode-ai/core/session/prompt" +import { AgentAttachment, FileAttachment, Prompt, ReferenceAttachment, Source } from "@opencode-ai/core/session/prompt" import { Reference } from "@/reference/reference" import * as DateTime from "effect/DateTime" import { eq } from "drizzle-orm" @@ -1191,12 +1191,13 @@ export const layer = Layer.effect( yield* events.publish(SessionEvent.Prompted, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(info.time.created), - prompt: { + delivery: "steer", + prompt: new Prompt({ text: nextPrompt.text.join("\n"), files: nextPrompt.files, agents: nextPrompt.agents, references: nextPrompt.references, - }, + }), }) } for (const text of nextPrompt.synthetic) { diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 696603adbb..78a8917414 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -1,10 +1,11 @@ import { chmod, mkdir, readFile, stat as statFile, writeFile } from "fs/promises" import { createWriteStream, existsSync, statSync } from "fs" import { realpathSync } from "fs" -import { dirname, isAbsolute, join, relative, resolve as pathResolve, win32 } from "path" +import { dirname, isAbsolute, join, resolve as pathResolve, win32 } from "path" import { Readable } from "stream" import { pipeline } from "stream/promises" import { Glob } from "@opencode-ai/core/util/glob" +import { FSUtil } from "@opencode-ai/core/fs-util" import { fileURLToPath } from "url" // Fast sync version for metadata checks @@ -163,13 +164,11 @@ export function windowsPath(p: string): string { ) } export function overlaps(a: string, b: string) { - const relA = relative(a, b) - const relB = relative(b, a) - return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..") + return FSUtil.overlaps(a, b) } export function contains(parent: string, child: string) { - return !relative(parent, child).startsWith("..") + return FSUtil.contains(parent, child) } export async function findUp( diff --git a/packages/opencode/test/cli/tui/sync-v2.test.tsx b/packages/opencode/test/cli/tui/sync-v2.test.tsx new file mode 100644 index 0000000000..04e624004e --- /dev/null +++ b/packages/opencode/test/cli/tui/sync-v2.test.tsx @@ -0,0 +1,133 @@ +/** @jsxImportSource @opentui/solid */ +import { expect, test } from "bun:test" +import { testRender } from "@opentui/solid" +import type { Event, GlobalEvent } from "@opencode-ai/sdk/v2" +import { onMount } from "solid-js" +import { ProjectProvider } from "../../../src/cli/cmd/tui/context/project" +import { SDKProvider } from "../../../src/cli/cmd/tui/context/sdk" +import { SyncProviderV2, useSyncV2 } from "../../../src/cli/cmd/tui/context/sync-v2" +import { createEventSource, createFetch, directory } from "../../fixture/tui-sdk" + +async function wait(fn: () => boolean, timeout = 2000) { + const start = Date.now() + while (!fn()) { + if (Date.now() - start > timeout) throw new Error("timed out waiting for condition") + await Bun.sleep(10) + } +} + +function global(payload: Event): GlobalEvent { + return { directory, project: "proj_test", payload } +} + +test("sync v2 settles pending tools when a live failure arrives", async () => { + const events = createEventSource() + const calls = createFetch() + let sync!: ReturnType + let ready!: () => void + const mounted = new Promise((resolve) => { + ready = resolve + }) + + function Probe() { + sync = useSyncV2() + onMount(ready) + return + } + + const app = await testRender(() => ( + + + + + + + + )) + + try { + await mounted + events.emit( + global({ + id: "agent-1", + type: "session.next.agent.switched", + properties: { sessionID: "session-1", timestamp: 0, agent: "build" }, + }), + ) + events.emit( + global({ + id: "model-1", + type: "session.next.model.switched", + properties: { + sessionID: "session-1", + timestamp: 0, + model: { id: "model-1", providerID: "provider-1" }, + }, + }), + ) + events.emit( + global({ + id: "assistant-1", + type: "session.next.step.started", + properties: { + sessionID: "session-1", + timestamp: 1, + agent: "build", + model: { id: "model-1", providerID: "provider-1" }, + }, + }), + ) + events.emit( + global({ + id: "input-1", + type: "session.next.tool.input.started", + properties: { + sessionID: "session-1", + timestamp: 2, + assistantMessageID: "assistant-1", + callID: "call-1", + name: "bash", + }, + }), + ) + events.emit( + global({ + id: "failed-1", + type: "session.next.tool.failed", + properties: { + sessionID: "session-1", + timestamp: 3, + assistantMessageID: "assistant-1", + callID: "call-1", + error: { type: "unknown", message: "aborted" }, + provider: { executed: false }, + }, + }), + ) + + await wait(() => { + const assistant = sync.session.message.fromSession("session-1")[0] + return assistant?.type === "assistant" && assistant.content[0]?.type === "tool" && assistant.content[0].state.status === "error" + }) + + const assistant = sync.session.message.fromSession("session-1")[0] + expect(assistant?.type).toBe("assistant") + if (assistant?.type !== "assistant") return + const tool = assistant.content[0] + expect(tool?.type).toBe("tool") + if (tool?.type !== "tool") return + expect(tool.state.status).toBe("error") + if (tool.state.status !== "error") return + expect(tool.state.error).toEqual({ type: "unknown", message: "aborted" }) + expect(tool.state.input).toEqual({}) + expect(tool.state.structured).toEqual({}) + expect(tool.state.content).toEqual([]) + expect(sync.session.message.fromSession("session-1").map((message) => message.type)).toEqual([ + "assistant", + "model-switched", + "agent-switched", + ]) + } finally { + app.renderer.destroy() + } +}) diff --git a/packages/opencode/test/server/httpapi-exercise/backend.ts b/packages/opencode/test/server/httpapi-exercise/backend.ts index 6bd060e52c..76818b5965 100644 --- a/packages/opencode/test/server/httpapi-exercise/backend.ts +++ b/packages/opencode/test/server/httpapi-exercise/backend.ts @@ -40,7 +40,15 @@ export function callAuthProbe(scenario: ActiveScenario, credentials: "missing" | }) } -const appCache: Partial> = {} +type CachedApp = BackendApp & { readonly dispose: () => Promise } + +const appCache: Partial> = {} + +export async function disposeApps() { + const apps = Object.values(appCache) + for (const key of Object.keys(appCache)) delete appCache[key] + await Promise.all(apps.flatMap((app) => app === undefined ? [] : [app.dispose()])) +} function app(modules: Runtime, options: CallOptions) { const username = options.auth?.username @@ -48,7 +56,7 @@ function app(modules: Runtime, options: CallOptions) { const cacheKey = `${username ?? ""}:${password ?? ""}` if (appCache[cacheKey]) return appCache[cacheKey] - const handler = HttpRouter.toWebHandler( + const web = HttpRouter.toWebHandler( modules.HttpApiApp.routes.pipe( Layer.provide( ConfigProvider.layer( @@ -57,10 +65,11 @@ function app(modules: Runtime, options: CallOptions) { ), ), { disableLogger: true, memoMap: modules.memoMap }, - ).handler + ) return (appCache[cacheKey] = { + dispose: web.dispose, request(input: string | URL | Request, init?: RequestInit) { - return handler( + return web.handler( input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init), modules.HttpApiApp.context, ) diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts index f8e8db75f7..88356504e0 100644 --- a/packages/opencode/test/server/httpapi-exercise/index.ts +++ b/packages/opencode/test/server/httpapi-exercise/index.ts @@ -33,6 +33,7 @@ import { import { color, printHeader, printResults } from "./report" import { coverageResult, parseOptions, routeKey, routeKeys, selectedScenarios } from "./routing" import { runScenario } from "./runner" +import { disposeApps } from "./backend" import { runtime } from "./runtime" import { type Scenario } from "./types" @@ -621,6 +622,7 @@ const scenarios: Scenario[] = [ .at((ctx) => ({ path: route("/api/provider/{providerID}", { providerID: "missing" }), headers: ctx.headers() })) .json(404, object, "status"), http.protected.get("/api/permission/request", "v2.permission.request.list").json(200, array), + http.protected.get("/api/question/request", "v2.question.request.list").json(200, array), http.protected .get("/api/session/{sessionID}/permission/request", "v2.session.permission.list") .seeded((ctx) => ctx.session({ title: "Permission list owner" })) @@ -641,6 +643,29 @@ const scenarios: Scenario[] = [ body: { reply: "once" }, })) .json(404, object, "status"), + http.protected + .post("/api/session/{sessionID}/question/request/{requestID}/reply", "v2.session.question.reply") + .seeded((ctx) => ctx.session({ title: "Question reply owner" })) + .at((ctx) => ({ + path: route("/api/session/{sessionID}/question/request/{requestID}/reply", { + sessionID: ctx.state.id, + requestID: "que_httpapi_missing", + }), + headers: ctx.headers(), + body: { answers: [] }, + })) + .json(404, object, "status"), + http.protected + .post("/api/session/{sessionID}/question/request/{requestID}/reject", "v2.session.question.reject") + .seeded((ctx) => ctx.session({ title: "Question reject owner" })) + .at((ctx) => ({ + path: route("/api/session/{sessionID}/question/request/{requestID}/reject", { + sessionID: ctx.state.id, + requestID: "que_httpapi_missing", + }), + headers: ctx.headers(), + })) + .json(404, object, "status"), http.protected.get("/api/permission/saved", "v2.permission.saved.list").json(200, array), http.protected .delete("/api/permission/saved/{id}", "v2.permission.saved.remove") @@ -1393,7 +1418,7 @@ const llmScenarios = new Set([ ]) const main = Effect.gen(function* () { - yield* Effect.addFinalizer(() => cleanupExercisePaths) + yield* Effect.addFinalizer(() => Effect.promise(() => disposeApps()).pipe(Effect.andThen(cleanupExercisePaths))) const options = parseOptions(Bun.argv.slice(2)) const modules = yield* Effect.promise(() => runtime()) const effectRoutes = routeKeys(OpenApi.fromApi(modules.PublicApi)) diff --git a/packages/opencode/test/server/httpapi-exercise/runner.ts b/packages/opencode/test/server/httpapi-exercise/runner.ts index 5db893baba..dade954387 100644 --- a/packages/opencode/test/server/httpapi-exercise/runner.ts +++ b/packages/opencode/test/server/httpapi-exercise/runner.ts @@ -7,7 +7,7 @@ import type { Config } from "../../../src/config/config" import type { MessageV2 } from "../../../src/session/message-v2" import { MessageID, PartID } from "../../../src/session/schema" -import { call, callAuthProbe } from "./backend" +import { call, callAuthProbe, disposeApps } from "./backend" import { original } from "./environment" import { runtime } from "./runtime" import type { ActiveScenario, Options, ProjectOptions, Result, Scenario, ScenarioContext, SeededContext } from "./types" @@ -259,6 +259,7 @@ const resetState = Effect.promise(async () => { const modules = await runtime() Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME + await disposeApps() await modules.disposeAllInstances() await modules.resetDatabase() await Bun.sleep(25) diff --git a/packages/opencode/test/server/httpapi-exercise/runtime.ts b/packages/opencode/test/server/httpapi-exercise/runtime.ts index bd261cbe40..81bd302a1a 100644 --- a/packages/opencode/test/server/httpapi-exercise/runtime.ts +++ b/packages/opencode/test/server/httpapi-exercise/runtime.ts @@ -2,7 +2,7 @@ export type Runtime = { PublicApi: (typeof import("../../../src/server/routes/instance/httpapi/public"))["PublicApi"] HttpApiApp: (typeof import("../../../src/server/routes/instance/httpapi/server"))["HttpApiApp"] AppLayer: (typeof import("../../../src/effect/app-runtime"))["AppLayer"] - memoMap: (typeof import("@opencode-ai/core/effect/memo-map"))["memoMap"] + memoMap: import("effect").Layer.MemoMap InstanceRef: (typeof import("../../../src/effect/instance-ref"))["InstanceRef"] InstanceStore: (typeof import("../../../src/project/instance-store"))["InstanceStore"] Session: (typeof import("../../../src/session/session"))["Session"] @@ -22,7 +22,7 @@ export function runtime() { const publicApi = await import("../../../src/server/routes/instance/httpapi/public") const httpApiServer = await import("../../../src/server/routes/instance/httpapi/server") const appRuntime = await import("../../../src/effect/app-runtime") - const memoMap = await import("@opencode-ai/core/effect/memo-map") + const { Layer } = await import("effect") const instanceRef = await import("../../../src/effect/instance-ref") const instanceStore = await import("../../../src/project/instance-store") const session = await import("../../../src/session/session") @@ -36,7 +36,7 @@ export function runtime() { PublicApi: publicApi.PublicApi, HttpApiApp: httpApiServer.HttpApiApp, AppLayer: appRuntime.AppLayer, - memoMap: memoMap.memoMap, + memoMap: Layer.makeMemoMapUnsafe(), InstanceRef: instanceRef.InstanceRef, InstanceStore: instanceStore.InstanceStore, Session: session.Session, diff --git a/packages/opencode/test/server/httpapi-public-catalog-redaction.test.ts b/packages/opencode/test/server/httpapi-public-catalog-redaction.test.ts new file mode 100644 index 0000000000..90a10bcc82 --- /dev/null +++ b/packages/opencode/test/server/httpapi-public-catalog-redaction.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, test } from "bun:test" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { Schema } from "effect" +import { OpenApi } from "effect/unstable/httpapi" +import { PublicApi } from "../../src/server/routes/instance/httpapi/public" + +type OpenApiSchema = { + readonly $ref?: string + readonly items?: OpenApiSchema + readonly properties?: Record +} + +type OpenApiSpec = { + readonly components?: { readonly schemas?: Record } + readonly paths: Record< + string, + { + readonly get?: { + readonly responses?: Record }> + } + } + > +} + +function responseSchema(spec: OpenApiSpec, path: string) { + return spec.paths[path]?.get?.responses?.["200"]?.content?.["application/json"]?.schema +} + +function componentName(ref: string | undefined) { + return ref?.replace("#/components/schemas/", "") +} + +describe("PublicApi v2 catalog redaction", () => { + test("routes use redacted provider and model DTO schemas", () => { + const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec + const provider = responseSchema(spec, "/api/provider/{providerID}") + const providers = responseSchema(spec, "/api/provider") + const models = responseSchema(spec, "/api/model") + + expect(componentName(provider?.$ref)).toBe("ProviderV2PublicInfo") + expect(componentName(providers?.items?.$ref)).toBe("ProviderV2PublicInfo") + expect(componentName(models?.items?.$ref)).toBe("ModelV2PublicInfo") + + const providerProperties = spec.components?.schemas?.ProviderV2PublicInfo?.properties + const modelProperties = spec.components?.schemas?.ModelV2PublicInfo?.properties + expect(providerProperties).not.toHaveProperty("request") + expect(modelProperties).not.toHaveProperty("request") + expect(JSON.stringify(providerProperties)).not.toMatch(/settings|headers|body|data/) + expect(JSON.stringify(modelProperties)).not.toMatch(/settings|headers|body/) + }) + + test("DTOs sanitize provider and model API URLs", () => { + const providerID = ProviderV2.ID.make("test") + const providers = [ + new ProviderV2.Info({ + ...ProviderV2.Info.empty(providerID), + api: { + type: "native", + url: "https://provider-user:provider-password@example.com:8443/provider/v1?api_key=provider-secret#fragment", + settings: {}, + }, + }), + new ProviderV2.Info({ + ...ProviderV2.Info.empty(providerID), + api: { + type: "aisdk", + package: "@ai-sdk/openai", + url: "https://provider-aisdk-user:provider-aisdk-password@example.com:8444/provider/aisdk?api_key=provider-aisdk-secret#fragment", + }, + }), + ].map((provider) => Schema.encodeSync(ProviderV2.PublicInfo)(ProviderV2.toPublic(provider))) + const models = [ + new ModelV2.Info({ + ...ModelV2.Info.empty(providerID, ModelV2.ID.make("native")), + api: { + id: ModelV2.ID.make("native"), + type: "native", + url: "https://native-user:native-password@example.com:9443/native/v1?api_key=native-secret#fragment", + settings: {}, + }, + }), + new ModelV2.Info({ + ...ModelV2.Info.empty(providerID, ModelV2.ID.make("aisdk")), + api: { + id: ModelV2.ID.make("aisdk"), + type: "aisdk", + package: "@ai-sdk/openai", + url: "https://aisdk-user:aisdk-password@example.com:10443/aisdk/v1?api_key=aisdk-secret#fragment", + }, + }), + ].map((model) => Schema.encodeSync(ModelV2.PublicInfo)(ModelV2.toPublic(model))) + + expect(providers.map((provider) => provider.api)).toEqual([ + { type: "native", url: "https://example.com:8443" }, + { type: "aisdk", package: "@ai-sdk/openai", url: "https://example.com:8444" }, + ]) + expect(models.map((model) => model.api)).toEqual([ + { id: "native", type: "native", url: "https://example.com:9443" }, + { id: "aisdk", type: "aisdk", package: "@ai-sdk/openai", url: "https://example.com:10443" }, + ]) + expect(JSON.stringify({ providers, models })).not.toMatch(/user|password|api_key|secret|fragment/) + }) + + test("DTOs omit malformed API URLs", () => { + const providerID = ProviderV2.ID.make("test") + const provider = Schema.encodeSync(ProviderV2.PublicInfo)( + ProviderV2.toPublic( + new ProviderV2.Info({ + ...ProviderV2.Info.empty(providerID), + api: { type: "native", url: "not a url?api_key=provider-secret", settings: {} }, + }), + ), + ) + const modelID = ModelV2.ID.make("aisdk") + const model = Schema.encodeSync(ModelV2.PublicInfo)( + ModelV2.toPublic( + new ModelV2.Info({ + ...ModelV2.Info.empty(providerID, modelID), + api: { id: modelID, type: "aisdk", package: "@ai-sdk/openai", url: "model-secret" }, + }), + ), + ) + + expect(provider.api).toEqual({ type: "native" }) + expect(model.api).toEqual({ id: "aisdk", type: "aisdk", package: "@ai-sdk/openai" }) + expect(JSON.stringify({ provider, model })).not.toMatch(/secret|api_key/) + }) +}) diff --git a/packages/opencode/test/server/httpapi-public-openapi.test.ts b/packages/opencode/test/server/httpapi-public-openapi.test.ts index 70977eecae..6e1c5eae3e 100644 --- a/packages/opencode/test/server/httpapi-public-openapi.test.ts +++ b/packages/opencode/test/server/httpapi-public-openapi.test.ts @@ -3,7 +3,7 @@ import { OpenApi } from "effect/unstable/httpapi" import { PublicApi } from "../../src/server/routes/instance/httpapi/public" type Method = "get" | "post" | "put" | "delete" | "patch" -type OpenApiSchema = { readonly $ref?: string } +type OpenApiSchema = { readonly $ref?: string; readonly anyOf?: ReadonlyArray } type OpenApiResponse = { readonly description?: string readonly content?: Record @@ -16,6 +16,7 @@ type OpenApiOperation = { readonly schema?: { readonly type?: string } }> readonly responses?: Record + readonly requestBody?: { readonly required?: boolean } readonly security?: unknown } type OpenApiPathItem = Partial> @@ -44,6 +45,12 @@ function componentName(ref: string) { return ref.replace("#/components/schemas/", "") } +function componentNames(response: OpenApiResponse | undefined) { + const schema = response?.content?.["application/json"]?.schema + if (!schema) return [] + return [schema, ...(schema.anyOf ?? [])].flatMap((item) => (item.$ref ? [componentName(item.$ref)] : [])) +} + function isBuiltInEndpointError(name: string) { return name.startsWith("EffectHttpApiError") || name.startsWith("effect_HttpApiError_") } @@ -71,6 +78,18 @@ describe("PublicApi OpenAPI v2 errors", () => { } }) + test("preserves required request bodies for v2 mutations", () => { + const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec + + for (const path of [ + "/api/session/{sessionID}/prompt", + "/api/session/{sessionID}/permission/request/{requestID}/reply", + "/api/session/{sessionID}/question/request/{requestID}/reply", + ]) { + expect(spec.paths[path]?.post?.requestBody?.required, path).toBe(true) + } + }) + test("does not rewrite /api endpoint errors to legacy error components", () => { const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec const refs = v2Operations(spec) @@ -139,7 +158,6 @@ describe("PublicApi OpenAPI v2 errors", () => { const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec for (const route of [ - ["post", "/api/session/{sessionID}/prompt"], ["post", "/api/session/{sessionID}/compact"], ["post", "/api/session/{sessionID}/wait"], ] as const) { @@ -191,6 +209,15 @@ describe("PublicApi OpenAPI v2 errors", () => { "QuestionNotFoundError", ) } + for (const route of [ + ["post", "/api/session/{sessionID}/question/request/{requestID}/reply"], + ["post", "/api/session/{sessionID}/question/request/{requestID}/reject"], + ] as const) { + expect(componentNames(spec.paths[route[1]]?.[route[0]]?.responses?.["404"])).toEqual([ + "SessionNotFoundError", + "QuestionNotFoundError", + ]) + } }) test("documents MCP server not-found errors", () => { diff --git a/packages/opencode/test/server/httpapi-sdk.test.ts b/packages/opencode/test/server/httpapi-sdk.test.ts index dc4e52417d..4e8ba6c678 100644 --- a/packages/opencode/test/server/httpapi-sdk.test.ts +++ b/packages/opencode/test/server/httpapi-sdk.test.ts @@ -60,13 +60,20 @@ type TestScope = Scope.Scope | TestServices function client( serverPath: ServerPath, directory?: string, - input?: { password?: string; username?: string; headers?: Record }, + input?: { + password?: string + username?: string + headers?: Record + workspaceID?: string + onRequest?: (request: Request) => void + }, ) { return serverFetch(serverPath, input).pipe( Effect.map((fetch) => createOpencodeClient({ baseUrl: "http://localhost", directory, + experimental_workspaceID: input?.workspaceID, headers: input?.headers, fetch, }), @@ -74,7 +81,10 @@ function client( ) } -function serverFetch(serverPath: ServerPath, input?: { password?: string; username?: string }) { +function serverFetch( + serverPath: ServerPath, + input?: { password?: string; username?: string; onRequest?: (request: Request) => void }, +) { return HttpServer.HttpServer.use((server) => Effect.sync(() => { void serverPath @@ -84,6 +94,7 @@ function serverFetch(serverPath: ServerPath, input?: { password?: string; userna return Object.assign( async (request: RequestInfo | URL, init?: RequestInit) => { const source = request instanceof Request ? request : new Request(request, init) + input?.onRequest?.(source) const url = new URL(source.url) return globalThis.fetch(new Request(new URL(`${url.pathname}${url.search}`, baseUrl), source)) }, @@ -367,6 +378,31 @@ describe("HttpApi SDK", () => { }), ) + httpapi( + "routes configured SDK directory and workspace for v2 location GETs", + withProject("raw", { setup: writeStandardFiles }, ({ directory }) => + Effect.gen(function* () { + const workspaceID = "wrk_sdk" + let request: Request | undefined + const sdk = yield* client("raw", directory, { + workspaceID, + onRequest: (value) => (request = value), + }) + const file = yield* call(() => sdk.v2.fs.read({ path: "hello.txt" })) + const url = new URL(request!.url) + + expect(file.response.status).toBe(200) + expect(file.data).toMatchObject({ content: "hello" }) + expect(url.searchParams.get("directory")).toBe(directory) + expect(url.searchParams.get("workspace")).toBe(workspaceID) + expect(url.searchParams.get("location[directory]")).toBe(directory) + expect(url.searchParams.get("location[workspace]")).toBe(workspaceID) + expect(request!.headers.has("x-opencode-directory")).toBe(false) + expect(request!.headers.has("x-opencode-workspace")).toBe(false) + }), + ), + ) + serverPathParity("matches generated SDK global and control behavior", (serverPath) => Effect.gen(function* () { const sdk = yield* client(serverPath) diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 5763cf3b00..70e3112910 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -24,7 +24,7 @@ import { Session } from "@/session/session" import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from "../../src/session/schema" import { MessageV2 } from "../../src/session/message-v2" import { Database } from "@opencode-ai/core/database/database" -import { SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql" +import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql" import { SessionMessage } from "@opencode-ai/core/session/message" import { ModelV2 } from "@opencode-ai/core/model" import { ProviderV2 } from "@opencode-ai/core/provider" @@ -129,7 +129,7 @@ const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: stri (info) => Workspace.use.remove(info.id).pipe(Effect.ignore), ) -const insertLegacyAssistantMessage = (sessionID: SessionIDType, time = 1) => +const insertLegacyAssistantMessage = (sessionID: SessionIDType, seq = 1, time = seq) => Effect.gen(function* () { const message = new SessionMessage.Assistant({ id: SessionMessage.ID.create(), @@ -151,6 +151,7 @@ const insertLegacyAssistantMessage = (sessionID: SessionIDType, time = 1) => id: message.id, session_id: sessionID, type: message.type, + seq, time_created: time, data: { time: { created: time }, @@ -162,6 +163,7 @@ const insertLegacyAssistantMessage = (sessionID: SessionIDType, time = 1) => ]) .run() .pipe(Effect.orDie) + return message }) const insertCorruptV2Message = (sessionID: SessionIDType, time = 1) => @@ -174,6 +176,7 @@ const insertCorruptV2Message = (sessionID: SessionIDType, time = 1) => id: SessionMessage.ID.create(), session_id: sessionID, type: "assistant", + seq: time, time_created: time, data: {} as NonNullable<(typeof SessionMessageTable.$inferInsert)["data"]>, }, @@ -441,8 +444,8 @@ describe("session HttpApi", () => { const test = yield* TestInstance const headers = { "x-opencode-directory": test.directory } const session = yield* createSession({ title: "v2 cursor" }) - yield* insertLegacyAssistantMessage(session.id, 1) - yield* insertLegacyAssistantMessage(session.id, 2) + const firstMessage = yield* insertLegacyAssistantMessage(session.id, 1, 2) + const secondMessage = yield* insertLegacyAssistantMessage(session.id, 2, 1) const sessionPage = yield* request( `/api/session?${new URLSearchParams({ @@ -480,8 +483,30 @@ describe("session HttpApi", () => { }) const messagePage = yield* request(`/api/session/${session.id}/message?limit=1`, { headers }) - const messageCursor = (yield* json<{ cursor: { next?: string } }>(messagePage)).cursor.next + const messageBody = yield* json<{ items: SessionMessage.Message[]; cursor: { next?: string } }>(messagePage) + const messageCursor = messageBody.cursor.next expect(messageCursor).toBeTruthy() + expect(messageBody.items.map((message) => message.id)).toEqual([secondMessage.id]) + expect(JSON.parse(Buffer.from(messageCursor!, "base64url").toString("utf8"))).toEqual({ + id: secondMessage.id, + order: "desc", + direction: "next", + }) + + const nextMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${messageCursor}`, { headers }) + expect((yield* json<{ items: SessionMessage.Message[] }>(nextMessagePage)).items.map((message) => message.id)).toEqual([ + firstMessage.id, + ]) + + const legacyMessageCursor = Buffer.from( + JSON.stringify({ id: secondMessage.id, time: 1, order: "desc", direction: "next" }), + ).toString("base64url") + const legacyMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${legacyMessageCursor}`, { + headers, + }) + expect((yield* json<{ items: SessionMessage.Message[] }>(legacyMessagePage)).items.map((message) => message.id)).toEqual([ + firstMessage.id, + ]) const messageCursorWithOrder = yield* request( `/api/session/${session.id}/message?cursor=${messageCursor}&order=asc`, @@ -543,6 +568,64 @@ describe("session HttpApi", () => { { git: true, config: { formatter: false, lsp: false } }, ) + it.instance( + "durably records one v2 prompt for exact message-ID retries", + () => + Effect.gen(function* () { + const test = yield* TestInstance + const headers = { "x-opencode-directory": test.directory } + const session = yield* createSession({ title: "v2 prompt recording" }) + + const recordPrompt = () => + request(`/api/session/${session.id}/prompt`, { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({ id: "evt_http_prompt", prompt: { text: "hello" } }), + }) + const first = yield* recordPrompt() + const retried = yield* recordPrompt() + type PromptBody = { id: string; type: string; text: string } + const firstBody = yield* json(first) + const retriedBody = yield* json(retried) + expect(first.status).toBe(200) + expect(retried.status).toBe(200) + expect(retriedBody).toEqual(firstBody) + expect(firstBody).toMatchObject({ type: "user", text: "hello" }) + + const messages = yield* requestJson<{ items: PromptBody[] }>(`/api/session/${session.id}/message`, { + headers, + }) + expect(messages.items).toHaveLength(0) + const admitted = yield* Database.Service.use(({ db }) => + db + .select() + .from(SessionInputTable) + .where(eq(SessionInputTable.id, SessionMessage.ID.make("evt_http_prompt"))) + .get() + .pipe(Effect.orDie), + ) + expect(admitted).toMatchObject({ + id: "evt_http_prompt", + session_id: session.id, + delivery: "steer", + promoted_seq: null, + }) + + const conflict = yield* request(`/api/session/${session.id}/prompt`, { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({ id: "evt_http_prompt", prompt: { text: "goodbye" } }), + }) + expect(conflict.status).toBe(409) + expect(yield* responseJson(conflict)).toEqual({ + _tag: "ConflictError", + message: "Prompt message ID conflicts with an existing durable record: evt_http_prompt", + resource: "evt_http_prompt", + }) + }), + { git: true, config: { formatter: false, lsp: false } }, + ) + it.instance( "returns v2 public unavailable errors for unfinished session mutations", () => @@ -551,18 +634,6 @@ describe("session HttpApi", () => { const headers = { "x-opencode-directory": test.directory } const session = yield* createSession({ title: "v2 unavailable" }) - const prompt = yield* request(`/api/session/${session.id}/prompt`, { - method: "POST", - headers: { ...headers, "content-type": "application/json" }, - body: JSON.stringify({ prompt: { text: "hello" } }), - }) - expect(prompt.status).toBe(503) - expect(yield* responseJson(prompt)).toEqual({ - _tag: "ServiceUnavailableError", - message: "V2 session prompt is not available yet", - service: "v2.session.prompt", - }) - const compact = yield* request(`/api/session/${session.id}/compact`, { method: "POST", headers }) expect(compact.status).toBe(503) expect(yield* responseJson(compact)).toEqual({ diff --git a/packages/opencode/test/session/llm-native-recorded.test.ts b/packages/opencode/test/session/llm-native-recorded.test.ts index cc7343f222..8b57b89c64 100644 --- a/packages/opencode/test/session/llm-native-recorded.test.ts +++ b/packages/opencode/test/session/llm-native-recorded.test.ts @@ -1,14 +1,11 @@ -import { NodeFileSystem } from "@effect/platform-node" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { SessionV1 } from "@opencode-ai/core/v1/session" import { FSUtil } from "@opencode-ai/core/fs-util" import { ModelsDev } from "@opencode-ai/core/models-dev" -import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { HttpRecorder, Redactor } from "@opencode-ai/http-recorder" import { describe, expect, test } from "bun:test" import { tool, type ModelMessage, type JSONValue } from "ai" import { Effect, Layer, Option, Schema, Stream } from "effect" -import { FetchHttpClient } from "effect/unstable/http" import path from "node:path" import z from "zod" import { Auth } from "@/auth" @@ -280,13 +277,10 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) { Layer.provide(Plugin.defaultLayer), Layer.provide(ModelsDev.defaultLayer), Layer.provide(RuntimeFlags.defaultLayer), - Layer.provide(LocationServiceMap.layer), ) // Only the HTTP client is recorded; RequestExecutor and the opencode LLM stack remain real. - const recordedClient = LLMClient.layer.pipe( - Layer.provide(Layer.mergeAll(RequestExecutor.layer, WebSocketExecutor.layer)), - Layer.provide( - HttpRecorder.recordingLayer(scenario.cassette, { + const recordedHttp = HttpRecorder.cassetteLayer(scenario.cassette, { + directory: FIXTURES_DIR, mode: shouldRecord ? "record" : "replay", metadata: { provider: scenario.providerID, @@ -295,7 +289,10 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) { tags: scenario.tags, }, redactor: recordingRedactor, - }).pipe(Layer.provide(FetchHttpClient.layer)), + }) + const recordedClient = LLMClient.layer.pipe( + Layer.provide( + Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer), ), ) @@ -307,9 +304,6 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) { Layer.provide(provider), Layer.provide(Plugin.defaultLayer), Layer.provide(recordedClient), - Layer.provide( - HttpRecorder.Cassette.fileSystem({ directory: FIXTURES_DIR }).pipe(Layer.provide(NodeFileSystem.layer)), - ), Layer.provide(RuntimeFlags.layer({ experimentalNativeLlm: true })), ), ) diff --git a/packages/opencode/test/session/llm-native.test.ts b/packages/opencode/test/session/llm-native.test.ts index 29c25d1ad0..09766f0bc2 100644 --- a/packages/opencode/test/session/llm-native.test.ts +++ b/packages/opencode/test/session/llm-native.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test" -import { ToolFailure } from "@opencode-ai/llm" -import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route" +import { LLMEvent, ToolFailure } from "@opencode-ai/llm" +import { LLMClient, RequestExecutor, WebSocketExecutor, type LLMClientShape } from "@opencode-ai/llm/route" import { jsonSchema, tool, type ModelMessage, type Tool } from "ai" -import { Effect, Layer, Stream } from "effect" +import { Effect, Fiber, Layer, Stream } from "effect" import { LLMNative } from "@/session/llm/native-request" import { LLMNativeRuntime } from "@/session/llm/native-runtime" import type { Provider } from "@/provider/provider" @@ -535,6 +535,66 @@ describe("session.llm-native.request", () => { }), ) + it.effect("emits native tool calls before overlapping local settlements complete", () => + Effect.gen(function* () { + const observed: string[] = [] + const started: string[] = [] + let release: (() => void) | undefined + let notifyStarted: (() => void) | undefined + const gate = new Promise((resolve) => { + release = resolve + }) + const bothStarted = new Promise((resolve) => { + notifyStarted = resolve + }) + const lookup = { + description: "Lookup data", + inputSchema: jsonSchema({ type: "object" }), + execute: async (_args: unknown, options: { toolCallId: string }) => { + started.push(options.toolCallId) + if (started.length === 2) notifyStarted?.() + await gate + return { output: options.toolCallId } + }, + } satisfies Tool + const llmClient = { + prepare: () => Effect.die("unused"), + stream: () => + Stream.fromIterable([ + LLMEvent.toolCall({ id: "call-1", name: "lookup", input: {} }), + LLMEvent.toolCall({ id: "call-2", name: "lookup", input: {} }), + LLMEvent.finish({ reason: "tool-calls" }), + ]), + generate: () => Effect.die("unused"), + } as LLMClientShape + const native = LLMNativeRuntime.stream({ + model: baseModel, + provider: providerInfo, + auth: undefined, + llmClient, + messages: [], + tools: { lookup }, + headers: {}, + abort: new AbortController().signal, + }) + expect(native.type).toBe("supported") + if (native.type === "unsupported") throw new Error(native.reason) + + const fiber = yield* native.stream.pipe( + Stream.runForEach((event) => Effect.sync(() => observed.push(event.type))), + Effect.forkScoped, + ) + yield* Effect.promise(() => bothStarted) + + expect(started).toEqual(["call-1", "call-2"]) + expect(observed).toEqual(["tool-call", "tool-call", "finish"]) + + release?.() + yield* Fiber.join(fiber) + expect(observed).toEqual(["tool-call", "tool-call", "finish", "tool-result", "tool-result"]) + }), + ) + it.effect("compiles through the native OpenAI Responses route", () => expectOpenAIResponsesRequest({ history: [storedSession.user("hello")], diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index 06f50017a3..540193388a 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -4,7 +4,7 @@ import { Database } from "@opencode-ai/core/database/database" import { EventV2Bridge } from "@/event-v2-bridge" import { expect } from "bun:test" import { tool } from "ai" -import { Cause, Effect, Exit, Fiber, Layer } from "effect" +import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect" import path from "path" import z from "zod" import type { Agent } from "../../src/agent/agent" @@ -25,11 +25,13 @@ import { SessionSummary } from "../../src/session/summary" import { Snapshot } from "../../src/snapshot" import * as Log from "@opencode-ai/core/util/log" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" -import { provideTmpdirServer } from "../fixture/fixture" +import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { raw, reply, TestLLMServer } from "../lib/llm-server" import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" +import { SessionEvent } from "@opencode-ai/core/session/event" +import { LLMEvent } from "@opencode-ai/llm" void Log.init({ print: false }) @@ -198,6 +200,58 @@ const env = Layer.mergeAll( const it = testEffect(env) +const providerErrorLLM = Layer.succeed( + LLM.Service, + LLM.Service.of({ + stream: () => + Stream.make( + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolInputStart({ id: "call-1", name: "lookup" }), + LLMEvent.toolInputEnd({ id: "call-1", name: "lookup" }), + LLMEvent.toolCall({ id: "call-1", name: "lookup", input: {}, providerExecuted: true }), + LLMEvent.toolResult({ + id: "call-1", + name: "lookup", + result: { type: "error", value: "provider boom" }, + providerExecuted: true, + }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ), + }), +) +const providerErrorEnv = SessionProcessor.layer.pipe( + Layer.provide(summary), + Layer.provide(Image.defaultLayer), + Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), + Layer.provide(providerErrorLLM), + Layer.provideMerge(deps), +) +const itProviderError = testEffect(providerErrorEnv) + +const fragmentFailureLLM = Layer.succeed( + LLM.Service, + LLM.Service.of({ + stream: () => + Stream.make( + LLMEvent.stepStart({ index: 0 }), + LLMEvent.reasoningStart({ id: "reasoning-1" }), + LLMEvent.reasoningDelta({ id: "reasoning-1", text: "thinking" }), + LLMEvent.textStart({ id: "text-1" }), + LLMEvent.textDelta({ id: "text-1", text: "partial" }), + LLMEvent.providerError({ message: "provider boom" }), + ), + }), +) +const fragmentFailureEnv = SessionProcessor.layer.pipe( + Layer.provide(summary), + Layer.provide(Image.defaultLayer), + Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), + Layer.provide(fragmentFailureLLM), + Layer.provideMerge(deps), +) +const itFragmentFailure = testEffect(fragmentFailureEnv) + const boot = Effect.fn("test.boot")(function* () { const processors = yield* SessionProcessor.Service const session = yield* Session.Service @@ -936,3 +990,109 @@ it.live("session.processor effect tests mark interruptions aborted without manua { config: (url) => providerCfg(url) }, ), ) + +itProviderError.live("session.processor effect tests fail provider-executed error results", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const { processors, session, provider } = yield* boot() + const events = yield* EventV2Bridge.Service + + const chat = yield* session.create({}) + const parent = yield* user(chat.id, "provider tool error") + const msg = yield* assistant(chat.id, parent.id, path.resolve(dir)) + const mdl = yield* provider.getModel(ref.providerID, ref.modelID) + const settlements: Array = [] + const off = yield* events.listen((event) => { + if (event.type === SessionEvent.Tool.Failed.type) settlements.push(event as typeof SessionEvent.Tool.Failed.Type) + return Effect.void + }) + const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl }) + + yield* handle.process({ + user: { + id: parent.id, + sessionID: chat.id, + role: "user", + time: parent.time, + agent: parent.agent, + model: { providerID: ref.providerID, modelID: ref.modelID }, + } satisfies SessionV1.User, + sessionID: chat.id, + model: mdl, + agent: agent(), + system: [], + messages: [{ role: "user", content: "provider tool error" }], + tools: {}, + }) + yield* off + + const parts = yield* MessageV2.parts(msg.id) + const call = parts.find((part): part is SessionV1.ToolPart => part.type === "tool") + expect(call?.state.status).toBe("error") + if (call?.state.status === "error") expect(call.state.error).toBe("provider boom") + expect(settlements).toHaveLength(1) + expect(settlements[0]?.data).toMatchObject({ + callID: "call-1", + error: { type: "unknown", message: "provider boom" }, + result: { type: "error", value: "provider boom" }, + provider: { executed: true }, + }) + }), + { config: cfg }, + ), +) + +itFragmentFailure.live("session.processor effect tests flush partial v2 fragments before step failure", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const { processors, session, provider } = yield* boot() + const events = yield* EventV2Bridge.Service + + const chat = yield* session.create({}) + const parent = yield* user(chat.id, "provider failure") + const msg = yield* assistant(chat.id, parent.id, path.resolve(dir)) + const mdl = yield* provider.getModel(ref.providerID, ref.modelID) + const seen: string[] = [] + let text: string | undefined + let reasoning: string | undefined + const off = yield* events.listen((event) => { + seen.push(event.type) + if (event.type === SessionEvent.Text.Ended.type) text = (event.data as typeof SessionEvent.Text.Ended.data.Type).text + if (event.type === SessionEvent.Reasoning.Ended.type) + reasoning = (event.data as typeof SessionEvent.Reasoning.Ended.data.Type).text + return Effect.void + }) + const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl }) + + expect( + yield* handle.process({ + user: { + id: parent.id, + sessionID: chat.id, + role: "user", + time: parent.time, + agent: parent.agent, + model: { providerID: ref.providerID, modelID: ref.modelID }, + } satisfies SessionV1.User, + sessionID: chat.id, + model: mdl, + agent: agent(), + system: [], + messages: [{ role: "user", content: "provider failure" }], + tools: {}, + }), + ).toBe("stop") + yield* off + + const failed = seen.indexOf(SessionEvent.Step.Failed.type) + expect(failed).toBeGreaterThan(-1) + expect(seen.indexOf(SessionEvent.Text.Ended.type)).toBeLessThan(failed) + expect(seen.indexOf(SessionEvent.Reasoning.Ended.type)).toBeLessThan(failed) + expect(text).toBe("partial") + expect(reasoning).toBe("thinking") + }), + { config: cfg }, + ), +) diff --git a/packages/opencode/test/v2/session-message-updater.test.ts b/packages/opencode/test/v2/session-message-updater.test.ts index 365a8af20f..d240324c28 100644 --- a/packages/opencode/test/v2/session-message-updater.test.ts +++ b/packages/opencode/test/v2/session-message-updater.test.ts @@ -7,14 +7,16 @@ import { ModelV2 } from "@opencode-ai/core/model" import { ProviderV2 } from "@opencode-ai/core/provider" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater" +import { ToolOutput } from "@opencode-ai/core/tool-output" test.skip("step snapshots carry over to assistant messages", () => { const state: SessionMessageUpdater.MemoryState = { messages: [] } const sessionID = SessionID.make("session") + const assistantMessageID = EventV2.ID.create() Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: EventV2.ID.create(), + id: assistantMessageID, type: "session.next.step.started", data: { sessionID, @@ -36,6 +38,7 @@ test.skip("step snapshots carry over to assistant messages", () => { type: "session.next.step.ended", data: { sessionID, + assistantMessageID, timestamp: DateTime.makeUnsafe(2), finish: "stop", cost: 0, @@ -84,6 +87,7 @@ test.skip("text ended populates assistant text content", () => { data: { sessionID, timestamp: DateTime.makeUnsafe(2), + textID: "text-1", }, } satisfies SessionEvent.Event), ) @@ -95,6 +99,7 @@ test.skip("text ended populates assistant text content", () => { data: { sessionID, timestamp: DateTime.makeUnsafe(3), + textID: "text-1", text: "hello assistant", }, } satisfies SessionEvent.Event), @@ -102,17 +107,18 @@ test.skip("text ended populates assistant text content", () => { expect(state.messages[0]?.type).toBe("assistant") if (state.messages[0]?.type !== "assistant") return - expect(state.messages[0].content).toEqual([{ type: "text", text: "hello assistant" }]) + expect(state.messages[0].content).toEqual([{ type: "text", id: "text-1", text: "hello assistant" }]) }) test.skip("tool completion stores completed timestamp", () => { const state: SessionMessageUpdater.MemoryState = { messages: [] } const sessionID = SessionID.make("session") const callID = "call" + const assistantMessageID = EventV2.ID.create() Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: EventV2.ID.create(), + id: assistantMessageID, type: "session.next.step.started", data: { sessionID, @@ -133,6 +139,7 @@ test.skip("tool completion stores completed timestamp", () => { type: "session.next.tool.input.started", data: { sessionID, + assistantMessageID, timestamp: DateTime.makeUnsafe(2), callID, name: "bash", @@ -146,11 +153,12 @@ test.skip("tool completion stores completed timestamp", () => { type: "session.next.tool.called", data: { sessionID, + assistantMessageID, timestamp: DateTime.makeUnsafe(3), callID, tool: "bash", input: { command: "pwd" }, - provider: { executed: true, metadata: { source: "provider" } }, + provider: { executed: true, metadata: { fake: { source: "provider" } } }, }, } satisfies SessionEvent.Event), ) @@ -161,11 +169,12 @@ test.skip("tool completion stores completed timestamp", () => { type: "session.next.tool.success", data: { sessionID, + assistantMessageID, timestamp: DateTime.makeUnsafe(4), callID, structured: {}, - content: [{ type: "text", text: "/tmp" }], - provider: { executed: true, metadata: { status: "done" } }, + content: [ToolOutput.text({ type: "text", text: "/tmp" })], + provider: { executed: true, metadata: { fake: { status: "done" } } }, }, } satisfies SessionEvent.Event), ) @@ -175,7 +184,7 @@ test.skip("tool completion stores completed timestamp", () => { expect(state.messages[0].content[0]?.type).toBe("tool") if (state.messages[0].content[0]?.type !== "tool") return expect(state.messages[0].content[0].time.completed).toEqual(DateTime.makeUnsafe(4)) - expect(state.messages[0].content[0].provider).toEqual({ executed: true, metadata: { status: "done" } }) + expect(state.messages[0].content[0].provider).toEqual({ executed: true, metadata: { fake: { status: "done" } } }) }) test.skip("compaction events reduce to compaction message", () => { diff --git a/packages/sdk/js/src/v2/client.ts b/packages/sdk/js/src/v2/client.ts index 1c8afc0d64..61230cb927 100644 --- a/packages/sdk/js/src/v2/client.ts +++ b/packages/sdk/js/src/v2/client.ts @@ -1,4 +1,9 @@ export * from "./gen/types.gen.js" +export type { + FileSystemBinaryContent as LocationFileSystemBinaryContent, + FileSystemEntry as LocationFileSystemEntry, + FileSystemTextContent as LocationFileSystemTextContent, +} from "./gen/types.gen.js" import { createClient } from "./gen/client/client.gen.js" import { type Config } from "./gen/client/types.gen.js" @@ -30,8 +35,10 @@ function rewrite(request: Request, values: { directory?: string; workspace?: str key === "directory" ? encodeURIComponent : undefined, ) if (!value) continue - if (!url.searchParams.has(key)) { - url.searchParams.set(key, value) + for (const query of url.pathname.startsWith("/api/") ? [key, `location[${key}]`] : [key]) { + if (!url.searchParams.has(query)) { + url.searchParams.set(query, value) + } } changed = true } diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index e586ba61d8..a0492f681e 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -166,6 +166,7 @@ import type { QuestionRejectResponses, QuestionReplyErrors, QuestionReplyResponses, + QuestionV2Reply, SessionAbortErrors, SessionAbortResponses, SessionChildrenErrors, @@ -178,7 +179,6 @@ import type { SessionDeleteMessageErrors, SessionDeleteMessageResponses, SessionDeleteResponses, - SessionDelivery, SessionDiffErrors, SessionDiffResponses, SessionForkErrors, @@ -271,6 +271,8 @@ import type { V2ProviderGetResponses, V2ProviderListErrors, V2ProviderListResponses, + V2QuestionRequestListErrors, + V2QuestionRequestListResponses, V2SessionCompactErrors, V2SessionCompactResponses, V2SessionContextErrors, @@ -285,6 +287,10 @@ import type { V2SessionPermissionReplyResponses, V2SessionPromptErrors, V2SessionPromptResponses, + V2SessionQuestionRejectErrors, + V2SessionQuestionRejectResponses, + V2SessionQuestionReplyErrors, + V2SessionQuestionReplyResponses, V2SessionWaitErrors, V2SessionWaitResponses, VcsApplyErrors, @@ -4525,6 +4531,83 @@ export class Permission2 extends HeyApiClient { } } +export class Question2 extends HeyApiClient { + /** + * Reply to pending question request + * + * Answer a pending question request owned by a session. + */ + public reply( + parameters: { + sessionID: string + requestID: string + questionV2Reply: QuestionV2Reply + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "requestID" }, + { key: "questionV2Reply", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionQuestionReplyResponses, + V2SessionQuestionReplyErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/question/request/{requestID}/reply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Reject pending question request + * + * Reject a pending question request owned by a session. + */ + public reject( + parameters: { + sessionID: string + requestID: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "requestID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionQuestionRejectResponses, + V2SessionQuestionRejectErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/question/request/{requestID}/reject", + ...options, + ...params, + }) + } +} + export class Session3 extends HeyApiClient { /** * List v2 sessions @@ -4571,15 +4654,17 @@ export class Session3 extends HeyApiClient { /** * Send v2 message * - * Create a v2 session message and queue it for the agent loop. + * Durably admit one v2 session input and schedule agent-loop execution unless resume is false. */ public prompt( parameters: { sessionID: string directory?: string workspace?: string + id?: string prompt?: Prompt - delivery?: SessionDelivery + delivery?: "steer" | "queue" + resume?: boolean }, options?: Options, ) { @@ -4591,8 +4676,10 @@ export class Session3 extends HeyApiClient { { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "id" }, { in: "body", key: "prompt" }, { in: "body", key: "delivery" }, + { in: "body", key: "resume" }, ], }, ], @@ -4747,6 +4834,11 @@ export class Session3 extends HeyApiClient { get permission(): Permission2 { return (this._permission ??= new Permission2({ client: this.client })) } + + private _question?: Question2 + get question(): Question2 { + return (this._question ??= new Question2({ client: this.client })) + } } export class Model extends HeyApiClient { @@ -4990,6 +5082,41 @@ export class Fs extends HeyApiClient { } } +export class Request2 extends HeyApiClient { + /** + * List pending question requests + * + * Retrieve pending question requests for a location. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get< + V2QuestionRequestListResponses, + V2QuestionRequestListErrors, + ThrowOnError + >({ + url: "/api/question/request", + ...options, + ...params, + }) + } +} + +export class Question3 extends HeyApiClient { + private _request?: Request2 + get request(): Request2 { + return (this._request ??= new Request2({ client: this.client })) + } +} + export class V2 extends HeyApiClient { private _session?: Session3 get session(): Session3 { @@ -5015,6 +5142,11 @@ export class V2 extends HeyApiClient { get fs(): Fs { return (this._fs ??= new Fs({ client: this.client })) } + + private _question?: Question3 + get question(): Question3 { + return (this._question ??= new Question3({ client: this.client })) + } } export class Control extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index c738c38710..732985cef5 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -57,6 +57,10 @@ export type Event = | EventPtyUpdated | EventPtyExited | EventPtyDeleted + | EventQuestionV2Asked + | EventQuestionV2Replied + | EventQuestionV2Rejected + | EventTodoUpdated | EventLspUpdated | EventPermissionAsked | EventPermissionReplied @@ -75,7 +79,6 @@ export type Event = | EventSessionStatus | EventSessionIdle | EventSessionCompacted - | EventTodoUpdated | EventVcsBranchUpdated | EventWorktreeReady | EventWorktreeFailed @@ -631,6 +634,21 @@ export type Pty = { pid: number } +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string +} + export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -688,21 +706,6 @@ export type SessionStatus = type: "busy" } -export type Todo = { - /** - * Brief description of the task - */ - content: string - /** - * Current status of the task: pending, in_progress, completed, cancelled - */ - status: string - /** - * Priority level of the task: high, medium, low - */ - priority: string -} - export type GlobalEvent = { directory: string project?: string @@ -816,6 +819,7 @@ export type GlobalEvent = { timestamp: number sessionID: string prompt: Prompt + delivery: "steer" | "queue" } } | { @@ -868,6 +872,7 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + assistantMessageID: string finish: string cost: number tokens: { @@ -888,6 +893,7 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + assistantMessageID: string error: SessionErrorUnknown } } @@ -897,6 +903,7 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + textID: string } } | { @@ -905,6 +912,7 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + textID: string delta: string } } @@ -914,6 +922,7 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + textID: string text: string } } @@ -924,6 +933,11 @@ export type GlobalEvent = { timestamp: number sessionID: string reasoningID: string + providerMetadata?: { + [key: string]: { + [key: string]: unknown + } + } } } | { @@ -944,6 +958,11 @@ export type GlobalEvent = { sessionID: string reasoningID: string text: string + providerMetadata?: { + [key: string]: { + [key: string]: unknown + } + } } } | { @@ -952,6 +971,7 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string name: string } @@ -962,6 +982,7 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string delta: string } @@ -972,6 +993,7 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string text: string } @@ -982,6 +1004,7 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string tool: string input: { @@ -990,7 +1013,9 @@ export type GlobalEvent = { provider: { executed: boolean metadata?: { - [key: string]: unknown + [key: string]: { + [key: string]: unknown + } } } } @@ -1001,6 +1026,7 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string structured: { [key: string]: unknown @@ -1014,15 +1040,19 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string structured: { [key: string]: unknown } content: Array + result?: unknown provider: { executed: boolean metadata?: { - [key: string]: unknown + [key: string]: { + [key: string]: unknown + } } } } @@ -1033,12 +1063,16 @@ export type GlobalEvent = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string error: SessionErrorUnknown + result?: unknown provider: { executed: boolean metadata?: { - [key: string]: unknown + [key: string]: { + [key: string]: unknown + } } } } @@ -1220,6 +1254,44 @@ export type GlobalEvent = { id: string } } + | { + id: string + type: "question.v2.asked" + properties: { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionV2Tool + } + } + | { + id: string + type: "question.v2.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } + } + | { + id: string + type: "question.v2.rejected" + properties: { + sessionID: string + requestID: string + } + } + | { + id: string + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } + } | { id: string type: "lsp.updated" @@ -1416,14 +1488,6 @@ export type GlobalEvent = { sessionID: string } } - | { - id: string - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } - } | { id: string type: "vcs.branch.updated" @@ -1500,13 +1564,10 @@ export type GlobalEvent = { | SyncEventSessionNextStepEnded | SyncEventSessionNextStepFailed | SyncEventSessionNextTextStarted - | SyncEventSessionNextTextDelta | SyncEventSessionNextTextEnded | SyncEventSessionNextReasoningStarted - | SyncEventSessionNextReasoningDelta | SyncEventSessionNextReasoningEnded | SyncEventSessionNextToolInputStarted - | SyncEventSessionNextToolInputDelta | SyncEventSessionNextToolInputEnded | SyncEventSessionNextToolCalled | SyncEventSessionNextToolProgress @@ -2516,6 +2577,12 @@ export type UnauthorizedError = { message: string } +export type ConflictError = { + _tag: "ConflictError" + message: string + resource?: string +} + export type SessionNotFoundError = { _tag: "SessionNotFoundError" sessionID: string @@ -2796,7 +2863,19 @@ export type ToolTextContent = { export type ToolFileContent = { type: "file" - uri: string + source: + | { + type: "data" + data: string + } + | { + type: "url" + url: string + } + | { + type: "file" + uri: string + } mime: string name?: string } @@ -2846,6 +2925,41 @@ export type AuthInfo = { credential: AuthCredential } +export type QuestionV2Option = { + /** + * Display text (1-5 words, concise) + */ + label: string + /** + * Explanation of choice + */ + description: string +} + +export type QuestionV2Info = { + /** + * Complete question + */ + question: string + /** + * Very short label (max 30 chars) + */ + header: string + /** + * Available choices + */ + options: Array + multiple?: boolean + custom?: boolean +} + +export type QuestionV2Tool = { + messageID: string + callID: string +} + +export type QuestionV2Answer = Array + export type EventServerInstanceDisposed = { id: string type: "server.instance.disposed" @@ -2980,6 +3094,7 @@ export type SyncEventSessionNextPrompted = { timestamp: number sessionID: string prompt: Prompt + delivery: "steer" | "queue" } } @@ -3045,13 +3160,14 @@ export type SyncEventSessionNextStepStarted = { export type SyncEventSessionNextStepEnded = { type: "sync" - name: "session.next.step.ended.1" + name: "session.next.step.ended.2" id: string seq: number aggregateID: "sessionID" data: { timestamp: number sessionID: string + assistantMessageID: string finish: string cost: number tokens: { @@ -3069,13 +3185,14 @@ export type SyncEventSessionNextStepEnded = { export type SyncEventSessionNextStepFailed = { type: "sync" - name: "session.next.step.failed.1" + name: "session.next.step.failed.2" id: string seq: number aggregateID: "sessionID" data: { timestamp: number sessionID: string + assistantMessageID: string error: SessionErrorUnknown } } @@ -3089,19 +3206,7 @@ export type SyncEventSessionNextTextStarted = { data: { timestamp: number sessionID: string - } -} - -export type SyncEventSessionNextTextDelta = { - type: "sync" - name: "session.next.text.delta.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - delta: string + textID: string } } @@ -3114,6 +3219,7 @@ export type SyncEventSessionNextTextEnded = { data: { timestamp: number sessionID: string + textID: string text: string } } @@ -3128,20 +3234,11 @@ export type SyncEventSessionNextReasoningStarted = { timestamp: number sessionID: string reasoningID: string - } -} - -export type SyncEventSessionNextReasoningDelta = { - type: "sync" - name: "session.next.reasoning.delta.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - reasoningID: string - delta: string + providerMetadata?: { + [key: string]: { + [key: string]: unknown + } + } } } @@ -3156,6 +3253,11 @@ export type SyncEventSessionNextReasoningEnded = { sessionID: string reasoningID: string text: string + providerMetadata?: { + [key: string]: { + [key: string]: unknown + } + } } } @@ -3168,25 +3270,12 @@ export type SyncEventSessionNextToolInputStarted = { data: { timestamp: number sessionID: string + assistantMessageID: string callID: string name: string } } -export type SyncEventSessionNextToolInputDelta = { - type: "sync" - name: "session.next.tool.input.delta.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - callID: string - delta: string - } -} - export type SyncEventSessionNextToolInputEnded = { type: "sync" name: "session.next.tool.input.ended.1" @@ -3196,6 +3285,7 @@ export type SyncEventSessionNextToolInputEnded = { data: { timestamp: number sessionID: string + assistantMessageID: string callID: string text: string } @@ -3210,6 +3300,7 @@ export type SyncEventSessionNextToolCalled = { data: { timestamp: number sessionID: string + assistantMessageID: string callID: string tool: string input: { @@ -3218,7 +3309,9 @@ export type SyncEventSessionNextToolCalled = { provider: { executed: boolean metadata?: { - [key: string]: unknown + [key: string]: { + [key: string]: unknown + } } } } @@ -3233,6 +3326,7 @@ export type SyncEventSessionNextToolProgress = { data: { timestamp: number sessionID: string + assistantMessageID: string callID: string structured: { [key: string]: unknown @@ -3250,15 +3344,19 @@ export type SyncEventSessionNextToolSuccess = { data: { timestamp: number sessionID: string + assistantMessageID: string callID: string structured: { [key: string]: unknown } content: Array + result?: unknown provider: { executed: boolean metadata?: { - [key: string]: unknown + [key: string]: { + [key: string]: unknown + } } } } @@ -3273,12 +3371,16 @@ export type SyncEventSessionNextToolFailed = { data: { timestamp: number sessionID: string + assistantMessageID: string callID: string error: SessionErrorUnknown + result?: unknown provider: { executed: boolean metadata?: { - [key: string]: unknown + [key: string]: { + [key: string]: unknown + } } } } @@ -3387,7 +3489,20 @@ export type SessionV2Info = { subpath?: string } -export type SessionDelivery = "immediate" | "deferred" +export type SessionMessageUser = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + text: string + files?: Array + agents?: Array + references?: Array + type: "user" +} export type SessionMessageAgentSwitched = { id: string @@ -3417,21 +3532,6 @@ export type SessionMessageModelSwitched = { } } -export type SessionMessageUser = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - text: string - files?: Array - agents?: Array - references?: Array - type: "user" -} - export type SessionMessageSynthetic = { id: string metadata?: { @@ -3462,6 +3562,7 @@ export type SessionMessageShell = { export type SessionMessageAssistantText = { type: "text" + id: string text: string } @@ -3469,6 +3570,11 @@ export type SessionMessageAssistantReasoning = { type: "reasoning" id: string text: string + providerMetadata?: { + [key: string]: { + [key: string]: unknown + } + } } export type SessionMessageToolStatePending = { @@ -3497,6 +3603,7 @@ export type SessionMessageToolStateCompleted = { structured: { [key: string]: unknown } + result?: unknown } export type SessionMessageToolStateError = { @@ -3509,6 +3616,7 @@ export type SessionMessageToolStateError = { [key: string]: unknown } error: SessionErrorUnknown + result?: unknown } export type SessionMessageAssistantTool = { @@ -3518,7 +3626,14 @@ export type SessionMessageAssistantTool = { provider?: { executed: boolean metadata?: { - [key: string]: unknown + [key: string]: { + [key: string]: unknown + } + } + resultMetadata?: { + [key: string]: { + [key: string]: unknown + } } } state: @@ -3592,7 +3707,56 @@ export type SessionMessage = | SessionMessageAssistant | SessionMessageCompaction -export type ProviderV2Info = { +export type ModelV2PublicInfo = { + id: string + providerID: string + family?: string + name: string + api: + | { + id: string + type: "aisdk" + package: string + url?: string + } + | { + id: string + type: "native" + url?: string + } + capabilities: { + tools: boolean + input: Array + output: Array + } + variants: Array<{ + id: string + }> + time: { + released: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + } + cost: Array<{ + tier?: { + type: "context" + size: number + } + input: number + output: number + cache: { + read: number + write: number + } + }> + status: "alpha" | "beta" | "deprecated" | "active" + enabled: boolean + limit: { + context: number + input?: number + output: number + } +} + +export type ProviderV2PublicInfo = { id: string name: string enabled: @@ -3607,9 +3771,6 @@ export type ProviderV2Info = { } | { via: "custom" - data: { - [key: string]: unknown - } } env: Array api: @@ -3617,25 +3778,11 @@ export type ProviderV2Info = { type: "aisdk" package: string url?: string - settings?: { - [key: string]: unknown - } } | { type: "native" url?: string - settings: { - [key: string]: unknown - } } - request: { - headers: { - [key: string]: string - } - body: { - [key: string]: unknown - } - } } export type PermissionV2Request = { @@ -3657,26 +3804,43 @@ export type PermissionSavedInfo = { resource: string } -export type LocationFileSystemTextContent = { +export type FileSystemTextContent = { type: "text" content: string mime: string } -export type LocationFileSystemBinaryContent = { +export type FileSystemBinaryContent = { type: "binary" content: string encoding: "base64" mime: string } -export type LocationFileSystemEntry = { +export type FileSystemEntry = { path: string uri: string type: "file" | "directory" mime: string } +export type QuestionV2Request = { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionV2Tool +} + +export type QuestionV2Reply = { + /** + * User answers in order of questions (each answer is an array of selected labels) + */ + answers: Array +} + export type EventModelsDevRefreshed = { id: string type: "models-dev.refreshed" @@ -3867,6 +4031,7 @@ export type EventSessionNextPrompted = { timestamp: number sessionID: string prompt: Prompt + delivery: "steer" | "queue" } } @@ -3924,6 +4089,7 @@ export type EventSessionNextStepEnded = { properties: { timestamp: number sessionID: string + assistantMessageID: string finish: string cost: number tokens: { @@ -3945,6 +4111,7 @@ export type EventSessionNextStepFailed = { properties: { timestamp: number sessionID: string + assistantMessageID: string error: SessionErrorUnknown } } @@ -3955,6 +4122,7 @@ export type EventSessionNextTextStarted = { properties: { timestamp: number sessionID: string + textID: string } } @@ -3964,6 +4132,7 @@ export type EventSessionNextTextDelta = { properties: { timestamp: number sessionID: string + textID: string delta: string } } @@ -3974,6 +4143,7 @@ export type EventSessionNextTextEnded = { properties: { timestamp: number sessionID: string + textID: string text: string } } @@ -3985,6 +4155,11 @@ export type EventSessionNextReasoningStarted = { timestamp: number sessionID: string reasoningID: string + providerMetadata?: { + [key: string]: { + [key: string]: unknown + } + } } } @@ -4007,6 +4182,11 @@ export type EventSessionNextReasoningEnded = { sessionID: string reasoningID: string text: string + providerMetadata?: { + [key: string]: { + [key: string]: unknown + } + } } } @@ -4016,6 +4196,7 @@ export type EventSessionNextToolInputStarted = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string name: string } @@ -4027,6 +4208,7 @@ export type EventSessionNextToolInputDelta = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string delta: string } @@ -4038,6 +4220,7 @@ export type EventSessionNextToolInputEnded = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string text: string } @@ -4049,6 +4232,7 @@ export type EventSessionNextToolCalled = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string tool: string input: { @@ -4057,7 +4241,9 @@ export type EventSessionNextToolCalled = { provider: { executed: boolean metadata?: { - [key: string]: unknown + [key: string]: { + [key: string]: unknown + } } } } @@ -4069,6 +4255,7 @@ export type EventSessionNextToolProgress = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string structured: { [key: string]: unknown @@ -4083,15 +4270,19 @@ export type EventSessionNextToolSuccess = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string structured: { [key: string]: unknown } content: Array + result?: unknown provider: { executed: boolean metadata?: { - [key: string]: unknown + [key: string]: { + [key: string]: unknown + } } } } @@ -4103,12 +4294,16 @@ export type EventSessionNextToolFailed = { properties: { timestamp: number sessionID: string + assistantMessageID: string callID: string error: SessionErrorUnknown + result?: unknown provider: { executed: boolean metadata?: { - [key: string]: unknown + [key: string]: { + [key: string]: unknown + } } } } @@ -4311,6 +4506,48 @@ export type EventPtyDeleted = { } } +export type EventQuestionV2Asked = { + id: string + type: "question.v2.asked" + properties: { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionV2Tool + } +} + +export type EventQuestionV2Replied = { + id: string + type: "question.v2.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } +} + +export type EventQuestionV2Rejected = { + id: string + type: "question.v2.rejected" + properties: { + sessionID: string + requestID: string + } +} + +export type EventTodoUpdated = { + id: string + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + export type EventLspUpdated = { id: string type: "lsp.updated" @@ -4470,15 +4707,6 @@ export type EventSessionCompacted = { } } -export type EventTodoUpdated = { - id: string - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - export type EventVcsBranchUpdated = { id: string type: "vcs.branch.updated" @@ -8100,9 +8328,11 @@ export type V2SessionListResponses = { export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses] export type V2SessionPromptData = { - body?: { + body: { + id?: string prompt: Prompt - delivery?: SessionDelivery + delivery?: "steer" | "queue" + resume?: boolean } path: { sessionID: string @@ -8128,18 +8358,18 @@ export type V2SessionPromptErrors = { */ 404: SessionNotFoundError /** - * ServiceUnavailableError + * ConflictError */ - 503: ServiceUnavailableError + 409: ConflictError } export type V2SessionPromptError = V2SessionPromptErrors[keyof V2SessionPromptErrors] export type V2SessionPromptResponses = { /** - * Session.Message + * Session.Message.User */ - 200: SessionMessage + 200: SessionMessageUser } export type V2SessionPromptResponse = V2SessionPromptResponses[keyof V2SessionPromptResponses] @@ -8351,7 +8581,7 @@ export type V2ModelListResponses = { /** * Success */ - 200: Array + 200: Array } export type V2ModelListResponse = V2ModelListResponses[keyof V2ModelListResponses] @@ -8389,7 +8619,7 @@ export type V2ProviderListResponses = { /** * Success */ - 200: Array + 200: Array } export type V2ProviderListResponse = V2ProviderListResponses[keyof V2ProviderListResponses] @@ -8431,9 +8661,9 @@ export type V2ProviderGetError = V2ProviderGetErrors[keyof V2ProviderGetErrors] export type V2ProviderGetResponses = { /** - * ProviderV2.Info + * ProviderV2.PublicInfo */ - 200: ProviderV2Info + 200: ProviderV2PublicInfo } export type V2ProviderGetResponse = V2ProviderGetResponses[keyof V2ProviderGetResponses] @@ -8508,7 +8738,7 @@ export type V2SessionPermissionListResponses = { export type V2SessionPermissionListResponse = V2SessionPermissionListResponses[keyof V2SessionPermissionListResponses] export type V2SessionPermissionReplyData = { - body?: { + body: { reply: PermissionV2Reply message?: string } @@ -8640,7 +8870,7 @@ export type V2FsReadResponses = { /** * Success */ - 200: LocationFileSystemTextContent | LocationFileSystemBinaryContent + 200: FileSystemTextContent | FileSystemBinaryContent } export type V2FsReadResponse = V2FsReadResponses[keyof V2FsReadResponses] @@ -8676,11 +8906,117 @@ export type V2FsListResponses = { /** * Success */ - 200: Array + 200: Array } export type V2FsListResponse = V2FsListResponses[keyof V2FsListResponses] +export type V2QuestionRequestListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/question/request" +} + +export type V2QuestionRequestListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2QuestionRequestListError = V2QuestionRequestListErrors[keyof V2QuestionRequestListErrors] + +export type V2QuestionRequestListResponses = { + /** + * Success + */ + 200: Array +} + +export type V2QuestionRequestListResponse = V2QuestionRequestListResponses[keyof V2QuestionRequestListResponses] + +export type V2SessionQuestionReplyData = { + body: QuestionV2Reply + path: { + sessionID: string + requestID: string + } + query?: never + url: "/api/session/{sessionID}/question/request/{requestID}/reply" +} + +export type V2SessionQuestionReplyErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError | QuestionNotFoundError + */ + 404: SessionNotFoundError | QuestionNotFoundError +} + +export type V2SessionQuestionReplyError = V2SessionQuestionReplyErrors[keyof V2SessionQuestionReplyErrors] + +export type V2SessionQuestionReplyResponses = { + /** + * + */ + 204: void +} + +export type V2SessionQuestionReplyResponse = V2SessionQuestionReplyResponses[keyof V2SessionQuestionReplyResponses] + +export type V2SessionQuestionRejectData = { + body?: never + path: { + sessionID: string + requestID: string + } + query?: never + url: "/api/session/{sessionID}/question/request/{requestID}/reject" +} + +export type V2SessionQuestionRejectErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError | QuestionNotFoundError + */ + 404: SessionNotFoundError | QuestionNotFoundError +} + +export type V2SessionQuestionRejectError = V2SessionQuestionRejectErrors[keyof V2SessionQuestionRejectErrors] + +export type V2SessionQuestionRejectResponses = { + /** + * + */ + 204: void +} + +export type V2SessionQuestionRejectResponse = V2SessionQuestionRejectResponses[keyof V2SessionQuestionRejectResponses] + export type TuiAppendPromptData = { body?: { text: string diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index ddceaa0b4b..1fa02fa1fb 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8555,11 +8555,11 @@ "security": [], "responses": { "200": { - "description": "Session.Message", + "description": "Session.Message.User", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionMessage" + "$ref": "#/components/schemas/SessionMessageUser" } } } @@ -8594,18 +8594,18 @@ } } }, - "503": { - "description": "ServiceUnavailableError", + "409": { + "description": "ConflictError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" + "$ref": "#/components/schemas/ConflictError" } } } } }, - "description": "Create a v2 session message and queue it for the agent loop.", + "description": "Durably admit one v2 session input and schedule agent-loop execution unless resume is false.", "summary": "Send v2 message", "requestBody": { "content": { @@ -8613,18 +8613,26 @@ "schema": { "type": "object", "properties": { + "id": { + "type": "string" + }, "prompt": { "$ref": "#/components/schemas/Prompt" }, "delivery": { - "$ref": "#/components/schemas/SessionDelivery" + "type": "string", + "enum": ["steer", "queue"] + }, + "resume": { + "type": "boolean" } }, "required": ["prompt"], "additionalProperties": false } } - } + }, + "required": true }, "x-codeSamples": [ { @@ -9066,7 +9074,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ModelV2Info" + "$ref": "#/components/schemas/ModelV2PublicInfo" } } } @@ -9147,7 +9155,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ProviderV2Info" + "$ref": "#/components/schemas/ProviderV2PublicInfo" } } } @@ -9230,11 +9238,11 @@ "security": [], "responses": { "200": { - "description": "ProviderV2.Info", + "description": "ProviderV2.PublicInfo", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProviderV2Info" + "$ref": "#/components/schemas/ProviderV2PublicInfo" } } } @@ -9518,7 +9526,8 @@ "additionalProperties": false } } - } + }, + "required": true }, "x-codeSamples": [ { @@ -9688,10 +9697,10 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/LocationFileSystemTextContent" + "$ref": "#/components/schemas/FileSystemTextContent" }, { - "$ref": "#/components/schemas/LocationFileSystemBinaryContent" + "$ref": "#/components/schemas/FileSystemBinaryContent" } ] } @@ -9779,7 +9788,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/LocationFileSystemEntry" + "$ref": "#/components/schemas/FileSystemEntry" } } } @@ -9816,6 +9825,241 @@ ] } }, + "/api/question/request": { + "get": { + "tags": ["v2 questions"], + "operationId": "v2.question.request.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2Request" + } + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve pending question requests for a location.", + "summary": "List pending question requests", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.question.request.list({\n ...\n})" + } + ] + } + }, + "/api/session/{sessionID}/question/request/{requestID}/reply": { + "post": { + "tags": ["v2 session questions"], + "operationId": "v2.session.question.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^ses" + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^que" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/QuestionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Answer a pending question request owned by a session.", + "summary": "Reply to pending question request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionV2Reply" + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.question.reply({\n ...\n})" + } + ] + } + }, + "/api/session/{sessionID}/question/request/{requestID}/reject": { + "post": { + "tags": ["v2 session questions"], + "operationId": "v2.session.question.reject", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^ses" + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^que" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/QuestionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Reject a pending question request owned by a session.", + "summary": "Reject pending question request", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.question.reject({\n ...\n})" + } + ] + } + }, "/tui/append-prompt": { "post": { "tags": ["tui"], @@ -11444,6 +11688,18 @@ { "$ref": "#/components/schemas/EventPtyDeleted" }, + { + "$ref": "#/components/schemas/EventQuestionV2Asked" + }, + { + "$ref": "#/components/schemas/EventQuestionV2Replied" + }, + { + "$ref": "#/components/schemas/EventQuestionV2Rejected" + }, + { + "$ref": "#/components/schemas/EventTodoUpdated" + }, { "$ref": "#/components/schemas/EventLspUpdated" }, @@ -11498,9 +11754,6 @@ { "$ref": "#/components/schemas/EventSessionCompacted" }, - { - "$ref": "#/components/schemas/EventTodoUpdated" - }, { "$ref": "#/components/schemas/EventVcsBranchUpdated" }, @@ -11953,7 +12206,7 @@ "type": "object", "properties": { "created": { - "type": "integer", + "type": "number", "minimum": 0 } }, @@ -13203,6 +13456,25 @@ "required": ["id", "title", "command", "args", "cwd", "status", "pid"], "additionalProperties": false }, + "Todo": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Brief description of the task" + }, + "status": { + "type": "string", + "description": "Current status of the task: pending, in_progress, completed, cancelled" + }, + "priority": { + "type": "string", + "description": "Priority level of the task: high, medium, low" + } + }, + "required": ["content", "status", "priority"], + "additionalProperties": false + }, "QuestionOption": { "type": "object", "properties": { @@ -13339,25 +13611,6 @@ } ] }, - "Todo": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "Brief description of the task" - }, - "status": { - "type": "string", - "description": "Current status of the task: pending, in_progress, completed, cancelled" - }, - "priority": { - "type": "string", - "description": "Priority level of the task: high, medium, low" - } - }, - "required": ["content", "status", "priority"], - "additionalProperties": false - }, "GlobalEvent": { "type": "object", "properties": { @@ -13740,9 +13993,13 @@ }, "prompt": { "$ref": "#/components/schemas/Prompt" + }, + "delivery": { + "type": "string", + "enum": ["steer", "queue"] } }, - "required": ["timestamp", "sessionID", "prompt"], + "required": ["timestamp", "sessionID", "prompt", "delivery"], "additionalProperties": false } }, @@ -13918,6 +14175,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "finish": { "type": "string" }, @@ -13957,7 +14217,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "finish", "cost", "tokens"], + "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], "additionalProperties": false } }, @@ -13984,11 +14244,14 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "error": { "$ref": "#/components/schemas/SessionErrorUnknown" } }, - "required": ["timestamp", "sessionID", "error"], + "required": ["timestamp", "sessionID", "assistantMessageID", "error"], "additionalProperties": false } }, @@ -14014,9 +14277,12 @@ "sessionID": { "type": "string", "pattern": "^ses" + }, + "textID": { + "type": "string" } }, - "required": ["timestamp", "sessionID"], + "required": ["timestamp", "sessionID", "textID"], "additionalProperties": false } }, @@ -14043,11 +14309,14 @@ "type": "string", "pattern": "^ses" }, + "textID": { + "type": "string" + }, "delta": { "type": "string" } }, - "required": ["timestamp", "sessionID", "delta"], + "required": ["timestamp", "sessionID", "textID", "delta"], "additionalProperties": false } }, @@ -14074,11 +14343,14 @@ "type": "string", "pattern": "^ses" }, + "textID": { + "type": "string" + }, "text": { "type": "string" } }, - "required": ["timestamp", "sessionID", "text"], + "required": ["timestamp", "sessionID", "textID", "text"], "additionalProperties": false } }, @@ -14107,6 +14379,12 @@ }, "reasoningID": { "type": "string" + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["timestamp", "sessionID", "reasoningID"], @@ -14175,6 +14453,12 @@ }, "text": { "type": "string" + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["timestamp", "sessionID", "reasoningID", "text"], @@ -14204,6 +14488,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -14211,7 +14498,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "callID", "name"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], "additionalProperties": false } }, @@ -14238,6 +14525,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -14245,7 +14535,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "callID", "delta"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], "additionalProperties": false } }, @@ -14272,6 +14562,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -14279,7 +14572,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "callID", "text"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], "additionalProperties": false } }, @@ -14306,6 +14599,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -14322,14 +14618,17 @@ "type": "boolean" }, "metadata": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["executed"], "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "tool", "input", "provider"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], "additionalProperties": false } }, @@ -14356,6 +14655,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -14376,7 +14678,7 @@ } } }, - "required": ["timestamp", "sessionID", "callID", "structured", "content"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], "additionalProperties": false } }, @@ -14403,6 +14705,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -14422,6 +14727,7 @@ ] } }, + "result": {}, "provider": { "type": "object", "properties": { @@ -14429,14 +14735,25 @@ "type": "boolean" }, "metadata": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["executed"], "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "structured", "content", "provider"], + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "callID", + "structured", + "content", + "provider" + ], "additionalProperties": false } }, @@ -14463,12 +14780,16 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, "error": { "$ref": "#/components/schemas/SessionErrorUnknown" }, + "result": {}, "provider": { "type": "object", "properties": { @@ -14476,14 +14797,17 @@ "type": "boolean" }, "metadata": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["executed"], "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "error", "provider"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], "additionalProperties": false } }, @@ -15102,6 +15426,140 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["question.v2.asked"] + }, + "properties": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^que" + }, + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2Info" + }, + "description": "Questions to ask" + }, + "tool": { + "$ref": "#/components/schemas/QuestionV2Tool" + } + }, + "required": ["id", "sessionID", "questions"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["question.v2.replied"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "requestID": { + "type": "string", + "pattern": "^que" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2Answer" + } + } + }, + "required": ["sessionID", "requestID", "answers"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["question.v2.rejected"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "requestID": { + "type": "string", + "pattern": "^que" + } + }, + "required": ["sessionID", "requestID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["todo.updated"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "todos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": ["sessionID", "todos"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -15722,37 +16180,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["todo.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - }, - "required": ["sessionID", "todos"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, { "type": "object", "properties": { @@ -15994,27 +16421,18 @@ { "$ref": "#/components/schemas/SyncEventSessionNextTextStarted" }, - { - "$ref": "#/components/schemas/SyncEventSessionNextTextDelta" - }, { "$ref": "#/components/schemas/SyncEventSessionNextTextEnded" }, { "$ref": "#/components/schemas/SyncEventSessionNextReasoningStarted" }, - { - "$ref": "#/components/schemas/SyncEventSessionNextReasoningDelta" - }, { "$ref": "#/components/schemas/SyncEventSessionNextReasoningEnded" }, { "$ref": "#/components/schemas/SyncEventSessionNextToolInputStarted" }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolInputDelta" - }, { "$ref": "#/components/schemas/SyncEventSessionNextToolInputEnded" }, @@ -18821,6 +19239,23 @@ "required": ["_tag", "message"], "additionalProperties": false }, + "ConflictError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["ConflictError"] + }, + "message": { + "type": "string" + }, + "resource": { + "type": "string" + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, "SessionNotFoundError": { "type": "object", "properties": { @@ -19653,8 +20088,51 @@ "type": "string", "enum": ["file"] }, - "uri": { - "type": "string" + "source": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["data"] + }, + "data": { + "type": "string" + } + }, + "required": ["type", "data"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["url"] + }, + "url": { + "type": "string" + } + }, + "required": ["type", "url"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["file"] + }, + "uri": { + "type": "string" + } + }, + "required": ["type", "uri"], + "additionalProperties": false + } + ] }, "mime": { "type": "string" @@ -19663,7 +20141,7 @@ "type": "string" } }, - "required": ["type", "uri", "mime"], + "required": ["type", "source", "mime"], "additionalProperties": false }, "SessionNextRetry_error": { @@ -19788,6 +20266,68 @@ "required": ["id", "serviceID", "description", "credential"], "additionalProperties": false }, + "QuestionV2Option": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Display text (1-5 words, concise)" + }, + "description": { + "type": "string", + "description": "Explanation of choice" + } + }, + "required": ["label", "description"], + "additionalProperties": false + }, + "QuestionV2Info": { + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "Complete question" + }, + "header": { + "type": "string", + "description": "Very short label (max 30 chars)" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2Option" + }, + "description": "Available choices" + }, + "multiple": { + "type": "boolean" + }, + "custom": { + "type": "boolean" + } + }, + "required": ["question", "header", "options"], + "additionalProperties": false + }, + "QuestionV2Tool": { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": ["messageID", "callID"], + "additionalProperties": false + }, + "QuestionV2Answer": { + "type": "array", + "items": { + "type": "string" + } + }, "EventServerInstanceDisposed": { "type": "object", "properties": { @@ -20224,9 +20764,13 @@ }, "prompt": { "$ref": "#/components/schemas/Prompt" + }, + "delivery": { + "type": "string", + "enum": ["steer", "queue"] } }, - "required": ["timestamp", "sessionID", "prompt"], + "required": ["timestamp", "sessionID", "prompt", "delivery"], "additionalProperties": false } }, @@ -20435,7 +20979,7 @@ }, "name": { "type": "string", - "enum": ["session.next.step.ended.1"] + "enum": ["session.next.step.ended.2"] }, "id": { "type": "string" @@ -20457,6 +21001,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "finish": { "type": "string" }, @@ -20496,7 +21043,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "finish", "cost", "tokens"], + "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], "additionalProperties": false } }, @@ -20512,7 +21059,7 @@ }, "name": { "type": "string", - "enum": ["session.next.step.failed.1"] + "enum": ["session.next.step.failed.2"] }, "id": { "type": "string" @@ -20534,11 +21081,14 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "error": { "$ref": "#/components/schemas/SessionErrorUnknown" } }, - "required": ["timestamp", "sessionID", "error"], + "required": ["timestamp", "sessionID", "assistantMessageID", "error"], "additionalProperties": false } }, @@ -20575,51 +21125,12 @@ "sessionID": { "type": "string", "pattern": "^ses" - } - }, - "required": ["timestamp", "sessionID"], - "additionalProperties": false - } - }, - "required": ["type", "name", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - }, - "SyncEventSessionNextTextDelta": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "name": { - "type": "string", - "enum": ["session.next.text.delta.1"] - }, - "id": { - "type": "string" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string", - "enum": ["sessionID"] - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "delta": { + "textID": { "type": "string" } }, - "required": ["timestamp", "sessionID", "delta"], + "required": ["timestamp", "sessionID", "textID"], "additionalProperties": false } }, @@ -20657,11 +21168,14 @@ "type": "string", "pattern": "^ses" }, + "textID": { + "type": "string" + }, "text": { "type": "string" } }, - "required": ["timestamp", "sessionID", "text"], + "required": ["timestamp", "sessionID", "textID", "text"], "additionalProperties": false } }, @@ -20701,6 +21215,12 @@ }, "reasoningID": { "type": "string" + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["timestamp", "sessionID", "reasoningID"], @@ -20710,51 +21230,6 @@ "required": ["type", "name", "id", "seq", "aggregateID", "data"], "additionalProperties": false }, - "SyncEventSessionNextReasoningDelta": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "name": { - "type": "string", - "enum": ["session.next.reasoning.delta.1"] - }, - "id": { - "type": "string" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string", - "enum": ["sessionID"] - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "reasoningID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "reasoningID", "delta"], - "additionalProperties": false - } - }, - "required": ["type", "name", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - }, "SyncEventSessionNextReasoningEnded": { "type": "object", "properties": { @@ -20791,6 +21266,12 @@ }, "text": { "type": "string" + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["timestamp", "sessionID", "reasoningID", "text"], @@ -20831,6 +21312,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -20838,52 +21322,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "callID", "name"], - "additionalProperties": false - } - }, - "required": ["type", "name", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - }, - "SyncEventSessionNextToolInputDelta": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "name": { - "type": "string", - "enum": ["session.next.tool.input.delta.1"] - }, - "id": { - "type": "string" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string", - "enum": ["sessionID"] - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "callID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "callID", "delta"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], "additionalProperties": false } }, @@ -20921,6 +21360,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -20928,7 +21370,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "callID", "text"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], "additionalProperties": false } }, @@ -20966,6 +21408,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -20982,14 +21427,17 @@ "type": "boolean" }, "metadata": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["executed"], "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "tool", "input", "provider"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], "additionalProperties": false } }, @@ -21027,6 +21475,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -21047,7 +21498,7 @@ } } }, - "required": ["timestamp", "sessionID", "callID", "structured", "content"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], "additionalProperties": false } }, @@ -21085,6 +21536,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -21104,6 +21558,7 @@ ] } }, + "result": {}, "provider": { "type": "object", "properties": { @@ -21111,14 +21566,17 @@ "type": "boolean" }, "metadata": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["executed"], "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "structured", "content", "provider"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content", "provider"], "additionalProperties": false } }, @@ -21156,12 +21614,16 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, "error": { "$ref": "#/components/schemas/SessionErrorUnknown" }, + "result": {}, "provider": { "type": "object", "properties": { @@ -21169,14 +21631,17 @@ "type": "boolean" }, "metadata": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["executed"], "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "error", "provider"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], "additionalProperties": false } }, @@ -21402,7 +21867,8 @@ "type": "string" }, "workspaceID": { - "type": "string" + "type": "string", + "pattern": "^wrk" } }, "required": ["directory"], @@ -21502,9 +21968,53 @@ "required": ["id", "projectID", "cost", "tokens", "time", "title", "location"], "additionalProperties": false }, - "SessionDelivery": { - "type": "string", - "enum": ["immediate", "deferred"] + "SessionMessageUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + }, + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptFileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptAgentAttachment" + } + }, + "references": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptReferenceAttachment" + } + }, + "type": { + "type": "string", + "enum": ["user"] + } + }, + "required": ["id", "time", "text", "type"], + "additionalProperties": false }, "SessionMessageAgentSwitched": { "type": "object", @@ -21579,54 +22089,6 @@ "required": ["id", "time", "type", "model"], "additionalProperties": false }, - "SessionMessageUser": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - }, - "text": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptFileAttachment" - } - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptAgentAttachment" - } - }, - "references": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptReferenceAttachment" - } - }, - "type": { - "type": "string", - "enum": ["user"] - } - }, - "required": ["id", "time", "text", "type"], - "additionalProperties": false - }, "SessionMessageSynthetic": { "type": "object", "properties": { @@ -21707,11 +22169,14 @@ "type": "string", "enum": ["text"] }, + "id": { + "type": "string" + }, "text": { "type": "string" } }, - "required": ["type", "text"], + "required": ["type", "id", "text"], "additionalProperties": false }, "SessionMessageAssistantReasoning": { @@ -21726,6 +22191,12 @@ }, "text": { "type": "string" + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["type", "id", "text"], @@ -21806,7 +22277,8 @@ }, "structured": { "type": "object" - } + }, + "result": {} }, "required": ["status", "input", "content", "structured"], "additionalProperties": false @@ -21839,7 +22311,8 @@ }, "error": { "$ref": "#/components/schemas/SessionErrorUnknown" - } + }, + "result": {} }, "required": ["status", "input", "content", "structured", "error"], "additionalProperties": false @@ -21864,7 +22337,16 @@ "type": "boolean" }, "metadata": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "resultMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["executed"], @@ -22086,7 +22568,210 @@ } ] }, - "ProviderV2Info": { + "ModelV2PublicInfo": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "family": { + "type": "string" + }, + "name": { + "type": "string" + }, + "api": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["aisdk"] + }, + "package": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["id", "type", "package"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["native"] + }, + "url": { + "type": "string" + } + }, + "required": ["id", "type"], + "additionalProperties": false + } + ] + }, + "capabilities": { + "type": "object", + "properties": { + "tools": { + "type": "boolean" + }, + "input": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["tools", "input", "output"], + "additionalProperties": false + }, + "variants": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + } + }, + "time": { + "type": "object", + "properties": { + "released": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["released"], + "additionalProperties": false + }, + "cost": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tier": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["context"] + }, + "size": { + "type": "integer" + } + }, + "required": ["type", "size"], + "additionalProperties": false + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": ["read", "write"], + "additionalProperties": false + } + }, + "required": ["input", "output", "cache"], + "additionalProperties": false + } + }, + "status": { + "type": "string", + "enum": ["alpha", "beta", "deprecated", "active"] + }, + "enabled": { + "type": "boolean" + }, + "limit": { + "type": "object", + "properties": { + "context": { + "type": "integer" + }, + "input": { + "type": "integer" + }, + "output": { + "type": "integer" + } + }, + "required": ["context", "output"], + "additionalProperties": false + } + }, + "required": [ + "id", + "providerID", + "name", + "api", + "capabilities", + "variants", + "time", + "cost", + "status", + "enabled", + "limit" + ], + "additionalProperties": false + }, + "ProviderV2PublicInfo": { "type": "object", "properties": { "id": { @@ -22135,12 +22820,9 @@ "via": { "type": "string", "enum": ["custom"] - }, - "data": { - "type": "object" } }, - "required": ["via", "data"], + "required": ["via"], "additionalProperties": false } ] @@ -22165,9 +22847,6 @@ }, "url": { "type": "string" - }, - "settings": { - "type": "object" } }, "required": ["type", "package"], @@ -22182,34 +22861,15 @@ }, "url": { "type": "string" - }, - "settings": { - "type": "object" } }, - "required": ["type", "settings"], + "required": ["type"], "additionalProperties": false } ] - }, - "request": { - "type": "object", - "properties": { - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" - } - }, - "required": ["headers", "body"], - "additionalProperties": false } }, - "required": ["id", "name", "enabled", "env", "api", "request"], + "required": ["id", "name", "enabled", "env", "api"], "additionalProperties": false }, "PermissionV2Request": { @@ -22267,7 +22927,7 @@ "required": ["id", "projectID", "action", "resource"], "additionalProperties": false }, - "LocationFileSystemTextContent": { + "FileSystemTextContent": { "type": "object", "properties": { "type": { @@ -22284,7 +22944,7 @@ "required": ["type", "content", "mime"], "additionalProperties": false }, - "LocationFileSystemBinaryContent": { + "FileSystemBinaryContent": { "type": "object", "properties": { "type": { @@ -22305,7 +22965,7 @@ "required": ["type", "content", "encoding", "mime"], "additionalProperties": false }, - "LocationFileSystemEntry": { + "FileSystemEntry": { "type": "object", "properties": { "path": { @@ -22325,6 +22985,45 @@ "required": ["path", "uri", "type", "mime"], "additionalProperties": false }, + "QuestionV2Request": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^que" + }, + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2Info" + }, + "description": "Questions to ask" + }, + "tool": { + "$ref": "#/components/schemas/QuestionV2Tool" + } + }, + "required": ["id", "sessionID", "questions"], + "additionalProperties": false + }, + "QuestionV2Reply": { + "type": "object", + "properties": { + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2Answer" + }, + "description": "User answers in order of questions (each answer is an array of selected labels)" + } + }, + "required": ["answers"], + "additionalProperties": false + }, "EventModels-devRefreshed": { "type": "object", "properties": { @@ -22927,9 +23626,13 @@ }, "prompt": { "$ref": "#/components/schemas/Prompt" + }, + "delivery": { + "type": "string", + "enum": ["steer", "queue"] } }, - "required": ["timestamp", "sessionID", "prompt"], + "required": ["timestamp", "sessionID", "prompt", "delivery"], "additionalProperties": false } }, @@ -23105,6 +23808,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "finish": { "type": "string" }, @@ -23144,7 +23850,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "finish", "cost", "tokens"], + "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], "additionalProperties": false } }, @@ -23171,11 +23877,14 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "error": { "$ref": "#/components/schemas/SessionErrorUnknown" } }, - "required": ["timestamp", "sessionID", "error"], + "required": ["timestamp", "sessionID", "assistantMessageID", "error"], "additionalProperties": false } }, @@ -23201,9 +23910,12 @@ "sessionID": { "type": "string", "pattern": "^ses" + }, + "textID": { + "type": "string" } }, - "required": ["timestamp", "sessionID"], + "required": ["timestamp", "sessionID", "textID"], "additionalProperties": false } }, @@ -23230,11 +23942,14 @@ "type": "string", "pattern": "^ses" }, + "textID": { + "type": "string" + }, "delta": { "type": "string" } }, - "required": ["timestamp", "sessionID", "delta"], + "required": ["timestamp", "sessionID", "textID", "delta"], "additionalProperties": false } }, @@ -23261,11 +23976,14 @@ "type": "string", "pattern": "^ses" }, + "textID": { + "type": "string" + }, "text": { "type": "string" } }, - "required": ["timestamp", "sessionID", "text"], + "required": ["timestamp", "sessionID", "textID", "text"], "additionalProperties": false } }, @@ -23294,6 +24012,12 @@ }, "reasoningID": { "type": "string" + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["timestamp", "sessionID", "reasoningID"], @@ -23362,6 +24086,12 @@ }, "text": { "type": "string" + }, + "providerMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["timestamp", "sessionID", "reasoningID", "text"], @@ -23391,6 +24121,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -23398,7 +24131,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "callID", "name"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], "additionalProperties": false } }, @@ -23425,6 +24158,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -23432,7 +24168,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "callID", "delta"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], "additionalProperties": false } }, @@ -23459,6 +24195,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -23466,7 +24205,7 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "callID", "text"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], "additionalProperties": false } }, @@ -23493,6 +24232,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -23509,14 +24251,17 @@ "type": "boolean" }, "metadata": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["executed"], "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "tool", "input", "provider"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], "additionalProperties": false } }, @@ -23543,6 +24288,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -23563,7 +24311,7 @@ } } }, - "required": ["timestamp", "sessionID", "callID", "structured", "content"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], "additionalProperties": false } }, @@ -23590,6 +24338,9 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, @@ -23609,6 +24360,7 @@ ] } }, + "result": {}, "provider": { "type": "object", "properties": { @@ -23616,14 +24368,17 @@ "type": "boolean" }, "metadata": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["executed"], "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "structured", "content", "provider"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content", "provider"], "additionalProperties": false } }, @@ -23650,12 +24405,16 @@ "type": "string", "pattern": "^ses" }, + "assistantMessageID": { + "type": "string" + }, "callID": { "type": "string" }, "error": { "$ref": "#/components/schemas/SessionErrorUnknown" }, + "result": {}, "provider": { "type": "object", "properties": { @@ -23663,14 +24422,17 @@ "type": "boolean" }, "metadata": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "object" + } } }, "required": ["executed"], "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "error", "provider"], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], "additionalProperties": false } }, @@ -24289,6 +25051,140 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventQuestionV2Asked": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["question.v2.asked"] + }, + "properties": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^que" + }, + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2Info" + }, + "description": "Questions to ask" + }, + "tool": { + "$ref": "#/components/schemas/QuestionV2Tool" + } + }, + "required": ["id", "sessionID", "questions"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventQuestionV2Replied": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["question.v2.replied"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "requestID": { + "type": "string", + "pattern": "^que" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2Answer" + } + } + }, + "required": ["sessionID", "requestID", "answers"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventQuestionV2Rejected": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["question.v2.rejected"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "requestID": { + "type": "string", + "pattern": "^que" + } + }, + "required": ["sessionID", "requestID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventTodoUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["todo.updated"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "todos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": ["sessionID", "todos"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventLspUpdated": { "type": "object", "properties": { @@ -24775,37 +25671,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventTodoUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["todo.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - }, - "required": ["sessionID", "todos"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "EventVcsBranchUpdated": { "type": "object", "properties": { @@ -25116,6 +25981,14 @@ "name": "v2 filesystem", "description": "Experimental v2 location-scoped filesystem routes." }, + { + "name": "v2 questions", + "description": "Experimental v2 question routes." + }, + { + "name": "v2 session questions", + "description": "Experimental v2 session question routes." + }, { "name": "tui", "description": "Experimental HttpApi TUI routes." diff --git a/specs/v2/catalog-config-plugin-lifecycle.md b/specs/v2/catalog-config-plugin-lifecycle.md index 986cc019a3..785b6826b9 100644 --- a/specs/v2/catalog-config-plugin-lifecycle.md +++ b/specs/v2/catalog-config-plugin-lifecycle.md @@ -1,5 +1,7 @@ # Catalog / Config / Plugin Lifecycle Options +Status: current core has selected replayable Location-scoped Catalog transforms, aligned with option B. Reload/watch behavior and deferred external plugin activation remain design work; the option comparison below is retained as historical context. + We need to choose where provider/model inputs live and how visible catalog state changes after boot. The designs below compare config, models.dev, auth, plugin activation/disablement, config edits, and policy changes under each option. ## Scenarios diff --git a/specs/v2/provider-model.md b/specs/v2/provider-model.md index 4860cb787d..a80ec1dc43 100644 --- a/specs/v2/provider-model.md +++ b/specs/v2/provider-model.md @@ -44,6 +44,7 @@ export type OpenAICompletions = typeof OpenAICompletions.Type const AISDK = Schema.Struct({ type: Schema.Literal("aisdk"), package: Schema.String, + url: Schema.String.pipe(Schema.optional), }) const AnthropicMessages = Schema.Struct({ @@ -67,13 +68,22 @@ export type Endpoint = typeof Endpoint.Type export const Options = Schema.Struct({ headers: Schema.Record(Schema.String, Schema.String), body: Schema.Record(Schema.String, Schema.Any), + aisdk: Schema.Struct({ + provider: Schema.Record(Schema.String, Schema.Any), + request: Schema.Record(Schema.String, Schema.Any), + }), }) export type Options = typeof Options.Type export class Info extends Schema.Class("ProviderV2.Info")({ id: ID, name: Schema.String, - enabled: Schema.Boolean, + enabled: Schema.Union([ + Schema.Literal(false), + Schema.Struct({ via: Schema.Literal("env"), name: Schema.String }), + Schema.Struct({ via: Schema.Literal("account"), service: Schema.String }), + Schema.Struct({ via: Schema.Literal("custom"), data: Schema.Record(Schema.String, Schema.Any) }), + ]), env: Schema.String.pipe(Schema.Array), endpoint: Endpoint, options: Options, @@ -90,6 +100,7 @@ export class Info extends Schema.Class("ProviderV2.Info")({ options: { headers: {}, body: {}, + aisdk: { provider: {}, request: {} }, }, }) } @@ -149,12 +160,13 @@ export type Limit = typeof Limit.Type export const Ref = Schema.Struct({ id: ID, providerID: ProviderV2.ID, - variant: VariantID, + variant: VariantID.pipe(Schema.optional), }) export type Ref = typeof Ref.Type export class Info extends Schema.Class("ModelV2.Info")({ id: ID, + apiID: ID, providerID: ProviderV2.ID, family: Family.pipe(Schema.optional), name: Schema.String, @@ -170,11 +182,13 @@ export class Info extends Schema.Class("ModelV2.Info")({ }), cost: Cost.pipe(Schema.Array), status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), + enabled: Schema.Boolean, limit: Limit, }) { static empty(providerID: ProviderV2.ID, modelID: ID) { return new Info({ id: modelID, + apiID: modelID, providerID, name: modelID, endpoint: { @@ -188,6 +202,7 @@ export class Info extends Schema.Class("ModelV2.Info")({ options: { headers: {}, body: {}, + aisdk: { provider: {}, request: {} }, }, variants: [], time: { @@ -195,6 +210,7 @@ export class Info extends Schema.Class("ModelV2.Info")({ }, cost: [], status: "active", + enabled: true, limit: { context: 0, output: 0, @@ -208,22 +224,15 @@ export class Info extends Schema.Class("ModelV2.Info")({ ```ts export interface Interface { + readonly transform: State.Interface["transform"] readonly provider: { - readonly get: (providerID: ProviderV2.ID) => Effect.Effect> - readonly update: (providerID: ProviderV2.ID, fn: (provider: Draft) => void) => Effect.Effect - readonly remove: (providerID: ProviderV2.ID) => Effect.Effect + readonly get: (providerID: ProviderV2.ID) => Effect.Effect readonly all: () => Effect.Effect readonly available: () => Effect.Effect } readonly model: { - readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect> - readonly update: ( - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - fn: (model: Draft) => void, - ) => Effect.Effect - readonly remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect + readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect readonly all: () => Effect.Effect readonly available: () => Effect.Effect readonly default: () => Effect.Effect> @@ -232,7 +241,7 @@ export interface Interface { } ``` -`ProviderV2.Info.enabled` is stored provider state. Provider plugins set this field after checking env, account, config, or provider-specific availability. +`ProviderV2.Info.enabled` is stored provider state. Provider plugins set it to `false` or record whether availability comes from environment, account, or custom configuration. `ProviderV2.Endpoint` includes `{ type: "unknown" }`. `CatalogV2.model.get()` and `CatalogV2.model.all()` resolve `unknown` endpoints from the provider before returning models. @@ -247,12 +256,30 @@ type ProviderRecord = { let records = HashMap.empty() ``` -`ModelV2.Info` does not have an `enabled` field. Model availability is derived by `CatalogV2.model.available()` from provider state and model status. +`ModelV2.Info.enabled` stores model availability. `CatalogV2.model.available()` also requires a usable provider. ```ts -const available = provider.enabled && model.status !== "deprecated" +const available = provider.enabled !== false && model.enabled ``` +## Current Session Runner Adaptation + +The first local V2 Session runner waits for Location plugin boot, then resolves an explicit Session model without silently falling back. Without an explicit model it uses a supported Location catalog default, then falls back to the first available model with a supported route, and otherwise fails with `SessionRunnerModel.ModelNotSelectedError`. Its native adaptation surface is deliberately narrow: + +```text +openai/responses over HTTP +openai/completions for OpenAI Chat +openai/completions for OpenAI-compatible Chat +anthropic/messages +aisdk:@ai-sdk/openai +aisdk:@ai-sdk/openai-compatible with an explicit URL +aisdk:@ai-sdk/anthropic +``` + +Native endpoint URLs are complete endpoint URLs and are split into base URL plus request path when building an LLM route. AI SDK endpoint URLs remain base URLs. The adapter preserves model headers and body options, environment-backed provider credentials, direct model API keys, and selected Session variant overlays. + +Unsupported routes fail explicitly with `SessionRunnerModel.UnsupportedEndpointError`. In particular, `openai/responses` with WebSocket transport must not silently downgrade to HTTP. Google, Azure, Bedrock, OpenRouter-specific behavior, GitHub Copilot, Vertex, gateway adapters, and signed authentication remain future provider slices. + ## Plugin Interface ```ts diff --git a/specs/v2/schema-changelog.md b/specs/v2/schema-changelog.md new file mode 100644 index 0000000000..8138227830 --- /dev/null +++ b/specs/v2/schema-changelog.md @@ -0,0 +1,665 @@ +# V2 Schema Changelog + +Record V2 database, durable-event, projected-message, HTTP, and generated SDK schema changes here. Each entry states why the contract changed and whether consumers or stored data need compatibility handling. Commit messages for schema-affecting changes should include the same summary. + +This document covers meaningful contract changes introduced on the `feat/opencode-embedded-api` branch since its divergence from `origin/dev`. Mechanical file moves and internal refactors are omitted unless they changed stored data, replay behavior, public HTTP or SDK shapes, or model-facing tool contracts. + +## Earlier Branch History + +### Replayable Session Event Refinement And Cursor Stream + +Affected schema: + +- Existing synchronized `session.next.*` event family in `packages/core/src/session/event.ts`. +- Existing projected V2 Session-message union in `packages/core/src/session/message.ts`. +- New explicit durable-event union and internal replay cursor returned by `sessions.events({ sessionID, after? })`. + +Change: + +- Keep the existing Session lifecycle event family and projected-message union rather than introducing them in this branch. +- Stop synchronizing text deltas, reasoning deltas, and tool-input deltas; keep them explicitly ephemeral. +- Add an explicit durable-event union for replay-safe consumers. +- Add replay-and-tail aggregate cursors backed by durable Session-event sequence. +- Encode synchronized event payloads before writing JSON storage and decode them while replaying so schema transforms remain explicit at the durable boundary. + +Reason: + +- Embedded Session execution needs a reconnect-safe replay stream over the existing durable log and derived chronological read model. +- Fragment streams are useful to connected renderers but must not advance durable cursors or inflate synchronized storage. + +Compatibility: + +- The `session.next.*` lifecycle event family predates this branch; this branch refines its experimental V2 durability and replay contracts. +- Durable replay cursors are per-aggregate event sequences; ephemeral deltas are intentionally absent after reconnect. + +### Deterministic IDs From External Keys + +Affected schema: + +- Session and Event ID construction helpers. + +Change: + +- Add deterministic `SessionSchema.ID.fromExternal(...)` and `EventV2.ID.fromExternal(...)` constructors for trusted external keys. + +Reason: + +- Embedded adapters need stable local identities when the same external conversation or stimulus is delivered more than once. +- Deterministic IDs let durable admission and event publication retain their idempotency boundaries across retries. + +Compatibility: + +- Existing generated Session and Event IDs retain their current prefixes and generation behavior. +- Deterministic constructors are additive internal helpers; public ID schemas remain strings with their existing prefixes. + +### Durable Step Settlement Ownership + +Affected schema: + +- `session.next.step.ended` and `session.next.step.failed` synchronized event version `2`. + +Change: + +- Bind step settlement to an explicit `assistantMessageID`. + +Reason: + +- Provider-local call identifiers can repeat across turns. + +Compatibility: + +- Step settlement uses synchronized event version `2` because the durable payload changed. + +### Durable Session Input Inbox + +Affected schema: + +- New `session_input` table from `20260603141458_session_input_inbox.ts`. +- Updated pending-input index from `20260603160727_jittery_ezekiel_stane.ts`. +- New `SessionInput.Admitted` schema and `Prompted.delivery` field. +- Prompt-admission conflict behavior in `SessionV2.prompt(...)`. + +Change: + +- Persist admitted prompts before projection with an autoincrement inbox sequence, unique message ID, Session ID, encoded prompt, `steer` or `queue` delivery mode, optional promoted event sequence, and creation time. +- Index pending inputs by Session, promotion state, delivery mode, and admission sequence. + +Reason: + +- Prompt admission and model-visible promotion must be separate durable operations. +- Steering must promote at safe provider-turn boundaries while queued prompts remain separate FIFO activities. + +Compatibility: + +- Database migration creates the inbox table and replaces its first pending index with a delivery-aware index. +- Exact prompt retries are idempotent; reusing a message ID for different input fails. + +### Durable Session Projection Order + +Affected schema: + +- `session_message.seq` from `20260603040000_session_message_projection_order.ts`. +- Session-message and event indexes from `20260603001617_session_message_projection_indexes.ts`, `20260603040000_session_message_projection_order.ts`, and `20260603160727_jittery_ezekiel_stane.ts`. + +Change: + +- Add and backfill `session_message.seq` from matching synchronized events. +- Add event aggregate-sequence and aggregate-type-sequence indexes. +- Add Session-message sequence, type-sequence, and compatibility timestamp indexes. + +Reason: + +- Projected history, replay, compaction lookup, and pagination must follow durable aggregate order rather than timestamps or caller-generated IDs. +- Runner and HTTP read paths need covering indexes for their concrete lookup shapes. + +Compatibility: + +- Migration fails rather than inventing chronology if an existing projected Session message has no matching durable event. +- The timestamp compatibility index remains for legacy or transitional query shapes. + +### Structured Tool Registry And Canonical Output + +Affected schema: + +- Core-owned typed tool registry contract. +- Canonical tool output content and structured settlement schemas. +- Canonical tagged tool file sources in `@opencode-ai/llm`. +- Durable tool called, progress, success, and failure events and projected assistant-tool states. + +Change: + +- Validate model input against each registered tool's parameter schema. +- Validate handler success against each tool's success schema before optional pure model-output lowering. +- Generate optional tool-definition output JSON Schema from typed success schemas. +- Persist canonical structured output and content for running, completed, and failed tools. +- Represent tool files explicitly as inline data, remote URL, or managed file URI sources rather than one ambiguous URI string. + +Reason: + +- Embedded tool execution needs one typed boundary between provider calls, local side effects, durable settlement, and replay. + +Compatibility: + +- These are additive experimental V2 runtime contracts. +- Tool results are durably settled before provider continuation. +- Legacy text, JSON, and inline-media results remain convertible; unresolved URL and file sources must be materialized or explicitly rejected before provider lowering. + +### Managed Tool-Output Resources + +Affected schema: + +- New `ToolOutputStore.Resource` and `ToolOutputStore.Page` schemas. +- New `tool-output://` URI contract. +- `read` tool resource-page input. + +Change: + +- Spill oversized model-facing tool text into Session-owned opaque managed resources. +- Page stored UTF-8 content by byte offset with bounded reads and explicit `truncated` and `next` metadata. + +Reason: + +- Tool results need bounded model context without discarding the full output. +- Opaque Session ownership prevents one Session from reading another Session's managed output. + +Compatibility: + +- This is an additive internal and model-facing resource contract. +- Managed output is retained for a bounded period and is not a public filesystem path. + +### Location-Scoped Filesystem Read And Search Contracts + +Affected schema: + +- Core filesystem read, directory-list, root-resolution, and named-reference inputs. +- `LocationSearch.FilesInput`, `LocationSearch.GrepInput`, and bounded result schemas. +- `read`, `glob`, and `grep` tool parameters and success payloads. + +Change: + +- Add bounded file reads, paged directory listings, bounded glob results, and bounded grep matches with line previews. +- Allow named project references for read-oriented operations. +- Resolve and pin canonical approved search roots before traversal. +- Exclude hidden path segments from broad V2 glob and grep discovery. + +Reason: + +- Embedded tools need deterministic bounds and a shared path-containment authority. +- Broad search should not disclose hidden files implicitly. + +Compatibility: + +- These are additive V2 tool contracts. +- Hidden-file discovery is intentionally narrower than an unconditional ripgrep `--hidden` traversal. + +### Location Workspace Identity + +Affected schema: + +- `Location.Ref.workspaceID`. +- V2 Location HTTP middleware routing. + +Change: + +- Brand optional Location workspace identity as `WorkspaceV2.ID` instead of an untyped string. +- Preserve nested `location[workspace]` and workspace-header routing inputs while decoding them into the branded identity. + +Reason: + +- Location-scoped services and embedded routing need one typed workspace identity boundary. + +Compatibility: + +- Existing workspace strings remain accepted when they satisfy the workspace ID schema. +- Generated OpenAPI reflects the workspace prefix constraint. + +### Structured Mutation Authority And File Leaves + +Affected schema: + +- New `LocationMutation.ResolveInput`, planned target, external-directory authorization, and typed path errors. +- New `write` and exact `edit` tool schemas. +- New internal file-mutation commit service. + +Change: + +- Resolve relative mutation paths within the active Location. +- Accept absolute internal paths and require explicit `external_directory` approval before leaf approval for external absolute paths. +- Keep named references read-oriented and reject them for mutation. +- Revalidate path authority immediately before write mechanics. + +Reason: + +- Mutation tools need explicit capability escalation and symlink/path-swap checks without pretending path APIs provide a syscall-level sandbox. + +Compatibility: + +- These are additive V2 mutation contracts. +- Richer V1 fuzzy edit behavior remains intentionally deferred. + +### V2 Permission Requests And Saved Rules + +Affected schema: + +- `PermissionV2.Request`, `AssertInput`, `ReplyInput`, source metadata, tagged errors, and lifecycle events. +- V2 permission list, reply, and saved-rule HTTP routes and generated SDK schemas. + +Change: + +- Add Location-scoped pending permission requests with `once`, `always`, and `reject` replies. +- Attach optional originating tool message and call IDs. +- Preserve authored ordered rules and saved approvals as separate inputs to evaluation. +- Establish action and resource conventions for `read`, `glob`, `grep`, `edit`, `external_directory`, `bash`, `todowrite`, and `webfetch` approvals. + +Reason: + +- Embedded tool calls need a Core-owned authorization boundary that can suspend and resume through HTTP. + +Compatibility: + +- These are additive experimental V2 contracts. +- Policy authors should account for canonical resource forms; originating tool source metadata remains optional until every registry call carries its durable assistant owner. + +### Initial Core V2 Built-In Tool Schemas + +Affected schema: + +- `read`, `glob`, `grep`, `write`, exact `edit`, `bash`, and `websearch` model-facing tool contracts. + +Change: + +- Add Core-owned Location-scoped built-ins with explicit parameter and success schemas. +- Bound bash output and timeout input, search result counts and previews, read sizes, directory pages, and websearch result/context controls. + +Reason: + +- Embedded runner launch requires a minimal typed tool set without importing legacy application orchestration. + +Compatibility: + +- These are additive V2 built-ins. +- Richer launch-follow-up leaves such as `apply_patch`, skill loading, task dispatch, and LSP remain separate slices. + +### Bash Advisory Warnings + +Affected schema: + +- Optional `warnings` in the `bash` tool success payload. + +Change: + +- Return advisory warning strings when best-effort command-argument scanning detects external absolute paths; keep structured external `workdir` approval enforced. + +Reason: + +- A shell subprocess has host-user filesystem, process, and network authority. Token scanning cannot honestly provide containment. + +Compatibility: + +- Consumers rendering bash success should tolerate optional warning strings. + +### V2 Session HTTP And Generated SDK Contracts + +Affected schema: + +- V2 Session list, prompt, context, message-list, compact, and wait HTTP routes. +- V2 Location query routing fields. +- Generated OpenAPI and JavaScript SDK schemas. + +Change: + +- Expose embedded Session creation and read-side behavior over the experimental HTTP API. +- Accept optional prompt admission `id`, `delivery`, and `resume` fields so callers can request idempotency, steering or queue semantics, and durable admission without immediate execution. +- Keep message cursors opaque and preserve configured Location routing through both legacy flat and nested `location[...]` query parameters in the V2 SDK client. + +Reason: + +- Remote and embedded consumers need one generated contract while Location middleware remains compatible with current server routing. + +Compatibility: + +- These are experimental V2 routes. +- Prompt admission now returns the admitted user-shaped message and may return a conflict error when one message ID is reused for different input. +- SDK Location GET rewriting preserves existing flat query behavior and adds nested compatibility parameters. + +## 2026-06-03: Durable Session Message Pagination + +Affected schema: + +- Internal `SessionV2.messages()` cursor input. +- Opaque cursor payload returned by `GET /api/session/:sessionID/message`. + +Change: + +- Remove wall-clock `time` from the message cursor payload. +- Resolve the opaque cursor's projected message `id` to its stored `session_message.seq`. +- Apply page boundaries and ordering with durable per-session `seq` rather than `time_created` plus `id`. + +Reason: + +- Projected V2 message chronology is defined by synchronized Session-event order. +- Wall-clock timestamps may collide or move backwards, so they are not safe pagination boundaries. +- The list endpoint must agree with replay and context loading, which already order by durable sequence. + +Compatibility: + +- No database migration is required. `session_message.seq` and its session-scoped index already exist. +- The HTTP cursor remains opaque and existing cursors remain usable because they already carry the projected message `id`; older extra `time` data is ignored while decoding. +- No OpenAPI or generated SDK schema changes are required for this pagination correction. + +## 2026-06-03: Public Provider And Model Catalog DTOs + +Affected schema: + +- Responses from `GET /api/provider`, `GET /api/provider/:providerID`, and `GET /api/model`. +- Generated `ProviderV2PublicInfo` and `ModelV2PublicInfo` SDK schemas. + +Change: + +- Replace internal catalog response schemas with explicit public DTOs. +- Remove provider request headers and bodies, API settings, custom enablement data, model request overrides, and variant request overrides from public responses. + +Reason: + +- Internal catalog records may contain credentials or provider-specific request material and must not cross the public HTTP serialization boundary. + +Compatibility: + +- Public V2 catalog responses intentionally expose fewer fields. +- Internal provider and model schemas remain available to the runtime. + +## 2026-06-03: Durable Reasoning And Hosted Tool Replay Metadata + +Affected schema: + +- Durable `session.next.reasoning.started` and `session.next.reasoning.ended` events. +- Durable `session.next.tool.success` and `session.next.tool.failed` events. +- Projected assistant reasoning and settled tool message state. + +Change: + +- Add optional reasoning `providerMetadata`. +- Add optional durable tool `result` and project it into settled tool message state. +- Preserve projected tool-call metadata separately from optional settlement-result metadata. +- Replay provider-native reasoning and tool metadata only when the historical assistant model matches the selected continuation model. + +Reason: + +- Provider continuation requires signed or encrypted reasoning metadata on later turns. +- Provider-executed hosted tool results must survive projection so replay can keep hosted calls and results inline in assistant content. +- Recovery settlement must not erase provider-native call metadata needed to reconstruct a valid continuation request. + +Compatibility: + +- Added durable-event fields are optional so previously recorded experimental events remain decodable. +- Projected settled tool state gains model-facing result data when available. +- Projected assistant tools gain optional result-side provider metadata; the existing metadata slot remains the backward-compatible call-side slot. +- OpenAI Responses lowers reconstructed provider-executed hosted results to stored item references instead of rejecting assistant history. +- Bedrock Converse signatures, Gemini `thoughtSignature`, and OpenAI-compatible Chat `reasoning_content` now round-trip through canonical continuation parts. + +## 2026-06-03: Projected Assistant Ownership And Full-Value Parts + +Affected schema: + +- Projected assistant text parts. +- Durable text and tool lifecycle boundaries. +- Projected assistant tool ownership. + +Change: + +- Preserve stable IDs on projected assistant text parts. +- Route durable tool projection updates through explicit owning `assistantMessageID` values rather than provider-local call IDs alone. +- Replay full-value text and tool-input end checkpoints while keeping fragment deltas ephemeral. + +Reason: + +- Provider-local tool call IDs may repeat across turns. +- Durable projection reconstruction must not depend on ephemeral fragments that disappear after reconnect. + +Compatibility: + +- Earlier experimental projected assistant rows without stable text IDs are not assumed replay-compatible. +- Current V2 histories reconstruct from durable full-value checkpoints. + +## 2026-06-03: Location-Scoped V2 Questions + +Affected schema: + +- New `QuestionV2.*` domain schemas. +- New `question.v2.asked`, `question.v2.replied`, and `question.v2.rejected` events. +- New question list, reply, and reject HTTP routes and generated SDK schemas. + +Change: + +- Add schemas for pending requests, question options, ordered answers, and tool ownership metadata. +- Add `GET /api/question/request`. +- Add `POST /api/session/:sessionID/question/request/:requestID/reply`. +- Add `POST /api/session/:sessionID/question/request/:requestID/reject`. + +Reason: + +- Embedded V2 tool execution needs a Location-owned pending-question service whose suspended replies can be settled through HTTP. + +Compatibility: + +- These are additive experimental V2 contracts. +- No database migration is required because pending questions are intentionally in-memory Location state. + +## 2026-06-03: Core-Owned Todo Update Event + +Affected schema: + +- Core-owned `SessionTodo.Info`. +- Global `todo.updated` event registration. + +Change: + +- Register the todo update event from Core session-todo ownership and expose the existing todo item shape to the Core V2 tool. + +Reason: + +- Embedded V2 `todowrite` execution needs Core-owned persistence and update publication without importing legacy application orchestration. + +Compatibility: + +- The todo table and public todo update event shape are preserved. +- No database migration is required. + +## 2026-06-03: Added Core V2 Tool Schemas + +Affected schema: + +- New `todowrite` tool parameters and success payload. +- New `question` tool parameters and success payload. +- New `webfetch` tool parameters and success payload. + +Change: + +- Add a todo replacement-list tool using `SessionTodo.Info` items. +- Add a question tool using ordered `QuestionV2.Prompt` values and ordered answer arrays. +- Add an HTTP(S) fetch tool with explicit `text`, `markdown`, and `html` formats, bounded timeout input, and optional managed output resource metadata. + +Reason: + +- Embedded V2 execution needs Core-owned built-ins rather than imports from legacy application orchestration. +- Explicit schemas keep model-facing definitions, runtime validation, and durable tool settlement aligned. + +Compatibility: + +- These are additive Location-scoped V2 built-ins. +- No database migration or public HTTP API migration is required. + +## 2026-06-03: Conditional File-Mutation Stale Error + +Affected schema: + +- New internal `FileMutation.StaleContentError` tagged error. + +Change: + +- Add a typed error carrying the mutation target path when an approved exact edit no longer matches the bytes at commit time. + +Reason: + +- V2 exact edits must fail rather than stale-clobber a concurrent cooperating write after permission approval. + +Compatibility: + +- This is an additive internal error contract. +- No database, HTTP, or generated SDK schema changes are required. + +## 2026-06-03: Provider Stream Watchdog Policy Deferred + +Affected schema: + +- No database, durable-event, HTTP, or generated SDK schema changes. +- Internal Session-runner provider-stream policy. + +Change: + +- Do not impose a universal provider-stream inactivity or absolute timeout. +- Remove the internal timeout error and hardcoded watchdog service. +- Defer provider timeout, retry, watchdog, durable failure-reporting, and drain-chain-release policy to a configurable design slice. + +Reason: + +- V1 had no universal processor inactivity watchdog. +- Providers and autonomous workloads have different runtime characteristics, so one hardcoded default is premature. + +Compatibility: + +- No migration or generated artifact regeneration is required. +- Embedded runner callers do not receive a runner-defined provider-stream timeout error. + +## 2026-06-03: Keyed Coalescing Durable Tail Signals + +Affected schema: + +- No database, durable-event, HTTP, or generated SDK schema changes. +- Internal durable aggregate-tail wake delivery only. + +Change: + +- Replace the process-global unbounded aggregate-ID PubSub with one sliding-capacity-1 dirty signal per active tail and aggregate. +- Subscribe and register the signal before historical SQLite replay, then remove it when the tail closes. +- Re-query durable rows after each dirty edge and advance only by persisted aggregate sequence. + +Reason: + +- Wake notifications are advisory edges, not durable event payloads. +- Slow consumers should not retain an unbounded number of redundant wake IDs when one SQLite query can recover every committed row after their cursor. +- Per-tail signaling preserves independent cursors for multiple consumers of the same aggregate. + +Compatibility: + +- No migration, synchronized event version, OpenAPI, or SDK regeneration is required. +- `sessions.events({ sessionID, after? })` remains a replay-and-tail stream of every durable event in aggregate sequence order. + +## 2026-06-03: Sequential V2 Apply Patch Tool + +Affected schema: + +- New Core-owned `apply_patch` model-facing tool parameters and success payload. +- New Core-owned pure patch hunk representation for add, update, and delete operations. + +Change: + +- Accept `{ patchText: string }` using the `*** Begin Patch` envelope. +- Return ordered applied-operation records carrying `type`, canonical `target`, and permission-facing `resource`. +- Resolve and approve every target before reading approved update/delete contents. +- Preflight update/delete correctness before committing operations sequentially. +- Report already-applied resources explicitly when a later commit fails. + +Reason: + +- Embedded V2 agents need reviewable multi-file edits without importing legacy application orchestration into Core. +- Sequential semantics are small and honest: they avoid claiming rollback or transactionality that path-based filesystem commits do not provide. + +Compatibility: + +- This is an additive model-facing V2 tool contract. +- Moves and atomic rollback are deliberately unsupported in the first slice and remain visible follow-ups. +- No database migration, durable-event version, public HTTP, OpenAPI, or generated SDK change is required. + +## 2026-06-03: Embedded Local-Tool Recovery Alignment + +Affected schema: + +- No database, durable-event, HTTP, or generated SDK schema changes. +- Internal runner recovery and permission evaluation behavior only. + +Change: + +- Evaluate permissions through the default `build` agent when a Session omits an explicit agent, matching provider-turn execution. +- Before assembling a provider request, durably fail local tools still projected as `running` from a previous process with the existing `session.next.tool.failed` shape and `Tool execution interrupted` message. + +Reason: + +- Agent-less embedded Sessions previously executed as `build` while evaluating an empty permission ruleset, so the first local tool could wait forever for an approval surface the local Discord proof did not expose. +- A process lost while a local tool was running previously left a dangling tool call that made later provider continuation invalid. Recovery must settle the durable projection without replaying an abandoned side effect. + +Compatibility: + +- No migration, synchronized event version, OpenAPI, or SDK regeneration is required. +- Existing experimental Session databases recover dangling local-tool projections on the next provider attempt. + +## 2026-06-03: V2 Skill Tool + +Affected schema: + +- New Core-owned `skill` model-facing tool parameters and success payload. +- Existing upstream `SkillV2` service remains the single Location-scoped skill registry. + +Change: + +- Accept `{ name: string }` for one skill selected from the upstream-discovered Location skill list. +- Assert `skill` permission for the selected name. +- Return V1-shaped `` model output with the skill base directory and a bounded sampled supporting-file list. + +Compatibility: + +- This is an additive model-facing V2 tool contract. +- No database migration, durable-event version, public HTTP, OpenAPI, or generated SDK change is required. + +## 2026-06-03: Pre-PR V2 Safety Review + +Affected schema: + +- V2 OpenAPI request bodies preserve requiredness instead of inheriting legacy optional-body normalization. +- Existing durable tool-failure and replay-owner schemas are reused without version changes. + +Change: + +- Fence replay envelopes whose aggregate ID differs from the decoded synchronized payload and persist owner claims when replay first adopts an existing unowned aggregate. +- Settle abandoned local and provider-executed tools durably before continuation; hosted failures preserve inline provider-executed replay. +- Give `apply_patch` add hunks create-only semantics, make sequential commits uninterruptible after preflight, and reject malformed patch grammar eagerly. +- Wait for initial plugin boot before materializing the `skill` built-in, discover conventional config-root skill directories, and resolve current skills again during execution. +- Sanitize provider and model public API URLs by stripping credentials, queries, and fragments. +- Keep V1-like `webfetch` network semantics: approve the requested HTTP(S) URL, allow ordinary hostnames, and delegate redirects to the HTTP transport. +- Keep V2 request bodies required in generated OpenAPI and SDK types. + +Compatibility: + +- No database migration is required. +- Pre-launch `session.next.*` databases remain disposable experimental state rather than compatibility targets; reset experimental V2 data when upgrading across incompatible event-schema iterations. +- V1 returns fetched images as attachments. The first Core V2 typed settlement remains text-only, so V2 continues to reject fetched images and other non-text files until attachment settlement is designed explicitly. + +## 2026-06-03: Defer V2 Bash Background Execution + +Affected schema: + +- Core V2 model-facing `bash` tool parameters and success payload. + +Change: + +- Remove the optional `background` bash parameter and process-local background settlement shape from the shipped tool. +- Retain the internal `BackgroundJob` prototype for a later integration slice. + +Reason: + +- The model has no registered observation or cancellation tool for background bash jobs, and process-local status is not a sufficient remote contract. + +Compatibility: + +- Foreground V2 bash execution is unchanged. +- Reintroduce background bash only with durable status observation, completion delivery, and explicit cancellation semantics. diff --git a/specs/v2/session.md b/specs/v2/session.md index cae90ba7c8..20cd4393ab 100644 --- a/specs/v2/session.md +++ b/specs/v2/session.md @@ -1,5 +1,99 @@ # Session API +## Current V2 Core Slice + +The Effect-native core facade treats prompt recording and execution as separate responsibilities: + +```text +sessions.create({ id?, location, ... }) + -> omitted ID generates one internal Session ID + -> supplied ID creates the Session when absent + -> reused ID returns the existing Session identity + +sessions.prompt({ id?, sessionID, prompt, delivery?, resume? }) + -> omitted ID generates one internal message ID + -> supplied ID admits one durable Session input when absent + -> exact reuse returns the same admitted user-shaped message + -> reusing one message ID for another Session, prompt, or delivery mode fails + -> exact retry schedules another wake unless resume is false + -> resume omitted or true schedules execution after admission + -> resume false admits only +``` + +`session_input` is the durable admission inbox. Admitted inputs remain outside model-visible Session history until the serialized runner promotes them by publishing ordinary `Prompted` events. The existing projector atomically writes the visible user message and marks its inbox row promoted in the same event transaction. + +Execution routing starts from only the Session ID: + +```text +SessionExecution.resume(sessionID) +-> SessionStore.get(sessionID) +-> LocationServiceMap.get(session.location) +-> SessionRunner.run({ sessionID, force? }) +``` + +`SessionExecution` and the read-side `SessionStore` are process-global. `SessionRunner`, catalog, model resolver, tool registry, permission state, and filesystem are cached per Location. No layer takes a Session ID. An omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics. + +The local runner issues one explicit `llm.stream(request)` per provider turn, projects each complete local tool call durably before eagerly starting its structured child execution, awaits every started tool fiber after provider-stream closure, reloads projected history once before continuation, and fails after 25 provider turns within one local drain activity only when work remains. Tool settlement events carry the owning assistant-message ID because provider-local call IDs may repeat across turns. Before assembling a provider request, the runner durably fails any local tool still projected as `running` from a previous process with `Tool execution interrupted`; abandoned side effects are never silently replayed. + +Projected hosted tools preserve call-side and settlement-side provider metadata separately so settlement and interruption recovery cannot erase continuation identifiers. Provider-native reasoning and provider metadata replay only while the historical assistant model matches the selected continuation model; after a model switch, visible reasoning text remains ordinary assistant text and provider-native metadata is omitted. + +Provider timeout, retry, and watchdog policy is intentionally deferred. The runner does not impose a universal provider-stream inactivity or absolute timeout. A future slice should design configurable policy around provider behavior, durable failure reporting, and local drain-chain release rather than hardcoding one default for every provider. + +Inbox delivery is explicit: + +- `steer` inputs promote at the next safe provider-turn boundary, including continuation inside the current drain. +- `queue` inputs form a FIFO of future activities. When the current activity settles, the runner promotes exactly one queued input to open the next activity. Multiple queued inputs remain separate activities. + +Execution has two entry points: + +- `run` is an explicit resume. It joins an active drain chain or starts one, and performs at least one provider attempt even when no input is eligible. +- `wake` reports newly recorded durable inbox work. Repeated wakes coalesce. A wake calls the provider only when it can promote eligible input. + +Post-crash activity recovery is intentionally deferred. A wake does not infer that ambiguous provider work is safe to retry after an input has already been promoted. Explicit `run` may deliberately continue from durable projected history. A future recovery slice should model durable activity identity, provider-dispatch ambiguity, required continuation, queue-opener reservation, retry policy, and visible recovery status together. + +A location-scoped `SessionRunCoordinator` serializes each Session drain chain while allowing different Sessions to drain concurrently. Automatic startup discovery, durable multi-node ownership, stale-owner fencing, interruption controls, and retry policy remain future work. + +Inbox promotion coalesces pending steers in durable admission order and opens one queued activity at a time in FIFO order. Add explicit inbox backlog and steering-batch limits before exposing broad multi-caller admission or untrusted queue growth. + +Eager local-tool execution is intentionally unbounded in the current local slice. This minimizes tool latency but does not increase SQLite settlement throughput: Session-event publication remains serialized per provider turn. Before broadening exposure, revisit per-turn call limits, output truncation, and operational backpressure using observed workloads. The `session.next.*` event schemas remain experimental and unshipped; databases created by earlier experimental builds are disposable rather than compatibility targets. + +The synchronized `session.next.*` event family and projected Session-message model predate this branch. This slice refines their replay contract: projected Session messages retain their source aggregate sequence so canonical context ordering and `sessions.messages(...)` pagination follow durable event order even when caller-supplied IDs or timestamps do not. Consumers can use `sessions.events({ sessionID, after? })` to replay durable `session.next.*` events after an aggregate sequence cursor, then tail durable events without a race. Live-only text, reasoning, and tool-input fragments remain available through EventV2 subscriptions for connected renderers; they are intentionally absent from the replayable Session stream. + +The first `sessions.events(...)` contract is durable-only during both replay and live tailing. This keeps one cursor equal to one persisted aggregate sequence and is sufficient for reconnect-safe consumers such as Discord publication. A later UI-facing API may optionally interleave live-only deltas while connected, but those fragments must remain explicitly ephemeral: they cannot advance the durable cursor, replay after reconnect, or be mistaken for publication boundaries. Until that contract is designed, connected renderers can combine `sessions.events(...)` with direct EventV2 delta subscriptions. + +Durable event tail wakeups are advisory and edge-triggered. Each active tail owns one sliding-capacity-1 dirty signal for its aggregate and re-queries SQLite after a wake. Repeated commits coalesce while the tail is busy because durable rows, not in-memory notifications, preserve every event and sequence. Subscribe and register the dirty signal before historical replay, then remove it when the tail closes, so replay handoff cannot miss a commit and inactive aggregates retain no wake state. + +Event replay owner claims are separate from clustered Session execution ownership. The former already fences synchronized projection reconstruction; the latter still needs distributed active-run acquisition, stale-runtime rejection, interruption, and placement orchestration. + +## Current Tool Registry Slice + +`ToolRegistry` is Location-scoped. Contributions are scoped replayable transforms: closing a contribution scope removes its definition and rebuilds the advertised catalog. Execution decodes input, optionally authorizes the call, invokes the retained handler, validates output, and settles failures as typed tool-result errors. + +When a Session omits `agent`, both execution and permission evaluation use the default `build` agent. A caller must not observe `build` model behavior while permission checks silently evaluate an empty no-agent policy. + +The first built-in contribution is bounded `read`: + +```text +resolve one path relative to the Location or a named project reference +-> reject absolute paths, path escapes, and symlink escapes +-> authorize read against the canonical resource identity +-> for a file: return UTF-8 text or base64 binary content; page oversized UTF-8 text by bounded line ranges +-> for a directory: return direct children in directory-first alphabetical order +-> page directory results with one-based offset and next cursor +``` + +V2 `bash` uses the normal permission semantics: configured agent rules plus saved project approvals, with `ask` as the default when no rule matches. Bash is not sandboxed: the spawned shell runs with the host user's filesystem, process, and network authority. Structured external `workdir` resolution remains an enforced `external_directory` authority check. Best-effort scans of absolute command arguments produce advisory warnings only; they are not sandbox boundaries and do not request or enforce `external_directory` approval. + +The first V2 `apply_patch` leaf supports add, update, and delete hunks. It parses every hunk, resolves every mutation target, approves external directories, approves one edit batch, and preflights approved update/delete targets before committing operations sequentially. A later commit-time failure leaves earlier operations applied and returns an explicit partial-application report. Moves and atomic rollback remain separate follow-ups rather than implied behavior. + +### Current Runner Follow-Ups + +- Keep eager structured local-tool settlement: durably record each complete call, start its child execution immediately, await all started settlements after provider-turn consumption, persist every result, and reload history once before continuation. +- Buffer or coalesce streamed deltas before rewriting growing assistant projections. +- Revisit additional covering indexes as larger-history query shapes become concrete. +- Expose replayable Session events over HTTP and the generated SDK where remote consumers need them, deciding whether that public cursor should be opaque rather than the embedded API's branded aggregate sequence. +- Decide whether UI-facing Session subscriptions should optionally interleave ephemeral deltas while connected without advancing the durable cursor. + ## Remove Dedicated `session.init` Route The dedicated `POST /session/:sessionID/init` endpoint exists only as a compatibility wrapper around the normal `/init` command flow. diff --git a/specs/v2/todo.md b/specs/v2/todo.md index ca38931f8f..ee18ebbd7b 100644 --- a/specs/v2/todo.md +++ b/specs/v2/todo.md @@ -15,8 +15,65 @@ and shell commands. ## Rework agent loop - Kit? -I think this needs to be done so we can take advantage of the simpler data -model. It can stop doing all the +The first Effect-native local runner slice is implemented without bridging +through legacy `SessionPrompt.loop(...)`: + +- process-global `SessionExecution.resume(sessionID)` discovers Location from + the Session read model +- cached Location-scoped `SessionRunner` resolves one supported catalog model + and issues one explicit `llm.stream(request)` provider turn at a time +- durable V2 projections record text, reasoning, provider failures, tool calls, + tool results, and assistant output +- a scoped `ToolRegistry` advertises definitions and the first permission-checked + `read` built-in +- local continuation reloads projected history and stops after 25 provider turns within one local drain activity +- concurrent resumes for one Session join one process-local run while different + Sessions remain concurrent + +Prompt admission now uses a durable `session_input` inbox rather than immediate +transcript projection. `steer` inputs coalesce into the active activity at the +next safe provider-turn boundary. `queue` inputs form a FIFO of future activities +that open one at a time. A location-scoped `SessionRunCoordinator` coalesces process-local wakeups +around settlement races. Explicit `run` resumes perform at least one provider +attempt; advisory `wake` notifications call the provider only for eligible inbox +work. Steers coalesce into the active activity at +safe provider boundaries; queued inputs open later activities one at a time in +FIFO order. + +Next reviewed slices: + +- preserve eager structured local-tool settlement: durably record each complete + call, start its child execution immediately, await every settlement after the + provider turn closes, then reload projected history once +- revisit per-turn tool-call limits, output truncation, and operational + backpressure before broadening exposure; eager local execution is deliberately + unbounded in the current local slice while SQLite publication stays serialized +- remove the public in-memory `@opencode-ai/llm` tool loop after replacing its + remaining one-turn native-adapter use with a narrow typed dispatcher +- batch streamed deltas and add covering context indexes +- expose replayable Session event cursors over HTTP and the generated SDK where remote consumers need them +- integrate the new BackgroundJob service with V2 tool execution: support background + bash jobs and background agent dispatch with durable status observation, + completion delivery, and explicit cancellation / continuation semantics +- add compaction, interruption, retries, and stale-owner fencing + only as their slices become concrete + +### Deferred durable activity recovery + +Do not infer that ambiguous provider work is safe to retry from an advisory wake. +The first inbox-driven runner intentionally omits outer provider-attempt markers +until they have a concrete consumer and a complete recovery policy. + +Design post-crash activity recovery as one explicit slice. It should model: + +- durable activity identity and settlement +- queue-opener reservation and steer assignment +- provider-attempt preparation versus provider-dispatch ambiguity +- required post-tool continuation across process loss +- explicit `retry` and `abandon` decisions for unknown outcomes +- bounded automatic retry only where provider and tool idempotency make it safe +- retry budget, backoff, visible recovery status, startup discovery, and future + clustered ownership fencing ## Rework compaction - Aiden? @@ -53,8 +110,42 @@ want / config. They should register models into model database ## Event - Kit -I have this v2/event.ts but it needs to be self contained instead of using the -old bus system +The self-contained durable `EventV2` core service is implemented. It owns +sync-versioned persistence, transactional sequencing, pub/sub, replay, and +replay-owner claims without relying on the old bus system. + +Remaining slices: + +- expose the embedded consumer-facing Session cursor API over HTTP and the + generated SDK where remote consumers need it +- keep replay-owner claims distinct from future clustered Session execution + ownership and stale-runtime fencing + +## Deferred hardening cleanup + +Keep these visible, but do not block functionality slices on them unless a concrete +failure appears during canary work: + +- serialize database migration claiming across processes; current migration + application is protected only by an in-process semaphore, so two processes + starting against one SQLite database can still race +- simplify process-local durable-tail wake lifecycle with Effect `RcMap` and one + shared `PubSub.sliding(1)` per active aggregate; keep SQLite cursor replay + and subscribe-before-history semantics unchanged +- page large durable aggregate replay reads instead of loading every row after a + stale cursor into one array +- decide whether connected tails need a periodic polling fallback for + cross-process SQLite writers; current advisory wakes are intentionally + process-local +- stream-cap websearch body collection before parsing +- add ripgrep execution timeout and bounded line framing +- materialize or consistently reject unresolved URL and file attachment sources +- decide stateless OpenAI Responses hosted-tool continuation behavior; reconstructed hosted output can replay as a stored `item_reference` when `store !== false`, while `store: false` intentionally omits the unavailable reference path +- decide whether to preserve deprecated `@opencode-ai/llm` orchestration exports +- preserve or alias renamed filesystem SDK generated type names if compatibility + consumers require them +- revisit syscall-level mutation confinement for hostile external processes + (`openat`, `O_NOFOLLOW`, and descriptor-relative mutation where supported) ## Everything is hotreloadable - ???